mixedreality/com.microsoft.mixedreality..../Core/Inspectors/PropertyDrawers/SceneInfoUtils.cs

428 lines
17 KiB
C#

// 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
{
/// <summary>
/// Class responsible for updating scene info structs to reflect changes made to scene assets.
/// Extends AssetPostprocessor so it can respond to asset changes.
/// </summary>
public class SceneInfoUtils : AssetPostprocessor, IProcessSceneWithReport
{
/// <summary>
/// Cached scenes used by SceneInfoDrawer to keep property drawer performant.
/// </summary>
public static EditorBuildSettingsScene[] CachedScenes { get; private set; } = Array.Empty<EditorBuildSettingsScene>();
public int callbackOrder => 0;
/// <summary>
/// The frame of the last update. Used to ensure we don't spam the system with updates.
/// </summary>
private static int frameScriptableObjectsLastUpdated;
private static int frameScenesLastUpdated;
private static List<Tuple<Type, FieldInfo>> cachedComponentTypes = new List<Tuple<Type, FieldInfo>>();
private static HashSet<Type> fieldTypesToSearch = new HashSet<Type>
{
typeof(SceneInfo),
typeof(SceneInfo[]),
typeof(List<SceneInfo>)
};
/// <summary>
/// Call this when you make a change to the build settings and need those changes to be reflected immediately.
/// </summary>
public static void RefreshCachedScenes()
{
CachedScenes = EditorBuildSettings.scenes;
}
/// <summary>
/// Finds all relative properties of a SceneInfo struct.
/// </summary>
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");
}
/// <summary>
/// Finds a missing scene asset reference for a SceneInfo struct.
/// </summary>
/// <returns>True if scene was found.</returns>
public static bool FindScene(SerializedProperty nameProperty, SerializedProperty pathProperty, ref UnityEngine.Object asset)
{
// Attempt to load via the scene path
SceneAsset newSceneAsset = AssetDatabase.LoadAssetAtPath<SceneAsset>(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<SceneAsset>(scenePath);
if (newSceneAsset != null)
{
Debug.Log("Found missing scene at path " + scenePath);
asset = newSceneAsset;
return true;
}
}
}
}
return false;
}
/// <summary>
/// Implements IProcessSceneWithReport.OnProcessScene
/// </summary>
public void OnProcessScene(Scene scene, BuildReport report)
{
if (!MixedRealityToolkit.IsSceneSystemEnabled)
{
return;
}
RefreshSceneInfoFieldsInScene(scene);
}
/// <summary>
/// Updates all the serialized properties for a SceneInfo struct.
/// </summary>
/// <returns>True if a property has changed.</returns>
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;
}
/// <summary>
/// Searches for all components in a scene and refreshes any SceneInfo fields found.
/// </summary>
[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();
}
/// <summary>
/// Updates the cached component types which use SceneInfo fields.
/// </summary>
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<Type, FieldInfo>(t, f));
}
}
}
}
}
private static void SceneOpened(Scene scene, OpenSceneMode mode)
{
RefreshSceneInfoFieldsInOpenScenes();
}
/// <summary>
/// Updates the cached scene array when build settings change.
/// </summary>
private static void SceneListChanged()
{
RefreshCachedScenes();
RefreshSceneInfoFieldsInScriptableObjects();
RefreshSceneInfoFieldsInOpenScenes();
}
/// <summary>
/// Calls RefreshSceneInfoFieldsInScriptableObjects when an asset is modified.
/// </summary>
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
if (!MixedRealityToolkit.IsSceneSystemEnabled)
{
return;
}
RefreshSceneInfoFieldsInScriptableObjects();
RefreshSceneInfoFieldsInOpenScenes();
}
/// <summary>
/// Searches through all ScriptableObject instances and refreshes any SceneInfo fields found.
/// </summary>
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<ScriptableObject>())
{
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<Type, FieldInfo> 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<Type, FieldInfo> 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<SceneInfo>))
{
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);
}
}
}
}