Files
dungeons/Assets/UltimateXR/Editor/UxrEditorUtils.ComponentProcessing.cs
2024-08-06 21:58:35 +02:00

1108 lines
52 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrEditorUtils.ComponentProcessing.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using UltimateXR.Extensions.Unity;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace UltimateXR.Editor
{
public static partial class UxrEditorUtils
{
#region Public Methods
/// <summary>
/// Modifies a component in a prefab or prefab instance, allowing to apply modifications along the prefab variant chain
/// and original prefab if they exist.
/// The method is guaranteed to process prefabs in an order where the source prefab is processed first and then all
/// child prefabs down to the outermost prefab.
/// </summary>
/// <typeparam name="T">The component type</typeparam>
/// <param name="componentOrGameObject">
/// Component or GameObject to process. It can be an instance in the scene or a
/// component/GameObject in a prefab
/// </param>
/// <param name="options">Component processing options (flags)</param>
/// <param name="componentProcessor">Processor that modifies the component</param>
/// <param name="progressUpdater">
/// Will receive updates of the process so that the information can be fed to a Unity
/// progress bar
/// </param>
/// <param name="canceled">Returns whether the user canceled the operation using the progress updater</param>
/// <param name="onlyCheck">
/// Whether to only check if components should be processed, without making any changes. This
/// can be used to get how many elements would be changed without modifying any data
/// </param>
/// <typeparam name="T">The component type</typeparam>
/// <returns>Whether any modifications were made</returns>
public static bool ModifyComponent<T>(Object componentOrGameObject,
UxrComponentProcessingOptions options,
UxrComponentProcessor<T> componentProcessor,
UxrProgressUpdater progressUpdater,
out bool canceled,
bool onlyCheck = false) where T : Component
{
canceled = false;
if (componentOrGameObject == null)
{
return false;
}
// Gather component(s) to process
T[] components = null;
bool sourceIsPrefab = false;
if (componentOrGameObject is GameObject sourceGameObject)
{
components = options.HasFlag(UxrComponentProcessingOptions.RecurseIntoChildren) ? sourceGameObject.GetComponentsInChildren<T>(true) : sourceGameObject.GetComponents<T>();
sourceIsPrefab = sourceGameObject.IsInPrefab();
}
else if (componentOrGameObject is T sourceComponent)
{
components = new[] { sourceComponent };
sourceIsPrefab = sourceComponent.IsInPrefab();
}
else
{
Debug.LogError($"{nameof(ModifyComponent)}: Cannot process type {componentOrGameObject.GetType().Name}, expected {nameof(GameObject)} or component of type {typeof(T)}.");
return false;
}
// Gather all prefabs to process, if any, and for each prefab all its components to process
Dictionary<string, List<Component>> prefabComponents = new Dictionary<string, List<Component>>();
for (var i = 0; i < components.Length; i++)
{
T c = components[i];
if (c == null)
{
continue;
}
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Preprocessing components", $"Object {c.name}, Component {c.GetType().Name} ", (float)i / components.Length));
if (canceled)
{
return false;
}
}
// For each component, iterate over all components in variant prefab chain starting from the original prefab, so that changes get propagated in the correct order
T componentInChain = c;
if (!sourceIsPrefab)
{
// Component is instance. Get component in prefab to start.
componentInChain = PrefabUtility.GetCorrespondingObjectFromSource(c);
}
while (componentInChain != null)
{
if (!options.HasFlag(UxrComponentProcessingOptions.RecurseIntoPrefabs) && !sourceIsPrefab)
{
break;
}
T componentInParentPrefab = PrefabUtility.GetCorrespondingObjectFromSource(componentInChain);
bool isOriginalPrefab = componentInParentPrefab == null;
string assetPath = AssetDatabase.GetAssetPath(componentInChain.transform.root.gameObject);
bool process = (isOriginalPrefab && options.HasFlag(UxrComponentProcessingOptions.ProcessOriginalPrefabComponents)) ||
(!isOriginalPrefab && options.HasFlag(UxrComponentProcessingOptions.ProcessNonOriginalPrefabComponents));
if (process)
{
if (!ShouldIgnoreUltimateXRPath(assetPath, !options.HasFlag(UxrComponentProcessingOptions.ProcessUltimateXRAssetComponents)))
{
// Initialize
if (!string.IsNullOrEmpty(assetPath))
{
if (!prefabComponents.ContainsKey(assetPath))
{
prefabComponents.Add(assetPath, new List<Component>());
}
// Add if it was not added already through another instance chain
if (!prefabComponents[assetPath].Contains(componentInChain))
{
prefabComponents[assetPath].Add(componentInChain);
}
}
}
}
componentInChain = componentInParentPrefab;
if (!options.HasFlag(UxrComponentProcessingOptions.RecurseIntoPrefabs) && sourceIsPrefab)
{
break;
}
}
}
// Get sorted prefab list and process prefabs in correct order
List<string> sortedPrefabPaths = GetSortedPrefabProcessingPaths<T>(prefabComponents.Keys, !options.HasFlag(UxrComponentProcessingOptions.ProcessUltimateXRAssetComponents));
bool processedAny = false;
for (var i = 0; i < sortedPrefabPaths.Count; i++)
{
string prefabPath = sortedPrefabPaths[i];
// Filter out paths that are dependencies but are not included in the list that we built because they are not being processed
if (prefabComponents.ContainsKey(prefabPath))
{
GameObject prefab = AssetDatabase.LoadMainAssetAtPath(prefabPath) as GameObject;
bool processed = false;
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Processing components", $"Prefab {prefab.name}", (float)i / components.Length));
if (canceled)
{
return processedAny;
}
}
foreach (T component in prefabComponents[prefabPath])
{
bool isOriginalSource = PrefabUtility.GetCorrespondingObjectFromSource(component) == null;
if (componentProcessor != null && componentProcessor.Invoke(new UxrComponentInfo<T>(component, prefab, isOriginalSource, isOriginalSource), onlyCheck))
{
processed = true;
processedAny = true;
}
}
if (processed && !onlyCheck)
{
PrefabUtility.SavePrefabAsset(prefab);
}
}
}
// End with instances if there are any
if (!sourceIsPrefab)
{
for (var i = 0; i < components.Length; i++)
{
T c = components[i];
if (c == null)
{
continue;
}
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Processing components", $"Object {c.name}, Component {c.GetType().Name} ", (float)i / components.Length));
if (canceled)
{
return processedAny;
}
}
bool process = (c.gameObject.IsPrefabInstance() && options.HasFlag(UxrComponentProcessingOptions.ProcessPrefabSceneComponents)) ||
(!c.gameObject.IsPrefabInstance() && options.HasFlag(UxrComponentProcessingOptions.ProcessOriginalSceneComponents));
if (c.gameObject.scene.name != null)
{
if (ShouldIgnoreUltimateXRPath(c.gameObject.scene.path, !options.HasFlag(UxrComponentProcessingOptions.ProcessUltimateXRAssetComponents)))
{
process = false;
}
}
bool isOriginalSource = !c.gameObject.IsPrefabInstance();
if (process && componentProcessor != null && componentProcessor.Invoke(new UxrComponentInfo<T>(c, null, isOriginalSource, isOriginalSource), onlyCheck))
{
processedAny = true;
if (!onlyCheck)
{
EditorUtility.SetDirty(c);
}
}
}
}
return processedAny;
}
/// <summary>
/// Processes all components in a project:
/// <list type="bullet">
/// <item>Components in the currently open scenes</item>
/// <item>Components in other scenes in the project</item>
/// <item>Components in the project's prefabs</item>
/// </list>
/// Each component can be guaranteed to be processed only once by checking
/// <see cref="UxrComponentInfo{T}.IsOriginalSource" />.
/// It is true for:
/// <list type="bullet">
/// <item>Components in the scene that are not instantiated from a prefab</item>
/// <item>Components inside the original prefab</item>
/// </list>
/// And false for
/// <list type="bullet">
/// <item>Components in the scene that are instantiated from a prefab</item>
/// <item>Components inside prefabs that are inherited from another source prefab</item>
/// </list>
/// When processing components inside prefabs, <see cref="UxrComponentInfo{T}.TargetPrefab" /> will tell which prefab
/// is being processed.
/// Also, components are processed in an order that, in prefabs, guarantees that parent prefabs are processed first,
/// starting from the source prefab downwards to the outermost prefab.
/// This ensures that changes in a prefab that depend on parent prefabs will always get the updated inherited value in
/// the current prefab.
/// </summary>
/// <typeparam name="T">The type of the component to process</typeparam>
/// <param name="basePath">
/// Base path of scenes and prefabs to process. Use null or empty to process the whole project. If
/// using a base path, it should start with Assets/
/// </param>
/// <param name="componentProcessor">
/// The component processor. It will receive the component to process as argument and it requires to return a boolean
/// telling whether the component was modified or not.
/// </param>
/// <param name="progressUpdater">
/// Will receive updates of the process so that the information can be fed to a Unity progress bar
/// </param>
/// <param name="canceled">Returns whether the user canceled the operation using the progress updater</param>
/// <param name="ignoreUltimateXRAssets">Whether to ignore components in assets in UltimateXR folders</param>
/// <param name="onlyCheck">
/// Whether to only check if components should be processed, without making any changes. This
/// can be used to get how many elements would be changed without modifying any data
/// </param>
/// <typeparam name="T">The component type</typeparam>
/// <returns>Whether any modifications were made</returns>
public static bool ProcessAllProjectComponents<T>(string basePath,
UxrComponentProcessor<T> componentProcessor,
UxrProgressUpdater progressUpdater,
out bool canceled,
bool ignoreUltimateXRAssets = false,
bool onlyCheck = false) where T : Component
{
// Process prefabs in project in best order
bool processedAny = ProcessPrefabs(GetSortedPrefabProcessingPaths<T>(basePath, ignoreUltimateXRAssets), basePath, ignoreUltimateXRAssets, true, true, componentProcessor, progressUpdater, out canceled, onlyCheck);
if (canceled)
{
return processedAny;
}
// Open scenes
List<Scene> openScenes = new List<Scene>();
for (int i = 0; i < SceneManager.sceneCount; ++i)
{
Scene scene = SceneManager.GetSceneAt(i);
if (PathRequiresProcessing(basePath, scene.path, ignoreUltimateXRAssets))
{
if (ProcessAllNonPrefabInstanceGameObjects(scene, componentProcessor, progressUpdater, out canceled, onlyCheck))
{
processedAny = true;
}
if (canceled)
{
return processedAny;
}
if (ProcessAllInnermostPrefabInstanceGameObjects(scene, basePath, ignoreUltimateXRAssets, componentProcessor, progressUpdater, out canceled, onlyCheck))
{
processedAny = true;
}
if (canceled)
{
return processedAny;
}
}
openScenes.Add(scene);
}
// Process non-open scenes
foreach (string scenePath in GetAllAssetPathsExceptPackages().Where(path => path.EndsWith(".unity")))
{
if (openScenes.All(s => s.path != scenePath) && PathRequiresProcessing(basePath, scenePath, ignoreUltimateXRAssets))
{
Scene scene = default;
try
{
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Opening scene", scenePath, 0.0f));
if (canceled)
{
return processedAny;
}
}
scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
if (ProcessAllNonPrefabInstanceGameObjects(scene, componentProcessor, progressUpdater, out canceled, onlyCheck))
{
processedAny = true;
}
if (canceled)
{
return processedAny;
}
if (ProcessAllInnermostPrefabInstanceGameObjects(scene, basePath, ignoreUltimateXRAssets, componentProcessor, progressUpdater, out canceled, onlyCheck))
{
processedAny = true;
}
if (canceled)
{
return processedAny;
}
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Closing scene", scene.name, 1.0f));
if (canceled)
{
return processedAny;
}
}
if (processedAny && !onlyCheck)
{
EditorSceneManager.SaveScene(scene);
}
EditorSceneManager.CloseScene(scene, true);
}
catch (Exception)
{
Debug.LogWarning($"Error opening scene {scenePath}. Skipping...");
if (scene != default)
{
EditorSceneManager.CloseScene(scene, true);
}
}
}
}
if (!onlyCheck)
{
AssetDatabase.SaveAssets();
Resources.UnloadUnusedAssets();
}
return processedAny;
}
/// <summary>
/// Same as <see cref="ProcessAllProjectComponents{T}" /> with some key aspects:
/// <list type="bullet">
/// <item>
/// Instead of processing a path looking for scenes, the scene paths will be provided as a parameter.
/// </item>
/// <item>
/// From the scenes, components that do not belong to a prefab instance will be processed.
/// Additionally, all prefabs that have instances in the scene will be processed, including all prefabs up in
/// the prefab chain. Only prefabs instantiated in the scene that are in <paramref name="basePath" /> will be
/// processed.
/// </item>
/// <item>
/// If <paramref name="processBasePath" /> is true, all prefab assets in <see cref="basePath" /> will be
/// processed too. If any is instantiated in a scene processed from <see cref="scenePaths" />,
/// it will be processed only once.
/// </item>
/// </list>
/// </summary>
/// <param name="scenePaths">Scene paths to process</param>
/// <param name="processBasePath">Whether to process all prefab assets in <see cref="basePath" /></param>
/// <param name="basePath">
/// Base path to process looking for prefabs. In addition, prefabs in any scene from
/// <paramref name="scenePaths" /> will be processed only if they are located in this base path.
/// If the base path is null or empty, the whole project is considered
/// </param>
/// <param name="componentProcessor">
/// The component processor. It will receive the component to process as argument and it requires to return a boolean
/// telling whether the component was modified or not.
/// </param>
/// <param name="progressUpdater">
/// Will receive updates of the process so that the information can be fed to a Unity progress bar
/// </param>
/// <param name="canceled">Returns whether the user canceled the operation using the progress updater</param>
/// <param name="ignoreUltimateXRAssets">Whether to ignore components in assets in UltimateXR folders</param>
/// <param name="onlyCheck">
/// Whether to only check if components should be processed, without making any changes. This
/// can be used to get how many elements would be changed without modifying any data
/// </param>
/// <typeparam name="T">The component type</typeparam>
/// <returns>Whether any modifications were made</returns>
public static bool ProcessScenesAndProjectPathPrefabs<T>(IEnumerable<string> scenePaths,
bool processBasePath,
string basePath,
UxrComponentProcessor<T> componentProcessor,
UxrProgressUpdater progressUpdater,
out bool canceled,
bool ignoreUltimateXRAssets = false,
bool onlyCheck = false) where T : Component
{
bool processedAny = false;
canceled = false;
List<string> scenePrefabPaths = new List<string>();
// Process open scenes if they are in the list
if (scenePaths.Any())
{
List<Scene> openScenes = new List<Scene>();
for (int i = 0; i < SceneManager.sceneCount; ++i)
{
Scene scene = SceneManager.GetSceneAt(i);
if (scenePaths.Any(s => s == scene.path))
{
if (ProcessAllNonPrefabInstanceGameObjects(scene, componentProcessor, progressUpdater, out canceled, onlyCheck))
{
processedAny = true;
}
if (canceled)
{
return processedAny;
}
if (ProcessAllInnermostPrefabInstanceGameObjects(scene, basePath, ignoreUltimateXRAssets, componentProcessor, progressUpdater, out canceled, onlyCheck))
{
processedAny = true;
}
if (canceled)
{
return processedAny;
}
GetScenePrefabPaths<T>(scene, scenePrefabPaths);
if (canceled)
{
return processedAny;
}
}
openScenes.Add(scene);
}
// Process non-open scenes in list
foreach (string scenePath in scenePaths)
{
bool isSceneOpen = false;
for (int i = 0; i < SceneManager.sceneCount; ++i)
{
if (SceneManager.GetSceneAt(i).path == scenePath)
{
isSceneOpen = true;
break;
}
}
if (!isSceneOpen)
{
try
{
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Opening scene", scenePath, 0.0f));
if (canceled)
{
return processedAny;
}
}
Scene scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
if (ProcessAllNonPrefabInstanceGameObjects(scene, componentProcessor, progressUpdater, out canceled, onlyCheck))
{
processedAny = true;
}
if (canceled)
{
return processedAny;
}
if (ProcessAllInnermostPrefabInstanceGameObjects(scene, basePath, ignoreUltimateXRAssets, componentProcessor, progressUpdater, out canceled, onlyCheck))
{
processedAny = true;
}
if (canceled)
{
return processedAny;
}
GetScenePrefabPaths<T>(scene, scenePrefabPaths);
if (canceled)
{
return processedAny;
}
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Closing scene", scene.name, 1.0f));
if (canceled)
{
return processedAny;
}
}
if (processedAny && !onlyCheck)
{
EditorSceneManager.SaveScene(scene);
}
EditorSceneManager.CloseScene(scene, true);
}
catch (Exception)
{
Debug.LogWarning($"Error opening scene {scenePath}. Skipping...");
}
}
}
}
// Process prefabs in best order
if (processBasePath || scenePrefabPaths.Any())
{
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Processing prefabs", "Sorting prefabs", 0.0f));
if (canceled)
{
return processedAny;
}
}
processedAny = ProcessPrefabs(GetSortedPrefabProcessingPaths<T>(scenePrefabPaths, basePath, processBasePath, ignoreUltimateXRAssets), basePath, ignoreUltimateXRAssets, true, true, componentProcessor, progressUpdater, out canceled, onlyCheck);
if (canceled)
{
return processedAny;
}
}
if (!onlyCheck)
{
AssetDatabase.SaveAssets();
Resources.UnloadUnusedAssets();
}
return processedAny;
}
#endregion
#region Private Methods
/// <summary>
/// Checks whether a given path should be ignored based on the path and the ignore setting.
/// </summary>
/// <param name="assetPath">Asset path to check if it should be ignored</param>
/// <param name="ignoreUltimateXRAssets">Whether to ignore components in assets in UltimateXR folders</param>
/// <returns>Whether the given path should be ignored</returns>
private static bool ShouldIgnoreUltimateXRPath(string assetPath, bool ignoreUltimateXRAssets)
{
return ignoreUltimateXRAssets && PathIsInUltimateXR(assetPath);
}
/// <summary>
/// Checks whether the given asset path requires processing.
/// </summary>
/// <param name="basePath">Base path to process. Null or empty to process all paths</param>
/// <param name="assetPath">Asset path to check if it should be processed</param>
/// <param name="ignoreUltimateXRAssets">Whether to ignore components in assets in UltimateXR folders</param>
/// <returns>Whether <see cref="assetPath" /> should be processed</returns>
private static bool PathRequiresProcessing(string basePath, string assetPath, bool ignoreUltimateXRAssets)
{
if (ShouldIgnoreUltimateXRPath(assetPath, ignoreUltimateXRAssets))
{
return false;
}
return string.IsNullOrEmpty(basePath) || assetPath.StartsWith(basePath);
}
/// <summary>
/// Gets a list of prefab paths that should be modified to process all components of a given type. The order ensures
/// that parent prefabs will always come first than the child prefabs. This is useful when processing components in
/// prefabs that depend on the parent prefab value, because each prefab will always have the updated value from the
/// parent when it's its turn to be processed.
/// </summary>
/// <param name="paths">Prefab paths to process</param>
/// <param name="ignoreUltimateXRAssets">Whether to ignore components in assets in UltimateXR folders</param>
/// <typeparam name="T">Component type</typeparam>
/// <returns>Sorted prefab path list</returns>
private static List<string> GetSortedPrefabProcessingPaths<T>(IEnumerable<string> paths, bool ignoreUltimateXRAssets) where T : Component
{
// Build prefab dependencies
Dictionary<string, List<string>> assetDependencies = new Dictionary<string, List<string>>();
foreach (string prefabPath in paths)
{
if (!ShouldIgnoreUltimateXRPath(prefabPath, ignoreUltimateXRAssets) && AssetDatabase.GetMainAssetTypeAtPath(prefabPath) == typeof(GameObject) && !assetDependencies.ContainsKey(prefabPath))
{
Dictionary<string, int> dependencies = new Dictionary<string, int>(); // (path, count)
GameObject gameObject = AssetDatabase.LoadMainAssetAtPath(prefabPath) as GameObject;
// Get all components of the given type in the prefab
T[] components = gameObject.GetComponentsInChildren<T>(true);
// Build dictionary where we list all asset paths with inner prefabs that this prefab depends on.
foreach (T component in components)
{
if (component != null && GetInnermostNon3DModelPrefabRoot(component, out GameObject prefab, out GameObject prefabInstance, out T componentInPrefab))
{
if (prefab != gameObject)
{
// Not original source, it's coming from an inner prefab. Save dependency.
string assetPath = AssetDatabase.GetAssetPath(prefab);
if (!dependencies.ContainsKey(assetPath))
{
dependencies.Add(assetPath, 1);
}
else
{
dependencies[assetPath]++;
}
}
}
}
assetDependencies.Add(prefabPath, dependencies.Keys.ToList());
}
}
// Recursive method to create the list
void AddDependenciesAndAsset(string assetPath, Dictionary<string, List<string>> assetDependencies, ref List<string> sortedAssetList)
{
// Add all asset dependencies first
if (assetDependencies.ContainsKey(assetPath))
{
foreach (string path in assetDependencies[assetPath])
{
if (!sortedAssetList.Contains(path))
{
AddDependenciesAndAsset(path, assetDependencies, ref sortedAssetList);
}
}
}
// Add asset
if (!sortedAssetList.Contains(assetPath))
{
sortedAssetList.Add(assetPath);
}
}
// Create the list using the recursive method
List<string> sortedPathList = new List<string>();
foreach (string prefabPath in assetDependencies.Keys)
{
AddDependenciesAndAsset(prefabPath, assetDependencies, ref sortedPathList);
}
return sortedPathList;
}
/// <summary>
/// Same as <see cref="GetSortedPrefabProcessingPaths{T}(System.Collections.Generic.IEnumerable{string})" /> but
/// specifying a list of paths and a base path in the project folder. This method will take care of not processing
/// duplicates if there are prefabs specified in <paramref name="additionalPaths" /> that are also in
/// <see cref="basePath" />.
/// </summary>
/// <param name="additionalPaths">Paths to process</param>
/// <param name="basePath">Project base path to process</param>
/// <param name="processBasePath">Whether to process all prefabs in <paramref name="basePath" /></param>
/// <param name="ignoreUltimateXRAssets">Whether to ignore components in assets in UltimateXR folders</param>
/// <typeparam name="T">Component type</typeparam>
/// <returns>Sorted prefab path list</returns>
private static List<string> GetSortedPrefabProcessingPaths<T>(IEnumerable<string> additionalPaths, string basePath, bool processBasePath, bool ignoreUltimateXRAssets) where T : Component
{
// Filter required paths from all asset paths in project
IEnumerable<string> GetPrefabsToProcess()
{
foreach (string prefabPath in additionalPaths)
{
if (AssetDatabase.GetMainAssetTypeAtPath(prefabPath) == typeof(GameObject) && PathRequiresProcessing(basePath, prefabPath, ignoreUltimateXRAssets))
{
yield return prefabPath;
}
}
if (processBasePath)
{
foreach (string prefabPath in GetAllAssetPathsExceptPackages())
{
if (AssetDatabase.GetMainAssetTypeAtPath(prefabPath) == typeof(GameObject) && PathRequiresProcessing(basePath, prefabPath, ignoreUltimateXRAssets) && !additionalPaths.Contains(prefabPath))
{
yield return prefabPath;
}
}
}
}
return GetSortedPrefabProcessingPaths<T>(GetPrefabsToProcess(), ignoreUltimateXRAssets);
}
/// <summary>
/// Same as <see cref="GetSortedPrefabProcessingPaths{T}(System.Collections.Generic.IEnumerable{string})" /> but
/// specifying a base path in the project folder.
/// </summary>
/// <param name="basePath">Project base path to process</param>
/// <param name="ignoreUltimateXRAssets">Whether to ignore components in assets in UltimateXR folders</param>
/// <typeparam name="T">Component type</typeparam>
/// <returns>Sorted prefab path list</returns>
private static List<string> GetSortedPrefabProcessingPaths<T>(string basePath, bool ignoreUltimateXRAssets) where T : Component
{
// Filter required paths from all asset paths in project
IEnumerable<string> GetPrefabsToProcess()
{
foreach (string prefabPath in GetAllAssetPathsExceptPackages())
{
if (AssetDatabase.GetMainAssetTypeAtPath(prefabPath) == typeof(GameObject) && PathRequiresProcessing(basePath, prefabPath, ignoreUltimateXRAssets))
{
yield return prefabPath;
}
}
}
return GetSortedPrefabProcessingPaths<T>(GetPrefabsToProcess(), ignoreUltimateXRAssets);
}
/// <summary>
/// Processes all prefabs in the given paths.
/// </summary>
/// <param name="prefabAssetPaths">List of prefab paths to process</param>
/// <param name="basePath">The base path that was used to get the prefabs from the list</param>
/// <param name="ignoreUltimateXRAssets">Whether the prefab list was created ignoring UltimateXR assets</param>
/// <param name="processOriginalValues">
/// Whether to process components where they are in the original prefab that they were
/// added
/// </param>
/// <param name="processNonOriginalValues">
/// Whether to process components where they are not in the original prefab that
/// they were added, but inherited from another prefab instead
/// </param>
/// <param name="componentProcessor">
/// The component processor. It will receive the component to process as argument and it requires to return a boolean
/// telling whether the component was modified or not.
/// Here the GameObject that will be given as parameter is the GameObject that has the given component.
/// </param>
/// <param name="progressUpdater">
/// Will receive updates of the process so that the information can be fed to a Unity progress bar
/// </param>
/// <param name="canceled">Returns whether the user canceled the operation using the progress updater</param>
/// <param name="onlyCheckt">
/// Whether to only check if components should be processed, without making any changes. This
/// can be used to get how many elements would be changed without modifying any data
/// </param>
/// <typeparam name="T">The component type</typeparam>
/// <returns>True if there was at least one component that was modified</returns>
private static bool ProcessPrefabs<T>(List<string> prefabAssetPaths,
string basePath,
bool ignoreUltimateXRAssets,
bool processOriginalValues,
bool processNonOriginalValues,
UxrComponentProcessor<T> componentProcessor,
UxrProgressUpdater progressUpdater,
out bool canceled,
bool onlyCheck = false) where T : Component
{
bool processedAny = false;
canceled = false;
for (int i = 0; i < prefabAssetPaths.Count; ++i)
{
GameObject prefab = AssetDatabase.LoadMainAssetAtPath(prefabAssetPaths[i]) as GameObject;
bool processed = false;
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo("Processing prefabs", $"Prefab {prefab.name}", (float)i / prefabAssetPaths.Count));
if (canceled)
{
return processedAny;
}
}
// Get all components of the given type in the prefab
T[] components = prefab.GetComponentsInChildren<T>(true);
foreach (T component in components)
{
if (component != null && GetInnermostNon3DModelPrefabRoot(component, out GameObject originalPrefab, out GameObject prefabInstance, out T componentInPrefab))
{
// Only process those prefabs that are originally in this prefab and don't come from any prefab above in the hierarchy
bool isOriginalPrefab = originalPrefab == prefab;
bool process = (isOriginalPrefab && processOriginalValues) || (!isOriginalPrefab && processNonOriginalValues);
bool isInnermostPrefabInValidChain = isOriginalPrefab;
T componentInParentPrefab = PrefabUtility.GetCorrespondingObjectFromSource(component);
if (!isInnermostPrefabInValidChain && componentInParentPrefab != null && !PathRequiresProcessing(basePath, AssetDatabase.GetAssetPath(componentInParentPrefab), ignoreUltimateXRAssets))
{
isInnermostPrefabInValidChain = true;
}
if (process && componentProcessor != null && componentProcessor.Invoke(new UxrComponentInfo<T>(component, prefab, isOriginalPrefab, isInnermostPrefabInValidChain), onlyCheck))
{
processedAny = true;
processed = true;
}
}
}
if (processed && !onlyCheck)
{
PrefabUtility.SavePrefabAsset(prefab);
}
}
return processedAny;
}
/// <summary>
/// Processes all components of a given type in a scene only in gameObjects that are not prefab instances.
/// </summary>
/// <typeparam name="T">Component type to process</typeparam>
/// <param name="scene">Scene to process</param>
/// <param name="componentProcessor">
/// The component processor. It will receive the component to process as argument and it requires to return a boolean
/// telling whether the component was modified or not.
/// Here the GameObject that will be given as parameter is the GameObject that has the given component.
/// </param>
/// <param name="progressUpdater">
/// Will receive updates of the process so that the information can be fed to a Unity progress bar
/// </param>
/// <param name="canceled">Returns whether the user canceled the operation using the progress updater</param>
/// <param name="onlyCheckt">
/// Whether to only check if components should be processed, without making any changes. This
/// can be used to get how many elements would be changed without modifying any data
/// </param>
/// <returns>True if there was at least one component that was modified</returns>
/// <typeparam name="T">The component type</typeparam>
private static bool ProcessAllNonPrefabInstanceGameObjects<T>(Scene scene,
UxrComponentProcessor<T> componentProcessor,
UxrProgressUpdater progressUpdater,
out bool canceled,
bool onlyCheck = false) where T : Component
{
List<T> components = scene.GetRootGameObjects().SelectMany(go => go.GetComponentsInChildren<T>()).ToList();
bool processedAny = false;
canceled = false;
for (int c = 0; c < components.Count; ++c)
{
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo($"Processing scene {scene.name}", $"Object {components[c].name}", (float)c / components.Count));
if (canceled)
{
return processedAny;
}
}
bool isInstantiatedPrefab = GetInnermostNon3DModelPrefabRoot(components[c], out GameObject prefab, out GameObject prefabInstance, out T componentInPrefab);
// Only process if it's not an instantiated prefab
if (!isInstantiatedPrefab && componentProcessor != null && componentProcessor.Invoke(new UxrComponentInfo<T>(components[c], null, true, true), onlyCheck))
{
processedAny = true;
}
}
return processedAny;
}
/// <summary>
/// Processes all components of a given type in a scene only in prefab instances whose source prefabs lie outside a
/// specified project base path.
/// In this case, in <see cref="UxrComponentInfo{T}" /> the
/// <see cref="UxrComponentInfo{T}.IsInnermostInValidChain" /> will be marked as true.
/// This is useful in component processors that target the innermost prefab only, but when the user specifies a base
/// path that prevents the real innermost prefab from being processed.
/// </summary>
/// <typeparam name="T">Component type to process</typeparam>
/// <param name="scene">Scene to process</param>
/// <param name="basePath">
/// Project base path to process. This will be used to check whether the prefab instances in the
/// scene should be processed. The instances will be processed if their source prefab is not located in the base path
/// </param>
/// <param name="ignoreUltimateXRAssets">
/// Whether UltimateXR folders are being ignored. In a similar way to
/// <paramref name="basePath" />, if UltimateXR folders are being ignored, instances will be processed if their source
/// prefab is from UltimateXR directly
/// </param>
/// <param name="componentProcessor">
/// The component processor. It will receive the component to process as argument and it requires to return a boolean
/// telling whether the component was modified or not.
/// Here the GameObject that will be given as parameter is the GameObject that has the given component.
/// </param>
/// <param name="progressUpdater">
/// Will receive updates of the process so that the information can be fed to a Unity progress bar
/// </param>
/// <param name="canceled">Returns whether the user canceled the operation using the progress updater</param>
/// <param name="onlyCheckt">
/// Whether to only check if components should be processed, without making any changes. This
/// can be used to get how many elements would be changed without modifying any data
/// </param>
/// <returns>True if there was at least one component that was modified</returns>
/// <typeparam name="T">The component type</typeparam>
private static bool ProcessAllInnermostPrefabInstanceGameObjects<T>(Scene scene,
string basePath,
bool ignoreUltimateXRAssets,
UxrComponentProcessor<T> componentProcessor,
UxrProgressUpdater progressUpdater,
out bool canceled,
bool onlyCheck = false) where T : Component
{
List<T> components = scene.GetRootGameObjects().SelectMany(go => go.GetComponentsInChildren<T>()).ToList();
bool processedAny = false;
canceled = false;
for (int c = 0; c < components.Count; ++c)
{
if (progressUpdater != null)
{
canceled = progressUpdater.Invoke(new UxrProgressInfo($"Processing scene {scene.name}", $"Object {components[c].name}", (float)c / components.Count));
if (canceled)
{
return processedAny;
}
}
bool isInstantiatedPrefab = GetInnermostNon3DModelPrefabRoot(components[c], out GameObject prefab, out GameObject prefabInstance, out T componentInPrefab);
if (isInstantiatedPrefab)
{
T componentInParentPrefab = PrefabUtility.GetCorrespondingObjectFromSource(components[c]);
// Only process if it's an instantiated prefab whose prefab doesn't meet the path requirements
if (componentInParentPrefab != null && !PathRequiresProcessing(basePath, AssetDatabase.GetAssetPath(componentInParentPrefab), ignoreUltimateXRAssets))
{
if (componentProcessor != null && componentProcessor.Invoke(new UxrComponentInfo<T>(components[c], null, false, true), onlyCheck))
{
processedAny = true;
}
}
}
}
return processedAny;
}
/// <summary>
/// Gets all the prefab paths from a scene that contain components of the given type.
/// </summary>
/// <param name="scene">Scene to process</param>
/// <param name="paths">List where to add the prefab paths found</param>
/// <typeparam name="T">Component type</typeparam>
private static void GetScenePrefabPaths<T>(Scene scene, List<string> paths) where T : Component
{
List<T> components = scene.GetRootGameObjects().SelectMany(go => go.GetComponentsInChildren<T>()).ToList();
foreach (T c in components)
{
T componentInPrefab = PrefabUtility.GetCorrespondingObjectFromSource(c);
while (componentInPrefab != null)
{
string assetPath = AssetDatabase.GetAssetPath(componentInPrefab);
if (!string.IsNullOrEmpty(assetPath) &&
(PrefabUtility.GetPrefabAssetType(componentInPrefab) == PrefabAssetType.Regular || PrefabUtility.GetPrefabAssetType(componentInPrefab) == PrefabAssetType.Variant) &&
!paths.Contains(assetPath))
{
paths.Add(assetPath);
}
componentInPrefab = PrefabUtility.GetCorrespondingObjectFromSource(componentInPrefab);
}
}
}
/// <summary>
/// Same as AssetDatabase.GetAllAssetPaths() but filtering out assets from packages.
/// </summary>
/// <returns>List of asset paths filtering out assets from packages</returns>
private static string[] GetAllAssetPathsExceptPackages()
{
return AssetDatabase.GetAllAssetPaths().Where(p => !p.StartsWith("Packages/")).ToArray();
}
#endregion
}
}