using System; using System.IO; using UnityEngine.Assertions; #if UNITY_EDITOR using UnityEditor; using UnityEditorInternal; using System.Reflection; #endif namespace UnityEngine.Rendering { #if UNITY_EDITOR /// /// The resources that need to be reloaded in Editor can live in Runtime. /// The reload call should only be done in Editor context though but it /// could be called from runtime entities. /// public static class ResourceReloader { /// /// Looks for resources in the given object and reload the ones /// that are missing or broken. /// This version will still return null value without throwing error if the issue is due to /// AssetDatabase being not ready. But in this case the assetDatabaseNotReady result will be true. /// /// The object containing reload-able resources /// The base path for the package /// /// - 1 hasChange: True if something have been reloaded. /// - 2 assetDatabaseNotReady: True if the issue preventing loading is due to state of AssetDatabase /// public static (bool hasChange, bool assetDatabaseNotReady) TryReloadAllNullIn(System.Object container, string basePath) { try { return (ReloadAllNullIn(container, basePath), false); } catch (InvalidImportException) { return (false, true); } catch (Exception e) { throw e; } } /// /// Looks for resources in the given object and reload the ones /// that are missing or broken. /// /// The object containing reload-able resources /// The base path for the package /// True if something have been reloaded. public static bool ReloadAllNullIn(System.Object container, string basePath) { if (IsNull(container)) return false; var changed = false; foreach (var fieldInfo in container.GetType() .GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) { //Recurse on sub-containers if (IsReloadGroup(fieldInfo)) { changed |= FixGroupIfNeeded(container, fieldInfo); changed |= ReloadAllNullIn(fieldInfo.GetValue(container), basePath); } //Find null field and reload them var attribute = GetReloadAttribute(fieldInfo); if (attribute != null) { if (attribute.paths.Length == 1) { changed |= SetAndLoadIfNull(container, fieldInfo, GetFullPath(basePath, attribute), attribute.package); } else if (attribute.paths.Length > 1) { changed |= FixArrayIfNeeded(container, fieldInfo, attribute.paths.Length); var array = (Array)fieldInfo.GetValue(container); if (IsReloadGroup(array)) { //Recurse on each sub-containers for (int index = 0; index < attribute.paths.Length; ++index) { changed |= FixGroupIfNeeded(array, index); changed |= ReloadAllNullIn(array.GetValue(index), basePath); } } else { //Find each null element and reload them for (int index = 0; index < attribute.paths.Length; ++index) changed |= SetAndLoadIfNull(array, index, GetFullPath(basePath, attribute, index), attribute.package); } } } } if (changed && container is UnityEngine.Object c) EditorUtility.SetDirty(c); return changed; } static void CheckReloadGroupSupportedType(Type type) { if (type.IsSubclassOf(typeof(ScriptableObject))) throw new Exception(@$"ReloadGroup attribute must not be used on {nameof(ScriptableObject)}. If {nameof(ResourceReloader)} create an instance of it, it will be not saved as a file, resulting in corrupted ID when building."); } static bool FixGroupIfNeeded(System.Object container, FieldInfo info) { var type = info.FieldType; CheckReloadGroupSupportedType(type); if (IsNull(container, info)) { var value = Activator.CreateInstance(type); info.SetValue( container, value ); return true; } return false; } static bool FixGroupIfNeeded(Array array, int index) { Assert.IsNotNull(array); var type = array.GetType().GetElementType(); CheckReloadGroupSupportedType(type); if (IsNull(array.GetValue(index))) { var value = type.IsSubclassOf(typeof(ScriptableObject)) ? ScriptableObject.CreateInstance(type) : Activator.CreateInstance(type); array.SetValue(value, index); return true; } return false; } static bool FixArrayIfNeeded(System.Object container, FieldInfo info, int length) { if (IsNull(container, info) || ((Array)info.GetValue(container)).Length < length) { info.SetValue(container, Activator.CreateInstance(info.FieldType, length)); return true; } return false; } static ReloadAttribute GetReloadAttribute(FieldInfo fieldInfo) { var attributes = (ReloadAttribute[])fieldInfo .GetCustomAttributes(typeof(ReloadAttribute), false); if (attributes.Length == 0) return null; return attributes[0]; } static bool IsReloadGroup(FieldInfo info) => info.FieldType .GetCustomAttributes(typeof(ReloadGroupAttribute), false).Length > 0; static bool IsReloadGroup(Array field) => field.GetType().GetElementType() .GetCustomAttributes(typeof(ReloadGroupAttribute), false).Length > 0; static bool IsNull(System.Object container, FieldInfo info) => IsNull(info.GetValue(container)); static bool IsNull(System.Object field) => field == null || field.Equals(null); static UnityEngine.Object Load(string path, Type type, ReloadAttribute.Package location) { // Check if asset exist. // Direct loading can be prevented by AssetDatabase being reloading. var guid = AssetDatabase.AssetPathToGUID(path); if (location == ReloadAttribute.Package.Root && String.IsNullOrEmpty(guid)) throw new Exception($"Cannot load. Incorrect path: {path}"); // Else the path is good. Attempt loading resource if AssetDatabase available. UnityEngine.Object result; switch (location) { case ReloadAttribute.Package.Builtin: if (type == typeof(Shader)) result = Shader.Find(path); else result = Resources.GetBuiltinResource(type, path); //handle wrong path error break; case ReloadAttribute.Package.BuiltinExtra: if (type == typeof(Shader)) result = Shader.Find(path); else result = AssetDatabase.GetBuiltinExtraResource(type, path); //handle wrong path error break; case ReloadAttribute.Package.Root: result = AssetDatabase.LoadAssetAtPath(path, type); break; default: throw new NotImplementedException($"Unknown {location}"); } if (IsNull(result)) { throw new InvalidImportException($"Cannot load. Path {path} is correct but AssetDatabase cannot load now."); } return result; } static bool SetAndLoadIfNull(System.Object container, FieldInfo info, string path, ReloadAttribute.Package location) { if (IsNull(container, info)) { info.SetValue(container, Load(path, info.FieldType, location)); return true; } return false; } static bool SetAndLoadIfNull(Array array, int index, string path, ReloadAttribute.Package location) { var element = array.GetValue(index); if (IsNull(element)) { array.SetValue(Load(path, array.GetType().GetElementType(), location), index); return true; } return false; } static string GetFullPath(string basePath, ReloadAttribute attribute, int index = 0) { string path; switch (attribute.package) { case ReloadAttribute.Package.Builtin: path = attribute.paths[index]; break; case ReloadAttribute.Package.Root: path = basePath + "/" + attribute.paths[index]; break; default: throw new ArgumentException("Unknown Package Path!"); } return path; } // It's not perfect retrying right away but making it called in EditorApplication.delayCall // from EnsureResources creates GC which we want to avoid static void DelayedNullReload(string resourcePath) where T : RenderPipelineResources { T resourcesDelayed = AssetDatabase.LoadAssetAtPath(resourcePath); if (resourcesDelayed == null) EditorApplication.delayCall += () => DelayedNullReload(resourcePath); else ResourceReloader.ReloadAllNullIn(resourcesDelayed, resourcesDelayed.packagePath_Internal); } /// /// Ensures that all resources in a container has been loaded /// /// Set to true to force all resources to be reloaded even if they are loaded already /// The resource container with the resulting loaded resources /// The asset path to load the resource container from /// Function to test if the resource container is present in a RenderPipelineGlobalSettings /// RenderPipelineGlobalSettings to be passed to checker to test of the resource container is already loaded public static void EnsureResources(bool forceReload, ref T resources, string resourcePath, Func checker, S settings) where T : RenderPipelineResources where S : RenderPipelineGlobalSettings { T resourceChecked = null; if (checker(settings)) { if (!EditorUtility.IsPersistent(resources)) // if not loaded from the Asset database { // try to load from AssetDatabase if it is ready resourceChecked = AssetDatabase.LoadAssetAtPath(resourcePath); if (resourceChecked && !resourceChecked.Equals(null)) resources = resourceChecked; } if (forceReload) ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal); return; } resourceChecked = AssetDatabase.LoadAssetAtPath(resourcePath); if (resourceChecked != null && !resourceChecked.Equals(null)) { resources = resourceChecked; if (forceReload) ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal); } else { // Asset database may not be ready var objs = InternalEditorUtility.LoadSerializedFileAndForget(resourcePath); resources = (objs != null && objs.Length > 0) ? objs[0] as T : null; if (forceReload) { try { if (ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal)) { InternalEditorUtility.SaveToSerializedFileAndForget( new Object[] { resources }, resourcePath, true); } } catch (System.Exception e) { // This can be called at a time where AssetDatabase is not available for loading. // When this happens, the GUID can be get but the resource loaded will be null. // Using the ResourceReloader mechanism in CoreRP, it checks this and add InvalidImport data when this occurs. if (!(e.Data.Contains("InvalidImport") && e.Data["InvalidImport"] is int dii && dii == 1)) Debug.LogException(e); else DelayedNullReload(resourcePath); } } } Debug.Assert(checker(settings), $"Could not load {typeof(T).Name}."); } } #endif }