A game about forced loneliness, made by TACStudios
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}