using System; using UnityEngine; using UnityEditor; using UnityEngine.Assertions; using System.Collections.Generic; using System.Linq; using UnityEditor.IMGUI.Controls; using AssetBundleBrowser.AssetBundleDataSource; namespace AssetBundleBrowser.AssetBundleModel { /// /// Static class holding model data for Asset Bundle Browser tool. Data in Model is read from DataSource, but is not pushed. /// /// If not using a custom DataSource, then the data comes from the AssetDatabase. If you wish to alter the data from code, /// you should just push changes to the AssetDatabase then tell the Model to Rebuild(). If needed, you can also loop over /// Update() until it returns true to force all sub-items to refresh. /// /// public static class Model { private const string k_NewBundleBaseName = "newbundle"; private const string k_NewVariantBaseName = "newvariant"; internal static /*const*/ Color k_LightGrey = Color.grey * 1.5f; private static ABDataSource s_DataSource; private static BundleFolderConcreteInfo s_RootLevelBundles = new BundleFolderConcreteInfo("", null); private static List s_MoveData = new List(); private static List s_BundlesToUpdate = new List(); private static Dictionary s_GlobalAssetList = new Dictionary(); private static Dictionary> s_DependencyTracker = new Dictionary>(); private static bool s_InErrorState = false; private const string k_DefaultEmptyMessage = "Drag assets here or right-click to begin creating bundles."; private const string k_ProblemEmptyMessage = "There was a problem parsing the list of bundles. See console."; private static string s_EmptyMessageString; private static Texture2D s_folderIcon = null; private static Texture2D s_bundleIcon = null; private static Texture2D s_sceneIcon = null; /// /// If using a custom source of asset bundles, you can implement your own ABDataSource and set it here as the active /// DataSource. This will allow you to use the Browser with data that you provide. /// /// If no custom DataSource is provided, then the Browser will create one that feeds off of and into the /// AssetDatabase. /// /// public static ABDataSource DataSource { get { if (s_DataSource == null) s_DataSource = new AssetDatabaseABDataSource(); return s_DataSource; } set { s_DataSource = value; } } /// /// Update will loop over bundles that need updating and update them. It will only update one bundle /// per frame and will continue on the same bundle next frame until that bundle is marked as doneUpdating. /// By default, this will cause a very slow collection of dependency data as it will only update one bundle per /// public static bool Update() { bool shouldRepaint = false; ExecuteAssetMove(false); //this should never do anything. just a safety check. //TODO - look into EditorApplication callback functions. int size = s_BundlesToUpdate.Count; if (size > 0) { s_BundlesToUpdate[size - 1].Update(); s_BundlesToUpdate.RemoveAll(item => item.doneUpdating == true); if (s_BundlesToUpdate.Count == 0) { shouldRepaint = true; foreach (BundleInfo bundle in s_RootLevelBundles.GetChildList()) bundle.RefreshDupeAssetWarning(); } } return shouldRepaint; } internal static void ForceReloadData(TreeView tree) { s_InErrorState = false; Rebuild(); tree.Reload(); bool doneUpdating = s_BundlesToUpdate.Count == 0; EditorUtility.DisplayProgressBar("Updating Bundles", "", 0); int fullBundleCount = s_BundlesToUpdate.Count; while (!doneUpdating && !s_InErrorState) { int currCount = s_BundlesToUpdate.Count; EditorUtility.DisplayProgressBar("Updating Bundles", s_BundlesToUpdate[currCount - 1].displayName, (float) (fullBundleCount - currCount) / (float) fullBundleCount); doneUpdating = Update(); } EditorUtility.ClearProgressBar(); } /// /// Clears and rebuilds model data. /// public static void Rebuild() { s_RootLevelBundles = new BundleFolderConcreteInfo("", null); s_MoveData = new List(); s_BundlesToUpdate = new List(); s_GlobalAssetList = new Dictionary(); Refresh(); } internal static void AddBundlesToUpdate(IEnumerable bundles) { foreach (BundleInfo bundle in bundles) { bundle.ForceNeedUpdate(); s_BundlesToUpdate.Add(bundle); } } internal static void Refresh() { s_EmptyMessageString = k_ProblemEmptyMessage; if (s_InErrorState) return; string[] bundleList = ValidateBundleList(); if (bundleList != null) { s_EmptyMessageString = k_DefaultEmptyMessage; foreach (string bundleName in bundleList) AddBundleToModel(bundleName); AddBundlesToUpdate(s_RootLevelBundles.GetChildList()); } if (s_InErrorState) { s_RootLevelBundles = new BundleFolderConcreteInfo("", null); s_EmptyMessageString = k_ProblemEmptyMessage; } } internal static string[] ValidateBundleList() { string[] bundleList = DataSource.GetAllAssetBundleNames(); bool valid = true; HashSet bundleSet = new HashSet(); int index = 0; bool attemptedBundleReset = false; while (index < bundleList.Length) { string name = bundleList[index]; if (!bundleSet.Add(name)) { LogError("Two bundles share the same name: " + name); valid = false; } int lastDot = name.LastIndexOf('.'); if (lastDot > -1) { string bunName = name.Substring(0, lastDot); int extraDot = bunName.LastIndexOf('.'); if (extraDot > -1) { if (attemptedBundleReset) { string message = "Bundle name '" + bunName + "' contains a period."; message += " Internally Unity keeps 'bundleName' and 'variantName' separate, but externally treat them as 'bundleName.variantName'."; message += " If a bundleName contains a period, the build will (probably) succeed, but this tool cannot tell which portion is bundle and which portion is variant."; LogError(message); valid = false; } else { if (!DataSource.IsReadOnly()) DataSource.RemoveUnusedAssetBundleNames(); index = 0; bundleSet.Clear(); bundleList = DataSource.GetAllAssetBundleNames(); attemptedBundleReset = true; continue; } } if (bundleList.Contains(bunName)) { //there is a bundle.none and a bundle.variant coexisting. Need to fix that or return an error. if (attemptedBundleReset) { valid = false; string message = "Bundle name '" + bunName + "' exists without a variant as well as with variant '" + name.Substring(lastDot + 1) + "'."; message += " That is an illegal state that will not build and must be cleaned up."; LogError(message); } else { if (!DataSource.IsReadOnly()) DataSource.RemoveUnusedAssetBundleNames(); index = 0; bundleSet.Clear(); bundleList = DataSource.GetAllAssetBundleNames(); attemptedBundleReset = true; continue; } } } index++; } if (valid) return bundleList; else return null; } internal static bool BundleListIsEmpty() { return s_RootLevelBundles.GetChildList().Count() == 0; } internal static string GetEmptyMessage() { return s_EmptyMessageString; } internal static BundleInfo CreateEmptyBundle(BundleFolderInfo folder = null, string newName = null) { if (folder as BundleVariantFolderInfo != null) return CreateEmptyVariant(folder as BundleVariantFolderInfo); folder = folder == null ? s_RootLevelBundles : folder; string name = GetUniqueName(folder, newName); BundleNameData nameData; nameData = new BundleNameData(folder.m_Name.bundleName, name); return AddBundleToFolder(folder, nameData); } internal static BundleInfo CreateEmptyVariant(BundleVariantFolderInfo folder) { string name = GetUniqueName(folder, k_NewVariantBaseName); string variantName = folder.m_Name.bundleName + "." + name; BundleNameData nameData = new BundleNameData(variantName); return AddBundleToFolder(folder.parent, nameData); } internal static BundleFolderInfo CreateEmptyBundleFolder(BundleFolderConcreteInfo folder = null) { folder = folder == null ? s_RootLevelBundles : folder; string name = GetUniqueName(folder) + "/dummy"; BundleNameData nameData = new BundleNameData(folder.m_Name.bundleName, name); return AddFoldersToBundle(s_RootLevelBundles, nameData); } private static BundleInfo AddBundleToModel(string name) { if (name == null) return null; BundleNameData nameData = new BundleNameData(name); BundleFolderInfo folder = AddFoldersToBundle(s_RootLevelBundles, nameData); BundleInfo currInfo = AddBundleToFolder(folder, nameData); return currInfo; } private static BundleFolderConcreteInfo AddFoldersToBundle(BundleFolderInfo root, BundleNameData nameData) { BundleInfo currInfo = root; BundleFolderConcreteInfo folder = currInfo as BundleFolderConcreteInfo; int size = nameData.pathTokens.Count; for (int index = 0; index < size; index++) if (folder != null) { currInfo = folder.GetChild(nameData.pathTokens[index]); if (currInfo == null) { currInfo = new BundleFolderConcreteInfo(nameData.pathTokens, index + 1, folder); folder.AddChild(currInfo); } folder = currInfo as BundleFolderConcreteInfo; if (folder == null) { s_InErrorState = true; LogFolderAndBundleNameConflict(currInfo.m_Name.fullNativeName); break; } } return currInfo as BundleFolderConcreteInfo; } private static void LogFolderAndBundleNameConflict(string name) { string message = "Bundle '"; message += name; message += "' has a name conflict with a bundle-folder."; message += "Display of bundle data and building of bundles will not work."; message += "\nDetails: If you name a bundle 'x/y', then the result of your build will be a bundle named 'y' in a folder named 'x'. You thus cannot also have a bundle named 'x' at the same level as the folder named 'x'."; LogError(message); } private static BundleInfo AddBundleToFolder(BundleFolderInfo root, BundleNameData nameData) { BundleInfo currInfo = root.GetChild(nameData.shortName); if (!string.IsNullOrEmpty(nameData.variant)) { if (currInfo == null) { currInfo = new BundleVariantFolderInfo(nameData.bundleName, root); root.AddChild(currInfo); } BundleVariantFolderInfo folder = currInfo as BundleVariantFolderInfo; if (folder == null) { string message = "Bundle named " + nameData.shortName; message += " exists both as a standard bundle, and a bundle with variants. "; message += "This message is not supported for display or actual bundle building. "; message += "You must manually fix bundle naming in the inspector."; LogError(message); return null; } currInfo = folder.GetChild(nameData.variant); if (currInfo == null) { currInfo = new BundleVariantDataInfo(nameData.fullNativeName, folder); folder.AddChild(currInfo); } } else { if (currInfo == null) { currInfo = new BundleDataInfo(nameData.fullNativeName, root); root.AddChild(currInfo); } else { BundleDataInfo dataInfo = currInfo as BundleDataInfo; if (dataInfo == null) { s_InErrorState = true; LogFolderAndBundleNameConflict(nameData.fullNativeName); } } } return currInfo; } private static string GetUniqueName(BundleFolderInfo folder, string suggestedName = null) { suggestedName = suggestedName == null ? k_NewBundleBaseName : suggestedName; string name = suggestedName; int index = 1; bool foundExisting = folder.GetChild(name) != null; while (foundExisting) { name = suggestedName + index; index++; foundExisting = folder.GetChild(name) != null; } return name; } internal static BundleTreeItem CreateBundleTreeView() { return s_RootLevelBundles.CreateTreeView(-1); } internal static AssetTreeItem CreateAssetListTreeView(IEnumerable selectedBundles) { AssetTreeItem root = new AssetTreeItem(); if (selectedBundles != null) foreach (BundleInfo bundle in selectedBundles) bundle.AddAssetsToNode(root); return root; } internal static bool HandleBundleRename(BundleTreeItem item, string newName) { BundleNameData originalName = new BundleNameData(item.bundle.m_Name.fullNativeName); int findDot = newName.LastIndexOf('.'); int findSlash = newName.LastIndexOf('/'); int findBSlash = newName.LastIndexOf('\\'); if (findDot == 0 || findSlash == 0 || findBSlash == 0) return false; //can't start a bundle with a / or . bool result = item.bundle.HandleRename(newName, 0); if (findDot > 0 || findSlash > 0 || findBSlash > 0) item.bundle.parent.HandleChildRename(newName, string.Empty); ExecuteAssetMove(); BundleInfo node = FindBundle(originalName); if (node != null) { string message = "Failed to rename bundle named: "; message += originalName.fullNativeName; message += ". Most likely this is due to the bundle being assigned to a folder in your Assets directory, AND that folder is either empty or only contains assets that are explicitly assigned elsewhere."; Debug.LogError(message); } return result; } internal static void HandleBundleReparent(IEnumerable bundles, BundleFolderInfo parent) { parent = parent == null ? s_RootLevelBundles : parent; foreach (BundleInfo bundle in bundles) bundle.HandleReparent(parent.m_Name.bundleName, parent); ExecuteAssetMove(); } internal static void HandleBundleMerge(IEnumerable bundles, BundleDataInfo target) { foreach (BundleInfo bundle in bundles) bundle.HandleDelete(true, target.m_Name.bundleName, target.m_Name.variant); ExecuteAssetMove(); } internal static void HandleBundleDelete(IEnumerable bundles) { List nameList = new List(); foreach (BundleInfo bundle in bundles) { nameList.Add(bundle.m_Name); bundle.HandleDelete(true); } ExecuteAssetMove(); //check to see if any bundles are still there... foreach (BundleNameData name in nameList) { BundleInfo node = FindBundle(name); if (node != null) { string message = "Failed to delete bundle named: "; message += name.fullNativeName; message += ". Most likely this is due to the bundle being assigned to a folder in your Assets directory, AND that folder is either empty or only contains assets that are explicitly assigned elsewhere."; Debug.LogError(message); } } } internal static BundleInfo FindBundle(BundleNameData name) { BundleInfo currNode = s_RootLevelBundles; foreach (string token in name.pathTokens) if (currNode is BundleFolderInfo) { currNode = (currNode as BundleFolderInfo).GetChild(token); if (currNode == null) return null; } else { return null; } if (currNode is BundleFolderInfo) { currNode = (currNode as BundleFolderInfo).GetChild(name.shortName); if (currNode is BundleVariantFolderInfo) currNode = (currNode as BundleVariantFolderInfo).GetChild(name.variant); return currNode; } else { return null; } } internal static BundleInfo HandleDedupeBundles(IEnumerable bundles, bool onlyOverlappedAssets) { BundleInfo newBundle = CreateEmptyBundle(); HashSet dupeAssets = new HashSet(); HashSet fullAssetList = new HashSet(); //if they were just selected, then they may still be updating. bool doneUpdating = s_BundlesToUpdate.Count == 0; while (!doneUpdating) doneUpdating = Update(); foreach (BundleInfo bundle in bundles) foreach (AssetInfo asset in bundle.GetDependencies()) if (onlyOverlappedAssets) { if (!fullAssetList.Add(asset.fullAssetName)) dupeAssets.Add(asset.fullAssetName); } else { if (asset.IsMessageSet(MessageSystem.MessageFlag.AssetsDuplicatedInMultBundles)) dupeAssets.Add(asset.fullAssetName); } if (dupeAssets.Count == 0) return null; MoveAssetToBundle(dupeAssets, newBundle.m_Name.bundleName, string.Empty); ExecuteAssetMove(); return newBundle; } internal static BundleInfo HandleConvertToVariant(BundleDataInfo bundle) { bundle.HandleDelete(true, bundle.m_Name.bundleName, k_NewVariantBaseName); ExecuteAssetMove(); BundleVariantFolderInfo root = bundle.parent.GetChild(bundle.m_Name.shortName) as BundleVariantFolderInfo; if (root != null) { return root.GetChild(k_NewVariantBaseName); } else { //we got here because the converted bundle was empty. BundleVariantFolderInfo vfolder = new BundleVariantFolderInfo(bundle.m_Name.bundleName, bundle.parent); BundleVariantDataInfo vdata = new BundleVariantDataInfo(bundle.m_Name.bundleName + "." + k_NewVariantBaseName, vfolder); bundle.parent.AddChild(vfolder); vfolder.AddChild(vdata); return vdata; } } internal class ABMoveData { internal string assetName; internal string bundleName; internal string variantName; internal ABMoveData(string asset, string bundle, string variant) { assetName = asset; bundleName = bundle; variantName = variant; } internal void Apply() { if (!DataSource.IsReadOnly()) DataSource.SetAssetBundleNameAndVariant(assetName, bundleName, variantName); } } internal static void MoveAssetToBundle(AssetInfo asset, string bundleName, string variant) { s_MoveData.Add(new ABMoveData(asset.fullAssetName, bundleName, variant)); } internal static void MoveAssetToBundle(string assetName, string bundleName, string variant) { s_MoveData.Add(new ABMoveData(assetName, bundleName, variant)); } internal static void MoveAssetToBundle(IEnumerable assets, string bundleName, string variant) { foreach (AssetInfo asset in assets) MoveAssetToBundle(asset, bundleName, variant); } internal static void MoveAssetToBundle(IEnumerable assetNames, string bundleName, string variant) { foreach (string assetName in assetNames) MoveAssetToBundle(assetName, bundleName, variant); } internal static void ExecuteAssetMove(bool forceAct = true) { int size = s_MoveData.Count; if (forceAct) { if (size > 0) { bool autoRefresh = EditorPrefs.GetBool("kAutoRefresh"); EditorPrefs.SetBool("kAutoRefresh", false); AssetDatabase.StartAssetEditing(); EditorUtility.DisplayProgressBar("Moving assets to bundles", "", 0); for (int i = 0; i < size; i++) { EditorUtility.DisplayProgressBar("Moving assets to bundle " + s_MoveData[i].bundleName, System.IO.Path.GetFileNameWithoutExtension(s_MoveData[i].assetName), (float) i / (float) size); s_MoveData[i].Apply(); } EditorUtility.ClearProgressBar(); AssetDatabase.StopAssetEditing(); EditorPrefs.SetBool("kAutoRefresh", autoRefresh); s_MoveData.Clear(); } if (!DataSource.IsReadOnly()) DataSource.RemoveUnusedAssetBundleNames(); Refresh(); } } //this version of CreateAsset is only used for dependent assets. internal static AssetInfo CreateAsset(string name, AssetInfo parent) { if (ValidateAsset(name)) { string bundleName = GetBundleName(name); return CreateAsset(name, bundleName, parent); } return null; } internal static AssetInfo CreateAsset(string name, string bundleName) { if (ValidateAsset(name)) return CreateAsset(name, bundleName, null); return null; } private static AssetInfo CreateAsset(string name, string bundleName, AssetInfo parent) { if (!string.IsNullOrEmpty(bundleName)) { return new AssetInfo(name, bundleName); } else { AssetInfo info = null; if (!s_GlobalAssetList.TryGetValue(name, out info)) { info = new AssetInfo(name, string.Empty); s_GlobalAssetList.Add(name, info); } info.AddParent(parent.displayName); return info; } } internal static bool ValidateAsset(string name) { if (!name.StartsWith("Assets/")) return false; string ext = System.IO.Path.GetExtension(name); if (ext == ".dll" || ext == ".cs" || ext == ".meta" || ext == ".js" || ext == ".boo") return false; return true; } internal static string GetBundleName(string asset) { return DataSource.GetAssetBundleName(asset); } internal static int RegisterAsset(AssetInfo asset, string bundle) { if (s_DependencyTracker.ContainsKey(asset.fullAssetName)) { s_DependencyTracker[asset.fullAssetName].Add(bundle); int count = s_DependencyTracker[asset.fullAssetName].Count; if (count > 1) asset.SetMessageFlag(MessageSystem.MessageFlag.AssetsDuplicatedInMultBundles, true); return count; } HashSet bundles = new HashSet(); bundles.Add(bundle); s_DependencyTracker.Add(asset.fullAssetName, bundles); return 1; } internal static void UnRegisterAsset(AssetInfo asset, string bundle) { if (s_DependencyTracker == null || asset == null) return; if (s_DependencyTracker.ContainsKey(asset.fullAssetName)) { s_DependencyTracker[asset.fullAssetName].Remove(bundle); int count = s_DependencyTracker[asset.fullAssetName].Count; switch (count) { case 0: s_DependencyTracker.Remove(asset.fullAssetName); break; case 1: asset.SetMessageFlag(MessageSystem.MessageFlag.AssetsDuplicatedInMultBundles, false); break; default: break; } } } internal static IEnumerable CheckDependencyTracker(AssetInfo asset) { if (s_DependencyTracker.ContainsKey(asset.fullAssetName)) return s_DependencyTracker[asset.fullAssetName]; return new HashSet(); } //TODO - switch local cache server on and utilize this method to stay up to date. //static List m_importedAssets = new List(); //static List m_deletedAssets = new List(); //static List> m_movedAssets = new List>(); //class AssetBundleChangeListener : AssetPostprocessor //{ // static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) // { // m_importedAssets.AddRange(importedAssets); // m_deletedAssets.AddRange(deletedAssets); // for (int i = 0; i < movedAssets.Length; i++) // m_movedAssets.Add(new KeyValuePair(movedFromAssetPaths[i], movedAssets[i])); // //m_dirty = true; // } //} internal static void LogError(string message) { Debug.LogError("AssetBundleBrowser: " + message); } internal static void LogWarning(string message) { Debug.LogWarning("AssetBundleBrowser: " + message); } internal static Texture2D GetFolderIcon() { if (s_folderIcon == null) FindBundleIcons(); return s_folderIcon; } internal static Texture2D GetBundleIcon() { if (s_bundleIcon == null) FindBundleIcons(); return s_bundleIcon; } internal static Texture2D GetSceneIcon() { if (s_sceneIcon == null) FindBundleIcons(); return s_sceneIcon; } private static void FindBundleIcons() { s_folderIcon = EditorGUIUtility.FindTexture("Folder Icon"); string packagePath = System.IO.Path.GetFullPath("Packages/com.unity.assetbundlebrowser"); if (System.IO.Directory.Exists(packagePath)) { s_bundleIcon = (Texture2D) AssetDatabase.LoadAssetAtPath("Packages/com.unity.assetbundlebrowser/Editor/Icons/ABundleBrowserIconY1756Basic.png", typeof(Texture2D)); s_sceneIcon = (Texture2D) AssetDatabase.LoadAssetAtPath("Packages/com.unity.assetbundlebrowser/Editor/Icons/ABundleBrowserIconY1756Scene.png", typeof(Texture2D)); } } } }