// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace Microsoft.MixedReality.Toolkit.Utilities { /// /// This tool allows the migration of obsolete components into up-to-date versions. /// In order to be processed by the migration tool, deprecated components require specific implementation of the IMigrationHandler /// public class MigrationTool { private List migrationHandlerTypes = new List(); /// /// Returns a copy of all loadable implementation types of IMigrationHandler /// public List MigrationHandlerTypes => new List(migrationHandlerTypes); private Dictionary migrationObjects = new Dictionary(); /// /// Returns a copy of all game objects, prefabs and scene assets selected for migration and their migration status /// public Dictionary MigrationObjects => new Dictionary(migrationObjects); private IMigrationHandler migrationHandlerInstance; private Type migrationHandlerInstanceType; /// /// Possible states for the migration tool /// public enum MigrationToolState { PreMigration = 0, // New object selection can be added to migration objects collection Migrating, // Processing migration objects PostMigration // New objects should not be added to migration objects collection }; /// /// Current migration process state of the tool /// public MigrationToolState MigrationState { get; private set; } public MigrationTool() { RefreshAvailableTypes(); } /// /// Adds selectedObject to the list of objects to be migrated. Return false if the object is not of type GameObject, or SceneAsset. /// public bool TryAddObjectForMigration(Type type, Object selectedObject) { if (MigrationState == MigrationToolState.Migrating) { Debug.LogError("Objects cannot be added during migration process."); return false; } else if (MigrationState == MigrationToolState.PostMigration) { ClearMigrationList(); MigrationState = MigrationToolState.PreMigration; } if (type == null) { Debug.LogError("Migration type needs to be selected before migration."); return false; } if (type != migrationHandlerInstanceType) { ClearMigrationList(); Debug.Log("New migration type selected for migration. Clearing previous selection."); if (!SetMigrationHandlerInstance(type)) { return false; } } if (!selectedObject) { Debug.LogWarning("Selection is empty. Please select object for migration."); return false; } if (selectedObject is GameObject || selectedObject is SceneAsset) { if (CheckIfCanMigrate(type, selectedObject) && !migrationObjects.ContainsKey(selectedObject)) { migrationObjects.Add(selectedObject, new MigrationStatus()); return true; } else { Debug.Log($"{selectedObject.name} does not support {type.Name} migration. Could not add object for migration"); return false; } } Debug.LogError("Object must be a GameObject, Prefab or SceneAsset. Could not add object for migration"); return false; } private bool CheckIfCanMigrate(Type type, Object selectedObject) { bool canMigrate = false; string objectPath = AssetDatabase.GetAssetPath(selectedObject); if (IsSceneGameObject(selectedObject)) { var objectHierarchy = ((GameObject)selectedObject).GetComponentsInChildren(true); for (int i = 0; i < objectHierarchy.Length; i++) { if (migrationHandlerInstance.CanMigrate(objectHierarchy[i].gameObject)) { return true; } } } else if (IsPrefabAsset(selectedObject)) { PrefabAssetType prefabType = PrefabUtility.GetPrefabAssetType(selectedObject); if (prefabType == PrefabAssetType.Regular || prefabType == PrefabAssetType.Variant) { var parent = UnityEditor.PrefabUtility.LoadPrefabContents(objectPath); canMigrate = CheckIfCanMigrate(type, parent); PrefabUtility.UnloadPrefabContents(parent); } } else if (IsSceneAsset(selectedObject)) { Scene scene = EditorSceneManager.OpenScene(objectPath); foreach (var parent in scene.GetRootGameObjects()) { if (CheckIfCanMigrate(type, parent)) { return true; } } } return canMigrate; } /// /// Adds all prefabs and scene assets found on the assets folder to the list of objects to be migrated /// public void TryAddProjectForMigration(Type migrationType) { AddAllAssetsOfTypeForMigration(migrationType, new Type[] { typeof(GameObject), typeof(SceneAsset) }); } /// /// Removes object from the list of objects to migrated /// /// Object to be removed public void RemoveObjectForMigration(Object selectedObject) { migrationObjects.Remove(selectedObject); } /// /// Clears list of objects to be migrated /// public void ClearMigrationList() { migrationObjects.Clear(); } /// /// Migrates all objects from list of objects to be migrated using the selected IMigrationHandler implementation. /// /// A type that implements IMigrationhandler public bool MigrateSelection(Type type, bool askForConfirmation) { if (migrationObjects.Count == 0) { Debug.LogError($"List of objects for migration is empty."); return false; } if (migrationHandlerInstanceType == null) { Debug.LogError($"Please select type for migration."); return false; } if (type == null || migrationHandlerInstanceType != type) { Debug.LogError($"Selected objects should be migrated with type: {migrationHandlerInstanceType}"); return false; } if (askForConfirmation && !EditorUtility.DisplayDialog("Migration Window", "Migration operation cannot be reverted.\n\nDo you want to continue?", "Continue", "Cancel")) { return false; } if (askForConfirmation && !EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { return false; } var previousScenePath = EditorSceneManager.GetActiveScene().path; int failures = 0; MigrationState = MigrationToolState.Migrating; for (int i = 0; i < migrationObjects.Count; i++) { var progress = (float)i / migrationObjects.Count; if (EditorUtility.DisplayCancelableProgressBar("Migration Tool", $"Migrating all {type.Name} components from selection", progress)) { break; } string assetPath = AssetDatabase.GetAssetPath(migrationObjects.ElementAt(i).Key); if (IsSceneGameObject(migrationObjects.ElementAt(i).Key)) { MigrateGameObjectHierarchy((GameObject)migrationObjects.ElementAt(i).Key, migrationObjects.ElementAt(i).Value); } else if (IsPrefabAsset(migrationObjects.ElementAt(i).Key)) { PrefabAssetType prefabType = PrefabUtility.GetPrefabAssetType(migrationObjects.ElementAt(i).Key); if (prefabType == PrefabAssetType.Regular || prefabType == PrefabAssetType.Variant) { // there's currently 5 types of prefab asset types - we're supporting the following: // - Regular: a regular prefab object // - Variant: a prefab derived from another prefab which could be a model, regular or variant prefab // we won't support the following types: // - Model: we can't migrate fbx or other mesh files // - MissingAsset: we can't migrate missing data // - NotAPrefab: we can't migrate as prefab if the given asset isn't a prefab MigratePrefab(assetPath, migrationObjects.ElementAt(i).Value); } } else if (IsSceneAsset(migrationObjects.ElementAt(i).Key)) { MigrateScene(assetPath, migrationObjects.ElementAt(i).Value); } migrationObjects.ElementAt(i).Value.IsProcessed = true; failures += migrationObjects.ElementAt(i).Value.Failures; Debug.Log(migrationObjects.ElementAt(i).Value.Log); } EditorUtility.ClearProgressBar(); if (!String.IsNullOrEmpty(previousScenePath) && previousScenePath != EditorSceneManager.GetActiveScene().path) { EditorSceneManager.OpenScene(Path.Combine(Directory.GetCurrentDirectory(), previousScenePath)); } if (askForConfirmation) { string msg; if (failures > 0) { msg = $"Migration completed with {failures} errors"; } else { msg = "Migration completed successfully!"; } EditorUtility.DisplayDialog("Migration Window", msg, "Close"); } MigrationState = MigrationToolState.PostMigration; return true; } private void AddAllAssetsOfTypeForMigration(Type migrationType, Type[] assetTypes) { var assetPaths = FindAllAssetsOfType(assetTypes); if (assetPaths != null) { for (int i = 0; i < assetPaths.Count; i++) { var progress = (float)i / assetPaths.Count; if (EditorUtility.DisplayCancelableProgressBar("Migration Tool", $"Selecting all assets that support {migrationType.Name} migration.", progress)) { break; } TryAddObjectForMigration(migrationType, AssetDatabase.LoadMainAssetAtPath(assetPaths[i])); } EditorUtility.ClearProgressBar(); } } private bool SetMigrationHandlerInstance(Type type) { if (!typeof(IMigrationHandler).IsAssignableFrom(type)) { Debug.LogError($"{type.Name} is not a valid implementation of IMigrationHandler."); return false; } if (!migrationHandlerTypes.Contains(type)) { Debug.LogError($"{type.Name} might not be a valid implementation of IMigrationHandler."); return false; } try { migrationHandlerInstance = Activator.CreateInstance(type) as IMigrationHandler; migrationHandlerInstanceType = type; Debug.LogWarning($"Migration tool will use {type.Name} type for next migration."); } catch (Exception) { Debug.LogError("Selected MigrationHandler implementation could not be instantiated."); return false; } return true; } private void RefreshAvailableTypes() { var type = typeof(IMigrationHandler); migrationHandlerTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(x => x.GetLoadableTypes()) .Where(x => type.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract).ToList(); } private void MigrateScene(String path, MigrationStatus status) { if (!AssetDatabase.LoadAssetAtPath(path, typeof(SceneAsset))) { return; } Scene scene = EditorSceneManager.OpenScene(path); bool didAnySceneObjectChange = false; foreach (var parent in scene.GetRootGameObjects()) { didAnySceneObjectChange |= MigrateGameObjectHierarchy(parent, status); } if (didAnySceneObjectChange) { EditorSceneManager.SaveScene(scene); } } private void MigratePrefab(String path, MigrationStatus status) { if (!AssetDatabase.LoadAssetAtPath(path, typeof(GameObject))) { return; } var parent = UnityEditor.PrefabUtility.LoadPrefabContents(path); if (MigrateGameObjectHierarchy(parent, status)) { UnityEditor.PrefabUtility.SaveAsPrefabAsset(parent, path); } PrefabUtility.UnloadPrefabContents(parent); } private bool MigrateGameObjectHierarchy(GameObject parent, MigrationStatus status) { bool changedAnyGameObject = false; foreach (var child in parent.GetComponentsInChildren(true)) { try { if (migrationHandlerInstance.CanMigrate(child.gameObject)) { changedAnyGameObject = true; migrationHandlerInstance.Migrate(child.gameObject); status.AddToLog($"Successfully migrated {child.gameObject.name} object \n"); } } catch (Exception e) { status.Failures++; status.AddToLog($"{e.Message}: GameObject {child.gameObject.name} could not be migrated \n"); } } return changedAnyGameObject; } private static List FindAllAssetsOfType(Type[] types) { var filter = string.Join(" ", types .Select(x => string.Format("t:{0}", x.Name)) .ToArray()); return AssetDatabase.FindAssets(filter, new[] { "Assets" }).Select(x => AssetDatabase.GUIDToAssetPath(x)).ToList(); } private static bool IsSceneGameObject(Object selectedObject) { string objectPath = AssetDatabase.GetAssetPath(selectedObject); return String.IsNullOrEmpty(objectPath) && selectedObject is GameObject; } private static bool IsPrefabAsset(Object selectedObject) { string objectPath = AssetDatabase.GetAssetPath(selectedObject); return !String.IsNullOrEmpty(objectPath) && selectedObject is GameObject; } private static bool IsSceneAsset(Object selectedObject) { return selectedObject is SceneAsset; } /// /// Utility class to keep migration status of each object /// public class MigrationStatus { /// /// Flag to indicate if object was already processed by migration /// public bool IsProcessed { get; set; } /// /// Keep track of the amount of issues found during migration process of every children object in the migration object hierarchy /// public int Failures { get; set; } /// /// Keep track of recorded messages logged during the migration process /// public String Log { get; private set; } public MigrationStatus() { IsProcessed = false; Failures = 0; Log = ""; } /// /// Add messages to status log /// public void AddToLog(String msg) { Log += msg; } } /// /// Util method to draw a deprecated warning for a given component in the inspector as well /// as a button to migrate / trigger the migration tool to upgrade to the new version via the /// indicated migration handler. /// /// Deprecated component type. /// Migration handler to call for migrating the component. /// Component to migrate. static public void DrawDeprecated(T target) where T : MonoBehaviour where THandler : IMigrationHandler { List requiringTypes; if (target.gameObject.IsComponentRequired(out requiringTypes)) { string requiringComponentNames = null; for (int i = 0; i < requiringTypes.Count; i++) { requiringComponentNames += "- " + requiringTypes[i].FullName; if (i < requiringTypes.Count - 1) { requiringComponentNames += '\n'; } } EditorGUILayout.HelpBox($"This component is deprecated. Please migrate object to up to date version. Remove the RequiredComponentAttribute from:\n{requiringComponentNames}", MessageType.Error); return; } EditorGUILayout.HelpBox("This component is deprecated. Please migrate object to up to date version", MessageType.Warning); if (GUILayout.Button("Migrate Object")) { Utilities.MigrationTool migrationTool = new Utilities.MigrationTool(); var component = target; migrationTool.TryAddObjectForMigration(typeof(THandler), (GameObject)component.gameObject); migrationTool.MigrateSelection(typeof(THandler), true); } } } }