428 lines
17 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
} |