500 lines
19 KiB
C#
500 lines
19 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public class MigrationTool
|
|
{
|
|
private List<Type> migrationHandlerTypes = new List<Type>();
|
|
/// <summary>
|
|
/// Returns a copy of all loadable implementation types of IMigrationHandler
|
|
/// </summary>
|
|
public List<Type> MigrationHandlerTypes => new List<Type>(migrationHandlerTypes);
|
|
|
|
private Dictionary<Object, MigrationStatus> migrationObjects = new Dictionary<Object, MigrationStatus>();
|
|
/// <summary>
|
|
/// Returns a copy of all game objects, prefabs and scene assets selected for migration and their migration status
|
|
/// </summary>
|
|
public Dictionary<Object, MigrationStatus> MigrationObjects => new Dictionary<Object, MigrationStatus>(migrationObjects);
|
|
|
|
private IMigrationHandler migrationHandlerInstance;
|
|
private Type migrationHandlerInstanceType;
|
|
|
|
/// <summary>
|
|
/// Possible states for the migration tool
|
|
/// </summary>
|
|
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
|
|
};
|
|
|
|
/// <summary>
|
|
/// Current migration process state of the tool
|
|
/// </summary>
|
|
public MigrationToolState MigrationState { get; private set; }
|
|
|
|
public MigrationTool()
|
|
{
|
|
RefreshAvailableTypes();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds selectedObject to the list of objects to be migrated. Return false if the object is not of type GameObject, or SceneAsset.
|
|
/// </summary>
|
|
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<Transform>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds all prefabs and scene assets found on the assets folder to the list of objects to be migrated
|
|
/// </summary>
|
|
public void TryAddProjectForMigration(Type migrationType)
|
|
{
|
|
AddAllAssetsOfTypeForMigration(migrationType, new Type[] { typeof(GameObject), typeof(SceneAsset) });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes object from the list of objects to migrated
|
|
/// </summary>
|
|
/// <param name="selectedObject">Object to be removed</param>
|
|
public void RemoveObjectForMigration(Object selectedObject)
|
|
{
|
|
migrationObjects.Remove(selectedObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears list of objects to be migrated
|
|
/// </summary>
|
|
public void ClearMigrationList()
|
|
{
|
|
migrationObjects.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Migrates all objects from list of objects to be migrated using the selected IMigrationHandler implementation.
|
|
/// </summary>
|
|
/// <param name="type">A type that implements IMigrationhandler</param>
|
|
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<Transform>(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<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Utility class to keep migration status of each object
|
|
/// </summary>
|
|
public class MigrationStatus
|
|
{
|
|
/// <summary>
|
|
/// Flag to indicate if object was already processed by migration
|
|
/// </summary>
|
|
public bool IsProcessed { get; set; }
|
|
|
|
/// <summary>
|
|
/// Keep track of the amount of issues found during migration process of every children object in the migration object hierarchy
|
|
/// </summary>
|
|
public int Failures { get; set; }
|
|
|
|
/// <summary>
|
|
/// Keep track of recorded messages logged during the migration process
|
|
/// </summary>
|
|
public String Log { get; private set; }
|
|
|
|
public MigrationStatus()
|
|
{
|
|
IsProcessed = false;
|
|
Failures = 0;
|
|
Log = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add messages to status log
|
|
/// </summary>
|
|
public void AddToLog(String msg)
|
|
{
|
|
Log += msg;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <typeparam name="T">Deprecated component type.</typeparam>
|
|
/// <typeparam name="THandler">Migration handler to call for migrating the component.</typeparam>
|
|
/// <param name="target">Component to migrate.</param>
|
|
static public void DrawDeprecated<T, THandler>(T target)
|
|
where T : MonoBehaviour
|
|
where THandler : IMigrationHandler
|
|
{
|
|
List<Type> requiringTypes;
|
|
|
|
if (target.gameObject.IsComponentRequired<T>(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);
|
|
}
|
|
}
|
|
}
|
|
}
|