using System; using System.IO; using System.Linq; using System.Collections.Generic; using System.Reflection; using UnityEditor.SceneManagement; using UnityEditor.U2D.Sprites; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.U2D.Animation; using Object = UnityEngine.Object; namespace UnityEditor.U2D.Animation.Upgrading { internal class SpriteLibUpgrader : BaseUpgrader { static class Contents { public static readonly string ProgressBarTitle = L10n.Tr("Upgrading Sprite Library Assets"); public static readonly string VerifyingSelection = L10n.Tr("Verifying the selection"); public static readonly string CreatingNewLibraries = L10n.Tr("Creating new Sprite Library Assets"); public static readonly string ReassignAssetsInComponents = L10n.Tr("Re-assigning assets in components"); public static readonly string RemoveOldSpriteLibraries = L10n.Tr("Removing old Sprite Library Assets"); } const string k_SpriteLibTypeId = "t:SpriteLibraryAsset"; const string k_PsbImporterCategoriesId = "m_SpriteCategoryList.m_Categories"; static readonly Dictionary> k_SpriteLibraryReferenceLookup; bool m_OnlyFindOldAssets; bool m_OnlySearchInAssets; HashSet m_IndicesWithAssetBundleConnection = new HashSet(); HashSet m_AssetBundlesNeedingUpgrade = new HashSet(); static SpriteLibUpgrader() { k_SpriteLibraryReferenceLookup = GetSpriteLibReferenceLookup(); } /// Set this to true if you only want to find .asset SpriteLibraries /// Set this to true if you only want to find SpriteLibraries in the Assets/ folder or its children public SpriteLibUpgrader(bool onlyFindOldAssets = true, bool onlySearchInAssets = true) { m_OnlyFindOldAssets = onlyFindOldAssets; m_OnlySearchInAssets = onlySearchInAssets; } static Dictionary> GetSpriteLibReferenceLookup() { var result = new Dictionary>(); var allObjectsWithSpriteLibProperties = TypeCache .GetTypesDerivedFrom() .Where(type => type .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Any(HasSpriteLibField)); foreach (var property in allObjectsWithSpriteLibProperties) { if (!result.ContainsKey(property)) result.Add(property, new List()); var libraryFields = property .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Where(HasSpriteLibField) .ToList(); foreach (var field in libraryFields) { result[property].Add(field); } } return result; } static bool HasSpriteLibField(FieldInfo field) { return (field.FieldType == typeof(SpriteLibraryAsset) || field.FieldType == typeof(SpriteLibraryAsset[]) || field.FieldType == typeof(List)) && (field.IsPublic || field.IsDefined(typeof(SerializeField))); } internal override List GetUpgradableAssets() { var rootFolder = m_OnlySearchInAssets ? new[] { "Assets" } : null; var assetPaths = AssetDatabase.FindAssets(k_SpriteLibTypeId, rootFolder) .Select(AssetDatabase.GUIDToAssetPath).ToArray(); if (m_OnlyFindOldAssets) { assetPaths = assetPaths .Where(x => x.EndsWith(".asset") || UpgradeUtilities.IsPsbImportedFile(x)) .ToArray(); } var assets = assetPaths .Select(AssetDatabase.LoadAssetAtPath) .Cast() .ToList(); return assets; } internal override UpgradeReport UpgradeSelection(List objects) { EditorUtility.DisplayProgressBar(Contents.ProgressBarTitle, Contents.VerifyingSelection, 0f); var entries = new List(); var libraryIndexPairs = new Dictionary(); string msg; foreach (var obj in objects) { if (obj.Target == null) { msg = "The upgrade failed. Invalid selection."; m_Logger.Add(msg); m_Logger.AddLineBreak(); entries.Add(new UpgradeEntry() { Result = UpgradeResult.Error, Target = obj.Target, Index = obj.Index, Message = msg }); } else if (obj.Target is SpriteLibraryAsset lib) { libraryIndexPairs.Add(obj.Index, lib); } else { msg = $"The upgrade failed. {obj.Target.name} is not a SpriteLibraryAsset."; m_Logger.Add(msg); m_Logger.AddLineBreak(); entries.Add(new UpgradeEntry() { Result = UpgradeResult.Error, Target = obj.Target, Index = obj.Index, Message = msg }); } } var oldLibraries = libraryIndexPairs.Values.ToList(); EditorUtility.DisplayProgressBar(Contents.ProgressBarTitle, Contents.CreatingNewLibraries, 0.2f); m_Logger.AddLineBreak(); m_Logger.Add(Contents.CreatingNewLibraries); var newSourceAssetPaths = CreateNewAssetLibraries(libraryIndexPairs); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); var newLibraries = LoadNewLibraries(newSourceAssetPaths); EditorUtility.DisplayProgressBar(Contents.ProgressBarTitle, Contents.ReassignAssetsInComponents, 0.4f); m_Logger.AddLineBreak(); m_Logger.Add(Contents.ReassignAssetsInComponents); var assetPaths = GetAllAssetPaths(libraryIndexPairs); if (assetPaths.Length > 0) ReassignAssets(assetPaths, oldLibraries, newLibraries); EditorUtility.DisplayProgressBar(Contents.ProgressBarTitle, Contents.RemoveOldSpriteLibraries, 0.8f); m_Logger.AddLineBreak(); m_Logger.Add(Contents.RemoveOldSpriteLibraries); RemoveOldLibraries(oldLibraries); AssetDatabase.Refresh(); for (var i = 0; i < newLibraries.Count; ++i) { UpgradeResult result; if (m_IndicesWithAssetBundleConnection.Contains(newSourceAssetPaths[i].Item1)) { msg = $"Successfully replaced {newLibraries[i].name}. Note that the asset is connected to an AssetBundle. Make sure to rebuild this AssetBundle to complete the upgrade. See more info in the upgrade log."; result = UpgradeResult.Warning; } else { msg = $"Successfully replaced {newLibraries[i].name}"; result = UpgradeResult.Successful; } m_Logger.Add(msg); entries.Add(new UpgradeEntry() { Result = result, Target = newLibraries[i], Index = newSourceAssetPaths[i].Item1, Message = msg }); } EditorUtility.ClearProgressBar(); if (m_AssetBundlesNeedingUpgrade.Count > 0) AddAssetBundlesToLog(); var report = new UpgradeReport() { UpgradeEntries = entries, Log = m_Logger.GetLog() }; m_Logger.Clear(); m_AssetBundlesNeedingUpgrade.Clear(); return report; } static string[] GetAllAssetPaths(Dictionary spriteLibraries) { var ids = new List(); foreach(var lib in spriteLibraries.Values) ids.Add(GetObjectIDString(lib)); var spriteLibIds = ids.ToArray(); var assetPaths = new List(); var allAssetPaths = AssetDatabase.GetAllAssetPaths(); foreach (var path in allAssetPaths) { if (!IsPrefabOrScenePath(path, spriteLibIds)) continue; assetPaths.Add(path); } return assetPaths.ToArray(); } static string GetObjectIDString(Object obj) { if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj.GetInstanceID(), out string guid, out long localId)) return "fileID: " + localId + ", guid: " + guid; return null; } static bool IsPrefabOrScenePath(string path, string[] ids) { if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); if (path.StartsWith("Packages")) return false; if (path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase)) return DoesFileContainString(path, ids); return false; } static bool DoesFileContainString(string path, string[] strings) { if (strings != null && strings.Length > 0) { using (var file = File.OpenText(path)) { string line; while ((line = file.ReadLine()) != null) { for (var i = 0; i < strings.Length; i++) { if (line.Contains(strings[i])) return true; } } } } return false; } List<(int, string)> CreateNewAssetLibraries(Dictionary spriteLibraries) { var newLibraryPaths = new List<(int, string)>(); foreach (var pair in spriteLibraries) { var sourceAsset = pair.Value; var path = AssetDatabase.GetAssetPath(sourceAsset); var currentAssetPath = Path.GetDirectoryName(path); var fileName = Path.GetFileNameWithoutExtension(path); var convertFileName = fileName + SpriteLibrarySourceAsset.extension; convertFileName = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(currentAssetPath, convertFileName)); var destAsset = ScriptableObject.CreateInstance(); destAsset.SetLibrary(new List(sourceAsset.categories.Count)); foreach (var sourceCat in sourceAsset.categories) { var destCat = new SpriteLibCategoryOverride() { overrideEntries = new List(sourceCat.categoryList.Count), name = sourceCat.name, entryOverrideCount = sourceCat.categoryList.Count, fromMain = false }; destAsset.AddCategory(destCat); foreach (var entry in sourceCat.categoryList) { destCat.overrideEntries.Add(new SpriteCategoryEntryOverride() { name = entry.name, sprite = entry.sprite, fromMain = false, spriteOverride = entry.sprite }); } } var assetBundle = AssetDatabase.GetImplicitAssetBundleName(path); if (!string.IsNullOrEmpty(assetBundle)) { m_AssetBundlesNeedingUpgrade.Add(assetBundle); m_IndicesWithAssetBundleConnection.Add(pair.Key); m_Logger.Add($"{sourceAsset.name} is connected with the following AssetBundle: {assetBundle}."); } SpriteLibrarySourceAssetImporter.SaveSpriteLibrarySourceAsset(destAsset, convertFileName); newLibraryPaths.Add(new ValueTuple(pair.Key, convertFileName)); m_Logger.Add($"Created a new SpriteLibrary with the data of {sourceAsset.name}. The new SpriteLibrary is located at: {convertFileName}"); } return newLibraryPaths; } static List LoadNewLibraries(List<(int, string)> sourceAssetPaths) { var newLibraries = new List(); foreach (var path in sourceAssetPaths) { var newLibraryAsset = AssetDatabase.LoadAssetAtPath(path.Item2); newLibraries.Add(newLibraryAsset); } return newLibraries; } void ReassignAssets(string[] assetPaths, List oldLibraries, List newLibraries) { var index = 0; foreach (var assetPath in assetPaths) { m_Logger.Add($"Scanning {assetPath} for components with SpriteLibraryAsset references in need of reassignment."); var ext = Path.GetExtension(assetPath); if (ext == ".prefab") UpgradePrefab(assetPath, oldLibraries, newLibraries, UpgradeGameObject); else if (ext == ".unity") UpgradeScene(assetPath, oldLibraries, newLibraries, UpgradeGameObject); var assetBundle = AssetDatabase.GetImplicitAssetBundleName(assetPath); if (!string.IsNullOrEmpty(assetBundle)) { m_AssetBundlesNeedingUpgrade.Add(assetBundle); m_IndicesWithAssetBundleConnection.Add(index); } index++; } } static void UpgradePrefab(string path, List oldLibraries, List newLibraries, Action, List> objectUpgrader) { var objects = AssetDatabase.LoadAllAssetsAtPath(path); var firstIndex = 0; for (var i = 0; i < objects.Length; i++) { if (objects[i] as GameObject) { firstIndex = i; break; } } if (!PrefabUtility.IsPartOfImmutablePrefab(objects[firstIndex])) { foreach (var obj in objects) { var go = obj as GameObject; if (go != null) { objectUpgrader(go, oldLibraries, newLibraries); } } var asset = objects[firstIndex] as GameObject; PrefabUtility.SavePrefabAsset(asset.transform.root.gameObject); } } static void UpgradeScene(string path, List oldLibraries, List newLibraries, Action, List> objectUpgrader) { var scene = default(Scene); var openedByUser = false; for (var i = 0; i < SceneManager.sceneCount && !openedByUser; i++) { scene = SceneManager.GetSceneAt(i); if (path == scene.path) openedByUser = true; } if (!openedByUser) scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Additive); var gameObjects = scene.GetRootGameObjects(); foreach (var go in gameObjects) objectUpgrader(go, oldLibraries, newLibraries); EditorSceneManager.SaveScene(scene); if (!openedByUser) EditorSceneManager.CloseScene(scene, true); } void UpgradeGameObject(GameObject go, List oldLibraries, List newLibraries) { var types = k_SpriteLibraryReferenceLookup.Keys; foreach (var referenceType in types) { var components = go.GetComponentsInChildren(referenceType); foreach (var component in components) { if (PrefabUtility.IsPartOfPrefabInstance(component)) continue; var fieldInfos = k_SpriteLibraryReferenceLookup[referenceType]; foreach (var field in fieldInfos) { var asset = field.GetValue(component); if (asset is SpriteLibraryAsset spriteLibAsset) { var index = oldLibraries.FindIndex(x => x.GetHashCode() == spriteLibAsset.GetHashCode()); if (index == -1) continue; field.SetValue(component, newLibraries[index]); m_Logger.Add($"Updated the SpriteLibraryAsset reference in {component.GetType()} on the GameObject {component.name}"); } else if (asset is SpriteLibraryAsset[] spriteLibArray) { for (var i = 0; i < spriteLibArray.Length; ++i) { var index = oldLibraries.FindIndex(x => x.GetHashCode() == spriteLibArray[i].GetHashCode()); if (index == -1) continue; spriteLibArray[i] = newLibraries[index]; } field.SetValue(component, spriteLibArray); m_Logger.Add($"Updated the SpriteLibraryAsset[] reference in {component.GetType()} on the GameObject {component.name}"); } else if (asset is List spriteLibList) { for (var i = 0; i < spriteLibList.Count; ++i) { var index = oldLibraries.FindIndex(x => x.GetHashCode() == spriteLibList[i].GetHashCode()); if (index == -1) continue; spriteLibList[i] = newLibraries[index]; } field.SetValue(component, spriteLibList); m_Logger.Add($"Updated the List reference in {component.GetType()} on the GameObject {component.name}"); } } } } } void RemoveOldLibraries(List oldLibraries) { foreach (var library in oldLibraries) { var path = AssetDatabase.GetAssetPath(library); var isPsbFile = UpgradeUtilities.IsPsbImportedFile(path); if (!string.IsNullOrEmpty(path) && !isPsbFile) { m_Logger.Add($"Deleting {path} from project"); AssetDatabase.DeleteAsset(path); } } } // Leaving this in if we want to cleanup .psbs in the future void RemoveSpriteLibFromPsb(string path) { var texture = AssetDatabase.LoadAssetAtPath(path); var factory = new SpriteDataProviderFactories(); factory.Init(); var dataProvider = factory.GetSpriteEditorDataProviderFromObject(texture); dataProvider.InitSpriteEditorDataProvider(); if (dataProvider.targetObject == null) { m_Logger.Add($"Could not load the PSDImporter from the path: {path}. Aborting the Sprite Library cleanup inside the .psb."); return; } var so = new SerializedObject(dataProvider.targetObject); var property = so.FindProperty(k_PsbImporterCategoriesId); if (property != null && property.isArray) { property.arraySize = 0; so.ApplyModifiedPropertiesWithoutUndo(); dataProvider.Apply(); m_Logger.Add($"Removed the Sprite Library asset inside {path}"); var assetImporter = dataProvider.targetObject as AssetImporter; assetImporter.SaveAndReimport(); m_Logger.Add($"Saved and re-imported the file."); } else { m_Logger.Add($"Could not find any Sprite Library asset inside the .psb to cleanup."); } } void AddAssetBundlesToLog() { m_Logger.AddLineBreak(); m_Logger.Add("[NOTE] The following AssetBundles need to be rebuilt:"); foreach(var assetBundle in m_AssetBundlesNeedingUpgrade) m_Logger.Add(assetBundle); } } }