A game about forced loneliness, made by TACStudios
at master 350 lines 15 kB view raw
1using System; 2using System.IO; 3using UnityEngine.Assertions; 4#if UNITY_EDITOR 5using UnityEditor; 6using UnityEditorInternal; 7using System.Reflection; 8#endif 9 10namespace UnityEngine.Rendering 11{ 12#if UNITY_EDITOR 13 /// <summary> 14 /// The resources that need to be reloaded in Editor can live in Runtime. 15 /// The reload call should only be done in Editor context though but it 16 /// could be called from runtime entities. 17 /// </summary> 18 public static class ResourceReloader 19 { 20 /// <summary> 21 /// Looks for resources in the given <paramref name="container"/> object and reload the ones 22 /// that are missing or broken. 23 /// This version will still return null value without throwing error if the issue is due to 24 /// AssetDatabase being not ready. But in this case the assetDatabaseNotReady result will be true. 25 /// </summary> 26 /// <param name="container">The object containing reload-able resources</param> 27 /// <param name="basePath">The base path for the package</param> 28 /// <returns> 29 /// - 1 hasChange: True if something have been reloaded. 30 /// - 2 assetDatabaseNotReady: True if the issue preventing loading is due to state of AssetDatabase 31 /// </returns> 32 public static (bool hasChange, bool assetDatabaseNotReady) TryReloadAllNullIn(System.Object container, string basePath) 33 { 34 try 35 { 36 return (ReloadAllNullIn(container, basePath), false); 37 } 38 catch (InvalidImportException) 39 { 40 return (false, true); 41 } 42 catch (Exception e) 43 { 44 throw e; 45 } 46 } 47 48 /// <summary> 49 /// Looks for resources in the given <paramref name="container"/> object and reload the ones 50 /// that are missing or broken. 51 /// </summary> 52 /// <param name="container">The object containing reload-able resources</param> 53 /// <param name="basePath">The base path for the package</param> 54 /// <returns>True if something have been reloaded.</returns> 55 public static bool ReloadAllNullIn(System.Object container, string basePath) 56 { 57 if (IsNull(container)) 58 return false; 59 60 var changed = false; 61 foreach (var fieldInfo in container.GetType() 62 .GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) 63 { 64 //Recurse on sub-containers 65 if (IsReloadGroup(fieldInfo)) 66 { 67 changed |= FixGroupIfNeeded(container, fieldInfo); 68 changed |= ReloadAllNullIn(fieldInfo.GetValue(container), basePath); 69 } 70 71 //Find null field and reload them 72 var attribute = GetReloadAttribute(fieldInfo); 73 if (attribute != null) 74 { 75 if (attribute.paths.Length == 1) 76 { 77 changed |= SetAndLoadIfNull(container, fieldInfo, GetFullPath(basePath, attribute), 78 attribute.package); 79 } 80 else if (attribute.paths.Length > 1) 81 { 82 changed |= FixArrayIfNeeded(container, fieldInfo, attribute.paths.Length); 83 84 var array = (Array)fieldInfo.GetValue(container); 85 if (IsReloadGroup(array)) 86 { 87 //Recurse on each sub-containers 88 for (int index = 0; index < attribute.paths.Length; ++index) 89 { 90 changed |= FixGroupIfNeeded(array, index); 91 changed |= ReloadAllNullIn(array.GetValue(index), basePath); 92 } 93 } 94 else 95 { 96 //Find each null element and reload them 97 for (int index = 0; index < attribute.paths.Length; ++index) 98 changed |= SetAndLoadIfNull(array, index, GetFullPath(basePath, attribute, index), 99 attribute.package); 100 } 101 } 102 } 103 } 104 105 if (changed && container is UnityEngine.Object c) 106 EditorUtility.SetDirty(c); 107 return changed; 108 } 109 110 static void CheckReloadGroupSupportedType(Type type) 111 { 112 if (type.IsSubclassOf(typeof(ScriptableObject))) 113 throw new Exception(@$"ReloadGroup attribute must not be used on {nameof(ScriptableObject)}. 114If {nameof(ResourceReloader)} create an instance of it, it will be not saved as a file, resulting in corrupted ID when building."); 115 } 116 117 static bool FixGroupIfNeeded(System.Object container, FieldInfo info) 118 { 119 var type = info.FieldType; 120 CheckReloadGroupSupportedType(type); 121 122 if (IsNull(container, info)) 123 { 124 var value = Activator.CreateInstance(type); 125 126 info.SetValue( 127 container, 128 value 129 ); 130 return true; 131 } 132 133 return false; 134 } 135 136 static bool FixGroupIfNeeded(Array array, int index) 137 { 138 Assert.IsNotNull(array); 139 140 var type = array.GetType().GetElementType(); 141 CheckReloadGroupSupportedType(type); 142 143 if (IsNull(array.GetValue(index))) 144 { 145 var value = type.IsSubclassOf(typeof(ScriptableObject)) 146 ? ScriptableObject.CreateInstance(type) 147 : Activator.CreateInstance(type); 148 149 array.SetValue(value, index); 150 return true; 151 } 152 153 return false; 154 } 155 156 static bool FixArrayIfNeeded(System.Object container, FieldInfo info, int length) 157 { 158 if (IsNull(container, info) || ((Array)info.GetValue(container)).Length < length) 159 { 160 info.SetValue(container, Activator.CreateInstance(info.FieldType, length)); 161 return true; 162 } 163 164 return false; 165 } 166 167 static ReloadAttribute GetReloadAttribute(FieldInfo fieldInfo) 168 { 169 var attributes = (ReloadAttribute[])fieldInfo 170 .GetCustomAttributes(typeof(ReloadAttribute), false); 171 if (attributes.Length == 0) 172 return null; 173 return attributes[0]; 174 } 175 176 static bool IsReloadGroup(FieldInfo info) 177 => info.FieldType 178 .GetCustomAttributes(typeof(ReloadGroupAttribute), false).Length > 0; 179 180 static bool IsReloadGroup(Array field) 181 => field.GetType().GetElementType() 182 .GetCustomAttributes(typeof(ReloadGroupAttribute), false).Length > 0; 183 184 static bool IsNull(System.Object container, FieldInfo info) 185 => IsNull(info.GetValue(container)); 186 187 static bool IsNull(System.Object field) 188 => field == null || field.Equals(null); 189 190 static UnityEngine.Object Load(string path, Type type, ReloadAttribute.Package location) 191 { 192 // Check if asset exist. 193 // Direct loading can be prevented by AssetDatabase being reloading. 194 var guid = AssetDatabase.AssetPathToGUID(path); 195 if (location == ReloadAttribute.Package.Root && String.IsNullOrEmpty(guid)) 196 throw new Exception($"Cannot load. Incorrect path: {path}"); 197 198 // Else the path is good. Attempt loading resource if AssetDatabase available. 199 UnityEngine.Object result; 200 switch (location) 201 { 202 case ReloadAttribute.Package.Builtin: 203 if (type == typeof(Shader)) 204 result = Shader.Find(path); 205 else 206 result = Resources.GetBuiltinResource(type, path); //handle wrong path error 207 break; 208 case ReloadAttribute.Package.BuiltinExtra: 209 if (type == typeof(Shader)) 210 result = Shader.Find(path); 211 else 212 result = AssetDatabase.GetBuiltinExtraResource(type, path); //handle wrong path error 213 break; 214 case ReloadAttribute.Package.Root: 215 result = AssetDatabase.LoadAssetAtPath(path, type); 216 break; 217 default: 218 throw new NotImplementedException($"Unknown {location}"); 219 } 220 221 if (IsNull(result)) 222 { 223 throw new InvalidImportException($"Cannot load. Path {path} is correct but AssetDatabase cannot load now."); 224 } 225 return result; 226 } 227 228 static bool SetAndLoadIfNull(System.Object container, FieldInfo info, 229 string path, ReloadAttribute.Package location) 230 { 231 if (IsNull(container, info)) 232 { 233 info.SetValue(container, Load(path, info.FieldType, location)); 234 return true; 235 } 236 237 return false; 238 } 239 240 static bool SetAndLoadIfNull(Array array, int index, string path, ReloadAttribute.Package location) 241 { 242 var element = array.GetValue(index); 243 if (IsNull(element)) 244 { 245 array.SetValue(Load(path, array.GetType().GetElementType(), location), index); 246 return true; 247 } 248 249 return false; 250 } 251 252 static string GetFullPath(string basePath, ReloadAttribute attribute, int index = 0) 253 { 254 string path; 255 switch (attribute.package) 256 { 257 case ReloadAttribute.Package.Builtin: 258 path = attribute.paths[index]; 259 break; 260 case ReloadAttribute.Package.Root: 261 path = basePath + "/" + attribute.paths[index]; 262 break; 263 default: 264 throw new ArgumentException("Unknown Package Path!"); 265 } 266 return path; 267 } 268 269 // It's not perfect retrying right away but making it called in EditorApplication.delayCall 270 // from EnsureResources creates GC which we want to avoid 271 static void DelayedNullReload<T>(string resourcePath) 272 where T : RenderPipelineResources 273 { 274 T resourcesDelayed = AssetDatabase.LoadAssetAtPath<T>(resourcePath); 275 if (resourcesDelayed == null) 276 EditorApplication.delayCall += () => DelayedNullReload<T>(resourcePath); 277 else 278 ResourceReloader.ReloadAllNullIn(resourcesDelayed, resourcesDelayed.packagePath_Internal); 279 } 280 281 /// <summary> 282 /// Ensures that all resources in a container has been loaded 283 /// </summary> 284 /// <param name="forceReload">Set to true to force all resources to be reloaded even if they are loaded already</param> 285 /// <param name="resources">The resource container with the resulting loaded resources</param> 286 /// <param name="resourcePath">The asset path to load the resource container from</param> 287 /// <param name="checker">Function to test if the resource container is present in a RenderPipelineGlobalSettings</param> 288 /// <param name="settings">RenderPipelineGlobalSettings to be passed to checker to test of the resource container is already loaded</param> 289 public static void EnsureResources<T, S>(bool forceReload, ref T resources, string resourcePath, Func<S, bool> checker, S settings) 290 where T : RenderPipelineResources where S : RenderPipelineGlobalSettings 291 { 292 T resourceChecked = null; 293 294 if (checker(settings)) 295 { 296 if (!EditorUtility.IsPersistent(resources)) // if not loaded from the Asset database 297 { 298 // try to load from AssetDatabase if it is ready 299 resourceChecked = AssetDatabase.LoadAssetAtPath<T>(resourcePath); 300 if (resourceChecked && !resourceChecked.Equals(null)) 301 resources = resourceChecked; 302 } 303 304 if (forceReload) 305 ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal); 306 307 return; 308 } 309 310 resourceChecked = AssetDatabase.LoadAssetAtPath<T>(resourcePath); 311 if (resourceChecked != null && !resourceChecked.Equals(null)) 312 { 313 resources = resourceChecked; 314 if (forceReload) 315 ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal); 316 } 317 else 318 { 319 // Asset database may not be ready 320 var objs = InternalEditorUtility.LoadSerializedFileAndForget(resourcePath); 321 resources = (objs != null && objs.Length > 0) ? objs[0] as T : null; 322 if (forceReload) 323 { 324 try 325 { 326 if (ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal)) 327 { 328 InternalEditorUtility.SaveToSerializedFileAndForget( 329 new Object[] { resources }, 330 resourcePath, 331 true); 332 } 333 } 334 catch (System.Exception e) 335 { 336 // This can be called at a time where AssetDatabase is not available for loading. 337 // When this happens, the GUID can be get but the resource loaded will be null. 338 // Using the ResourceReloader mechanism in CoreRP, it checks this and add InvalidImport data when this occurs. 339 if (!(e.Data.Contains("InvalidImport") && e.Data["InvalidImport"] is int dii && dii == 1)) 340 Debug.LogException(e); 341 else 342 DelayedNullReload<T>(resourcePath); 343 } 344 } 345 } 346 Debug.Assert(checker(settings), $"Could not load {typeof(T).Name}."); 347 } 348 } 349#endif 350}