using CaoCao.XAsset; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace CaoCao.Editor { public enum PlayerAssetsSplitMode { SplitByAssetPacksWithInstallTime, IncludeAllAssets, ExcludeAllAssets } [CreateAssetMenu(fileName = nameof(Settings), menuName = "CaoCao/XAsset/" + nameof(Settings))] public class Settings : ScriptableObject { [Header("Player")] public string downloadURL = "http://127.0.0.1/"; [Tooltip("大版本号,不一样则需要下载新包")] public int BigVersion = 0; [Tooltip("安装包资源分离模式: \n" + "Split By Asset Packs With Install Time:安装包中只包含交付模式为 Install Time 的 Asset Packs 中的资源。 \n" + "Include All Assets 安装包中包含所有资源。 \n" + "Exclude All Assets 安装包中不包含资源。")] public PlayerAssetsSplitMode playerAssetsSplitMode = PlayerAssetsSplitMode.IncludeAllAssets; /// /// 是否将安装包内的资源二次打包,Android 设备上开启这个选项可以优化 IO 效率。 /// [Tooltip("是否为 AssetPack 配置中的资源生成实体文件")] public bool packPlayerAssets = true; /// /// 离线模式,运行时有效,开启后不会触发更新。 /// [Tooltip("离线模式,运行时有效,开启后不会触发更新")] public bool offlineMode; /// /// 查找依赖引用输出的文件名 /// [Tooltip("查找依赖引用输出的文件名")] public string findReferencesInBuildPath = "FindReferencesInBuild.txt"; /// /// 打包 bundle 的设置 /// public BundleSettings bundle = new BundleSettings(); /// /// 编辑器仿真模式,开启后,无需打包可以进入播放模式。 /// [Header("Development")] public bool simulationMode = true; /// /// 编辑器下是否开启仿真下载模式,开启后,无需把资源部署到服务器,就行运行真机的更新过程。 /// public bool simulationDownload = true; /// /// 编辑器仿真模式下,是否采集所有资源,需要更长的初始化耗时,但是可以及时对未采集的内容进行预警。 /// public bool collectAllAssets; /// /// 最大并行下载数量 /// [Header("Download")] [Range(1, 10)] public byte maxDownloads = 5; /// /// 最大错误重试次数 /// [Range(0, 5)] public byte maxRetryTimes = 3; private static string Filename => $"Assets/GameAssets/Config/XAsset/{nameof(Settings)}.asset"; public static BuildGroup GetAutoGroup() { return GetOrCreateAsset("Assets/GameAssets/Config/XAsset/Auto.asset", group => { group.bundleMode = BundleMode.PackByFile; group.addressMode = AddressMode.LoadByDependencies; }); } public PlayerAssets GetPlayerAssets() { var assets = CreateInstance(); assets.updateInfoURL = $"{downloadURL}{Assets.Bundles}/{Platform}/{UpdateInfo.Filename}"; assets.downloadURL = $"{downloadURL}{Assets.Bundles}/{Platform}"; assets.offlineMode = offlineMode; assets.packed = packPlayerAssets; assets.maxDownloads = maxDownloads; assets.maxRetryTimes = maxRetryTimes; return assets; } public static string PlatformCachePath => $"{Environment.CurrentDirectory}/{Assets.Bundles}Cache/{Platform}".Replace('\\', '/'); public static string PlatformDataPath => $"{Environment.CurrentDirectory.Replace('\\', '/')}/{Assets.Bundles}/{Platform}"; public static Platform Platform => GetPlatform(); private static Settings defaultSettings; public static Settings GetDefaultSettings() { if (defaultSettings != null) return defaultSettings; var assets = FindAssets(); defaultSettings = assets.Length > 0 ? assets[0] : GetOrCreateAsset(Filename); return defaultSettings; } public static void FindReferencesInBuild() { var selection = Selection.GetFiltered(SelectionMode.DeepAssets); var sb = new StringBuilder(); foreach (var o in selection) { var asset = GetAsset(AssetDatabase.GetAssetPath(o)); sb.AppendLine(asset.path); sb.AppendLine(" - References By"); foreach (var s in asset.referencesBy) sb.AppendLine($" - {s}"); } var settings = GetDefaultSettings(); if (File.Exists(settings.findReferencesInBuildPath)) File.Delete(settings.findReferencesInBuildPath); File.WriteAllText(settings.findReferencesInBuildPath, sb.ToString()); EditorUtility.OpenWithDefaultApp(settings.findReferencesInBuildPath); } public static Versions GetDefaultVersions() { var versions = XAsset.Utility.LoadFromFile(GetCachePath(Versions.Filename)); return versions; } public static string GetDataPath(string filename) { return $"{PlatformDataPath}/{filename}"; } public static string GetCachePath(string filename) { return $"{PlatformCachePath}/{filename}"; } private static Platform GetPlatform() { // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault switch (EditorUserBuildSettings.activeBuildTarget) { case BuildTarget.Android: return Platform.Android; case BuildTarget.StandaloneOSX: return Platform.OSX; case BuildTarget.StandaloneWindows: case BuildTarget.StandaloneWindows64: return Platform.Windows; case BuildTarget.iOS: return Platform.iOS; case BuildTarget.WebGL: return Platform.WebGL; case BuildTarget.StandaloneLinux64: return Platform.Linux; default: return Platform.Default; } } public static long GetLastWriteTime(string path) { var file = new FileInfo(path); return file.Exists ? file.LastAccessTime.ToFileTime() : 0; } public static string[] GetDependenciesWithoutCache(string assetPath) { var set = new HashSet(); set.UnionWith(AssetDatabase.GetDependencies(assetPath)); set.Remove(assetPath); var exclude = GetDefaultSettings().bundle.excludeFiles; // Unity 会存在场景依赖场景的情况。 set.RemoveWhere(s => s.EndsWith(".unity") || exclude.Exists(s.EndsWith)); return set.ToArray(); } public static T GetOrCreateAsset(string path, Action onCreate = null) where T : ScriptableObject { var asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) return asset; XAsset.Utility.CreateDirectoryIfNecessary(path); asset = CreateInstance(); AssetDatabase.CreateAsset(asset, path); onCreate?.Invoke(asset); return asset; } public static T[] FindAssets() where T : ScriptableObject { var builds = new List(); var guilds = AssetDatabase.FindAssets("t:" + typeof(T).FullName); foreach (var guild in guilds) { var assetPath = AssetDatabase.GUIDToAssetPath(guild); if (string.IsNullOrEmpty(assetPath)) continue; var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (asset == null) continue; builds.Add(asset); } return builds.ToArray(); } public static BundleSettings BundleSettings => GetDefaultSettings().bundle; public static string extension => BundleSettings.extension; /// /// [path, entry, bundle, group, return(bundle)] /// public static Func customPacker { get; set; } = null; public static Func customFilter { get; set; } = s => true; private static string GetDirectoryName(string path) { var dir = Path.GetDirectoryName(path); return !string.IsNullOrEmpty(dir) ? dir.Replace("\\", "/") : string.Empty; } public static string PackAsset(BuildAsset asset) { var assetPath = asset.path; var group = asset.group; if (group == null) { asset.addressMode = AddressMode.LoadByDependencies; return "auto"; } var entry = asset.entry; var bundle = group.name.ToLower(); var dir = GetDirectoryName(entry) + "/"; assetPath = assetPath.Replace(dir, ""); entry = entry.Replace(dir, ""); switch (group.bundleMode) { case BundleMode.PackTogether: bundle = group.name; break; case BundleMode.PackByFolder: bundle = GetDirectoryName(assetPath); break; case BundleMode.PackByFile: bundle = assetPath; break; case BundleMode.PackByTopSubFolder: if (!string.IsNullOrEmpty(entry)) { var pos = assetPath.IndexOf("/", entry.Length + 1, StringComparison.Ordinal); bundle = pos != -1 ? assetPath.Substring(0, pos) : entry; } else { CaoCao.XAsset.Logger.E($"invalid rootPath {assetPath}"); } break; case BundleMode.PackByRaw: bundle = assetPath; break; case BundleMode.PackByEntry: bundle = Path.GetFileNameWithoutExtension(entry); break; case BundleMode.PackByCustom: if (customPacker != null) bundle = customPacker?.Invoke(assetPath, entry, bundle, group); break; default: throw new ArgumentOutOfRangeException(); } return PackAsset(assetPath, bundle, group.build); } public static string PackAsset(string assetPath, string bundle, string build) { var settings = GetDefaultSettings().bundle; if (settings.packTogetherForAllShaders && settings.shaderExtensions.Exists(assetPath.EndsWith)) bundle = "shaders"; if (settings.packByFileForAllScenes && assetPath.EndsWith(".unity")) bundle = assetPath; bundle = bundle.Replace(" ", "").Replace("/", "_").Replace("-", "_").Replace(".", "_").ToLower(); bundle += settings.extension; if (!string.IsNullOrEmpty(build) && settings.splitBundleNameWithBuild) return $"{build.ToLower()}/{bundle}"; return $"{bundle}"; } public static BuildAsset GetAsset(string path) { return GetBuildAssetCache().GetAsset(path); } private static BuildAssetCache BuildAssetCache; public static string[] GetDependencies(string assetPath) { return GetBuildAssetCache().GetDependencies(assetPath); } public static BuildAssetCache GetBuildAssetCache() { if (BuildAssetCache != null) return BuildAssetCache; var path = AssetDatabase.GetAssetPath(GetDefaultSettings()); var dir = Path.GetDirectoryName(path); BuildAssetCache = GetOrCreateAsset($"{dir}/{nameof(BuildAssetCache)}.asset"); return BuildAssetCache; } private static BuildEntryCache BuildEntryCache; public static BuildEntryCache GetBuildEntryCache() { if (BuildEntryCache != null) return BuildEntryCache; var path = AssetDatabase.GetAssetPath(GetDefaultSettings()); var dir = Path.GetDirectoryName(path); BuildEntryCache = GetOrCreateAsset($"{dir}/{nameof(BuildEntryCache)}.asset"); return BuildEntryCache; } public static BuildAsset[] Collect(BuildGroup group) { var assets = new List(); if (group.entries == null) return assets.ToArray(); foreach (var entry in group.entries) GetAssets(group, entry, assets); return assets.ToArray(); } private static void GetAssets(BuildGroup group, Object entry, List assets) { var path = AssetDatabase.GetAssetPath(entry); if (string.IsNullOrEmpty(path)) return; var cache = GetBuildEntryCache(); var entryAsset = cache.GetEntry(path, group); assets.AddRange(entryAsset.assets); } public static void Collect(AssetPack config, ISet assets) { foreach (var asset in config.assets) switch (asset) { case BuildGroup group: { foreach (var groupAsset in Collect(group)) assets.Add(groupAsset.path); break; } case Build build: foreach (var group in build.parameters.groups) foreach (var groupAsset in Collect(group)) assets.Add(groupAsset.path); break; default: var assetPath = AssetDatabase.GetAssetPath(asset); if (Directory.Exists(assetPath)) { var guilds = AssetDatabase.FindAssets("", new[] { assetPath }); foreach (var guild in guilds) { var child = AssetDatabase.GUIDToAssetPath(guild); if (string.IsNullOrEmpty(child) || Directory.Exists(child) || assets.Contains(child)) continue; assets.Add(child); } } else { assets.Add(assetPath); } break; } } } }