// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.SceneSystem; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEditor.Callbacks; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; namespace Microsoft.MixedReality.Toolkit.Editor { /// /// Class responsible for updating scene info structs to reflect changes made to scene assets. /// Extends AssetPostprocessor so it can respond to asset changes. /// public class SceneInfoUtils : AssetPostprocessor, IProcessSceneWithReport { /// /// Cached scenes used by SceneInfoDrawer to keep property drawer performant. /// public static EditorBuildSettingsScene[] CachedScenes { get; private set; } = Array.Empty(); public int callbackOrder => 0; /// /// The frame of the last update. Used to ensure we don't spam the system with updates. /// private static int frameScriptableObjectsLastUpdated; private static int frameScenesLastUpdated; private static List> cachedComponentTypes = new List>(); private static HashSet fieldTypesToSearch = new HashSet { typeof(SceneInfo), typeof(SceneInfo[]), typeof(List) }; /// /// Call this when you make a change to the build settings and need those changes to be reflected immediately. /// public static void RefreshCachedScenes() { CachedScenes = EditorBuildSettings.scenes; } /// /// Finds all relative properties of a SceneInfo struct. /// public static void GetSceneInfoRelativeProperties( SerializedProperty property, out SerializedProperty assetProperty, out SerializedProperty nameProperty, out SerializedProperty pathProperty, out SerializedProperty buildIndexProperty, out SerializedProperty includedProperty, out SerializedProperty tagProperty) { assetProperty = property.FindPropertyRelative("Asset"); nameProperty = property.FindPropertyRelative("Name"); pathProperty = property.FindPropertyRelative("Path"); buildIndexProperty = property.FindPropertyRelative("BuildIndex"); includedProperty = property.FindPropertyRelative("Included"); tagProperty = property.FindPropertyRelative("Tag"); } /// /// Finds a missing scene asset reference for a SceneInfo struct. /// /// True if scene was found. public static bool FindScene(SerializedProperty nameProperty, SerializedProperty pathProperty, ref UnityEngine.Object asset) { // Attempt to load via the scene path SceneAsset newSceneAsset = AssetDatabase.LoadAssetAtPath(pathProperty.stringValue); if (newSceneAsset != null) { Debug.Log("Found missing scene at path " + pathProperty.stringValue); asset = newSceneAsset; return true; } else { // If we didn't find it this way, search for all scenes in the project and try a name match foreach (string sceneGUID in AssetDatabase.FindAssets("t:Scene")) { string scenePath = AssetDatabase.GUIDToAssetPath(sceneGUID); string sceneName = System.IO.Path.GetFileNameWithoutExtension(scenePath); if (sceneName == nameProperty.stringValue) { pathProperty.stringValue = scenePath; newSceneAsset = AssetDatabase.LoadAssetAtPath(scenePath); if (newSceneAsset != null) { Debug.Log("Found missing scene at path " + scenePath); asset = newSceneAsset; return true; } } } } return false; } /// /// Implements IProcessSceneWithReport.OnProcessScene /// public void OnProcessScene(Scene scene, BuildReport report) { if (!MixedRealityToolkit.IsSceneSystemEnabled) { return; } RefreshSceneInfoFieldsInScene(scene); } /// /// Updates all the serialized properties for a SceneInfo struct. /// /// True if a property has changed. public static bool RefreshSceneInfo( UnityEngine.Object asset, SerializedProperty nameProperty, SerializedProperty pathProperty, SerializedProperty buildIndexProperty, SerializedProperty includedProperty, SerializedProperty tagProperty) { bool changed = false; if (asset == null) { // Leave the name and path alone, but reset the build index if (buildIndexProperty.intValue >= 0) { buildIndexProperty.intValue = -1; changed = true; } } else { // Refreshing these values is very expensive // Especially getting build scenes // We may want to move this out of the property drawer if (nameProperty.stringValue != asset.name) { nameProperty.stringValue = asset.name; changed = true; } string scenePath = AssetDatabase.GetAssetPath(asset); if (pathProperty.stringValue != scenePath) { pathProperty.stringValue = scenePath; changed = true; } // The method is using scenes by path is not reliable (code included // commented out here for reference). // Cached scenes are used instead (see CachedScenes). // Scene scene = EditorSceneManager.GetSceneByPath(scenePath); // int buildIndex = scene.buildIndex; int buildIndex = -1; int sceneCount = 0; bool included = false; for (int i = 0; i < CachedScenes.Length; i++) { if (CachedScenes[i].path == scenePath) { // If it's in here it's included, even if it's not enabled included = true; if (CachedScenes[i].enabled) { // Only store the build index if it's enabled buildIndex = sceneCount; } } if (CachedScenes[i].enabled) { // Disabled scenes don't count toward scene count sceneCount++; } } if (buildIndex != buildIndexProperty.intValue) { buildIndexProperty.intValue = buildIndex; changed = true; } if (included != includedProperty.boolValue) { includedProperty.boolValue = included; changed = true; } } if (string.IsNullOrEmpty(tagProperty.stringValue)) { tagProperty.stringValue = "Untagged"; changed = true; } return changed; } /// /// Searches for all components in a scene and refreshes any SceneInfo fields found. /// [PostProcessSceneAttribute] public static void OnPostProcessScene() { if (!MixedRealityToolkit.IsSceneSystemEnabled) { return; } RefreshSceneInfoFieldsInOpenScenes(); } [InitializeOnLoadMethod] private static void InitializeOnLoad() { if (!MixedRealityToolkit.IsSceneSystemEnabled) { return; } EditorBuildSettings.sceneListChanged += SceneListChanged; EditorSceneManager.sceneOpened += SceneOpened; frameScriptableObjectsLastUpdated = -1; frameScenesLastUpdated = -1; RefreshCachedTypes(); RefreshCachedScenes(); RefreshSceneInfoFieldsInScriptableObjects(); RefreshSceneInfoFieldsInOpenScenes(); } /// /// Updates the cached component types which use SceneInfo fields. /// private static void RefreshCachedTypes() { if (EditorApplication.isCompiling || BuildPipeline.isBuildingPlayer) { // Don't refresh cached types if we're in the middle of something important return; } cachedComponentTypes.Clear(); foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { foreach (Type t in assembly.GetLoadableTypes().Where(t => t.IsSubclassOf(typeof(Component)))) { foreach (FieldInfo f in t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { if (fieldTypesToSearch.Contains(f.FieldType)) { cachedComponentTypes.Add(new Tuple(t, f)); } } } } } private static void SceneOpened(Scene scene, OpenSceneMode mode) { RefreshSceneInfoFieldsInOpenScenes(); } /// /// Updates the cached scene array when build settings change. /// private static void SceneListChanged() { RefreshCachedScenes(); RefreshSceneInfoFieldsInScriptableObjects(); RefreshSceneInfoFieldsInOpenScenes(); } /// /// Calls RefreshSceneInfoFieldsInScriptableObjects when an asset is modified. /// private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { if (!MixedRealityToolkit.IsSceneSystemEnabled) { return; } RefreshSceneInfoFieldsInScriptableObjects(); RefreshSceneInfoFieldsInOpenScenes(); } /// /// Searches through all ScriptableObject instances and refreshes any SceneInfo fields found. /// private static void RefreshSceneInfoFieldsInScriptableObjects() { if (Time.frameCount == frameScriptableObjectsLastUpdated && !BuildPipeline.isBuildingPlayer) { // Don't update more than once per frame unless we're building return; } try { foreach (ScriptableObject source in ScriptableObjectExtensions.GetAllInstances()) { foreach (FieldInfo fieldInfo in source.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { if (fieldTypesToSearch.Contains(fieldInfo.FieldType)) { CheckForChangesInField(source, fieldInfo); } } } frameScriptableObjectsLastUpdated = Time.frameCount; } catch (Exception) { Debug.LogWarning("Error when attempting to update scene info fields. Scene info data may be stale."); } } private static void RefreshSceneInfoFieldsInOpenScenes() { if (Time.frameCount == frameScenesLastUpdated && !BuildPipeline.isBuildingPlayer) { // Don't update more than once per frame unless we're building return; } try { foreach (Tuple typeFieldInfoPair in cachedComponentTypes) { FieldInfo fieldInfo = typeFieldInfoPair.Item2; foreach (Component source in GameObject.FindObjectsOfType(typeFieldInfoPair.Item1)) { CheckForChangesInField(source, fieldInfo); } } frameScenesLastUpdated = Time.frameCount; } catch (Exception) { Debug.LogWarning("Error when attempting to update scene info fields. Scene info data may be stale."); } } private void RefreshSceneInfoFieldsInScene(Scene scene) { try { foreach (Tuple typeFieldInfoPair in cachedComponentTypes) { FieldInfo fieldInfo = typeFieldInfoPair.Item2; foreach (GameObject rootGameObject in scene.GetRootGameObjects()) { foreach (Component source in rootGameObject.GetComponentsInChildren(typeFieldInfoPair.Item1)) { CheckForChangesInField(source, fieldInfo); } } } } catch (Exception) { Debug.LogWarning("Error when attempting to update scene info fields. Scene info data may be stale."); } } private static void CheckForChangesInField(UnityEngine.Object source, FieldInfo fieldInfo) { if (fieldInfo.FieldType == typeof(SceneInfo)) { SerializedObject serializedObject = new SerializedObject(source); SerializedProperty property = serializedObject.FindProperty(fieldInfo.Name); SerializedProperty assetProperty, nameProperty, pathProperty, buildIndexProperty, includedProperty, tagProperty; GetSceneInfoRelativeProperties(property, out assetProperty, out nameProperty, out pathProperty, out buildIndexProperty, out includedProperty, out tagProperty); if (RefreshSceneInfo(assetProperty.objectReferenceValue, nameProperty, pathProperty, buildIndexProperty, includedProperty, tagProperty)) { if (BuildPipeline.isBuildingPlayer) { Debug.Log("Found out-of-date SceneInfo field '" + property.displayName + "' in asset '" + source.name + "' - The asset has been updated to: " + pathProperty.stringValue); } serializedObject.ApplyModifiedProperties(); } } else if (fieldInfo.FieldType == typeof(SceneInfo[]) || fieldInfo.FieldType == typeof(List)) { SerializedObject serializedObject = new SerializedObject(source); SerializedProperty arrayProperty = serializedObject.FindProperty(fieldInfo.Name); for (int i = 0; i < arrayProperty.arraySize; i++) { SerializedProperty property = arrayProperty.GetArrayElementAtIndex(i); SerializedProperty assetProperty, nameProperty, pathProperty, buildIndexProperty, includedProperty, tagProperty; GetSceneInfoRelativeProperties(property, out assetProperty, out nameProperty, out pathProperty, out buildIndexProperty, out includedProperty, out tagProperty); if (RefreshSceneInfo(assetProperty.objectReferenceValue, nameProperty, pathProperty, buildIndexProperty, includedProperty, tagProperty)) { serializedObject.ApplyModifiedProperties(); // If we're building, log this change if (BuildPipeline.isBuildingPlayer) { Debug.Log("Found out-of-date SceneInfo field '" + property.displayName + "' in asset '" + source.name + "' - The asset has been updated to: " + pathProperty.stringValue); } } } } else { Debug.LogWarning("Attempted to refresh SceneInfo field for a type that isn't recognized: " + fieldInfo.FieldType); } } } }