A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Reflection;
5using System.Runtime.Serialization;
6using Unity.VisualScripting.AssemblyQualifiedNameParser;
7using UnityEngine;
8using Exception = System.Exception;
9using Unity.VisualScripting;
10
11[assembly: Unity.VisualScripting.RenamedNamespace("Bolt", "Unity.VisualScripting")]
12[assembly: Unity.VisualScripting.RenamedNamespace("Ludiq", "Unity.VisualScripting")]
13
14namespace Unity.VisualScripting
15{
16 public static class RuntimeCodebase
17 {
18 private static readonly object @lock = new object();
19
20 private static readonly List<Type> _types = new List<Type>();
21
22 public static IEnumerable<Type> types => _types;
23
24 private static readonly List<Assembly> _assemblies = new List<Assembly>();
25
26 public static IEnumerable<Assembly> assemblies => _assemblies;
27
28 /* (disallowedAssemblies)
29 This is a hack to force our RuntimeCodebase to use the RenamedTypeLookup for certain types when we deserialize them.
30 When we migrate from asset store to package assemblies (With new names), we want to deserialize our types
31 to the new types with new namespaces that exist in our new assemblies
32 (Ex: Unity.VisualScripting.SuperUnit instead of Bolt.SuperUnit).
33
34 Problem arises because we're migrating via script. Deleting the old assembly files on the disk doesn't remove
35 them from our AppDomain, and we can't unload specific assemblies.
36 Reloading the whole AppDomain would reload the migration scripts too, which would re-trigger the whole
37 migration flow and be bad UX.
38
39 So to avoid this problem, we don't reload the AppDomain (old assemblies still loaded) but just avoid them when
40 trying to deserialize types temporarily. When we Domain Reload at the end, it's cleaned up.
41
42 Without this, we get deserialization errors on migration to do with trying to instantiate a new type from an
43 old interface type.
44
45 This shouldn't cause much of a perf difference for most use because all our types are cached anyway,
46 and logic to do with this sits beyond the cached types layer.
47 */
48 public static HashSet<string> disallowedAssemblies = new HashSet<string>();
49
50 private static readonly Dictionary<string, Type> typeSerializations = new Dictionary<string, Type>();
51
52 private static Dictionary<string, Type> _renamedTypes = null;
53
54 private static Dictionary<string, string> _renamedNamespaces = null;
55
56 private static Dictionary<string, string> _renamedAssemblies = null;
57
58 private static readonly Dictionary<Type, Dictionary<string, string>> _renamedMembers = new Dictionary<Type, Dictionary<string, string>>();
59
60 static RuntimeCodebase()
61 {
62 lock (@lock)
63 {
64 foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
65 {
66 _assemblies.Add(assembly);
67
68 foreach (var assemblyType in assembly.GetTypesSafely())
69 {
70 _types.Add(assemblyType);
71 }
72 }
73 }
74 }
75
76 #region Assembly Attributes
77
78 public static IEnumerable<Attribute> GetAssemblyAttributes(Type attributeType)
79 {
80 return GetAssemblyAttributes(attributeType, assemblies);
81 }
82
83 public static IEnumerable<Attribute> GetAssemblyAttributes(Type attributeType, IEnumerable<Assembly> assemblies)
84 {
85 Ensure.That(nameof(attributeType)).IsNotNull(attributeType);
86 Ensure.That(nameof(assemblies)).IsNotNull(assemblies);
87
88 foreach (var assembly in assemblies)
89 {
90 foreach (var attribute in assembly.GetCustomAttributes(attributeType))
91 {
92 if (attributeType.IsInstanceOfType(attribute))
93 {
94 yield return attribute;
95 }
96 }
97 }
98 }
99
100 public static IEnumerable<TAttribute> GetAssemblyAttributes<TAttribute>(IEnumerable<Assembly> assemblies) where TAttribute : Attribute
101 {
102 return GetAssemblyAttributes(typeof(TAttribute), assemblies).Cast<TAttribute>();
103 }
104
105 public static IEnumerable<TAttribute> GetAssemblyAttributes<TAttribute>() where TAttribute : Attribute
106 {
107 return GetAssemblyAttributes(typeof(TAttribute)).Cast<TAttribute>();
108 }
109
110 #endregion
111
112 #region Serialization
113
114 public static void PrewarmTypeDeserialization(Type type)
115 {
116 Ensure.That(nameof(type)).IsNotNull(type);
117
118 var serialization = SerializeType(type);
119
120 if (typeSerializations.ContainsKey(serialization))
121 {
122 // Some are duplicates, but almost always compiler generated stuff.
123 // Safe to ignore, and anyway what would we even do to deserialize them properly?
124 }
125 else
126 {
127 typeSerializations.Add(serialization, type);
128 }
129 }
130
131 public static string SerializeType(Type type)
132 {
133 Ensure.That(nameof(type)).IsNotNull(type);
134
135 return type?.FullName;
136 }
137
138 public static bool TryDeserializeType(string typeName, out Type type)
139 {
140 if (string.IsNullOrEmpty(typeName))
141 {
142 type = null;
143 return false;
144 }
145
146 lock (@lock)
147 {
148 if (!TryCachedTypeLookup(typeName, out type))
149 {
150 if (!TrySystemTypeLookup(typeName, out type))
151 {
152 if (!TryRenamedTypeLookup(typeName, out type))
153 {
154 return false;
155 }
156 }
157
158 typeSerializations.Add(typeName, type);
159 }
160
161 return true;
162 }
163 }
164
165 public static Type DeserializeType(string typeName)
166 {
167 if (!TryDeserializeType(typeName, out var type))
168 {
169 throw new SerializationException($"Unable to find type: '{typeName ?? "(null)"}'.");
170 }
171
172 return type;
173 }
174
175 public static void ClearCachedTypes()
176 {
177 typeSerializations.Clear();
178 }
179
180 private static bool TryCachedTypeLookup(string typeName, out Type type)
181 {
182 return typeSerializations.TryGetValue(typeName, out type);
183 }
184
185 private static bool TrySystemTypeLookup(string typeName, out Type type)
186 {
187 foreach (var assembly in _assemblies)
188 {
189 if (disallowedAssemblies.Contains(assembly.GetName().Name))
190 continue;
191
192 type = assembly.GetType(typeName);
193
194 if (type != null)
195 {
196 // This catches things like generic parameters of system collection types using disallowed assembly types
197 // Ex: System HashSet<Ludiq.xyz>
198 foreach (var disallowed in disallowedAssemblies)
199 {
200 if (type.FullName.Contains(disallowed))
201 {
202 return false;
203 }
204 }
205
206 return true;
207 }
208 }
209
210 type = null;
211 return false;
212 }
213
214 private static bool TrySystemTypeLookup(TypeName typeName, out Type type)
215 {
216 if (disallowedAssemblies.Contains(typeName.AssemblyName))
217 {
218 type = null;
219 return false;
220 }
221
222 // Can't retrieve an array with the ToLooseString format so use the type Name and compare Assemblies
223 if (typeName.IsArray)
224 {
225 foreach (var assembly in _assemblies.Where(a => typeName.AssemblyName == a.GetName().Name))
226 {
227 type = assembly.GetType(typeName.Name);
228 if (type != null)
229 {
230 return true;
231 }
232 }
233
234 type = null;
235 return false;
236 }
237
238 return TrySystemTypeLookup(typeName.ToLooseString(), out type);
239 }
240
241 private static bool TryRenamedTypeLookup(string previousTypeName, out Type type)
242 {
243 // Try for an exact match in our renamed types dictionary.
244 // That should work for every non-generic type.
245 if (renamedTypes.TryGetValue(previousTypeName, out var newType))
246 {
247 type = newType;
248 return true;
249 }
250 // If we can't get an exact match, we'll try parsing the previous type name,
251 // replacing all the renamed types we can find, then reconstructing it.
252 else
253 {
254 var parsedTypeName = TypeName.Parse(previousTypeName);
255
256 foreach (var renamedType in renamedTypes)
257 {
258 parsedTypeName.ReplaceName(renamedType.Key, renamedType.Value);
259 }
260
261 foreach (var renamedNamespace in renamedNamespaces)
262 {
263 parsedTypeName.ReplaceNamespace(renamedNamespace.Key, renamedNamespace.Value);
264 }
265
266 foreach (var renamedAssembly in renamedAssemblies)
267 {
268 parsedTypeName.ReplaceAssembly(renamedAssembly.Key, renamedAssembly.Value);
269 }
270
271 // Run the system lookup
272 if (TrySystemTypeLookup(parsedTypeName, out type))
273 {
274 return true;
275 }
276
277 type = null;
278 return false;
279 }
280 }
281
282 #endregion
283
284 #region Renaming
285
286 // Can't use AttributeUtility here, because the caching system will
287 // try to load all attributes of the type for efficiency, which is
288 // not allowed on the serialization thread because some of Unity's
289 // attribute constructors use Unity API methods (ugh!).
290
291 public static Dictionary<string, string> renamedNamespaces
292 {
293 get
294 {
295 if (_renamedNamespaces == null)
296 {
297 _renamedNamespaces = FetchRenamedNamespaces();
298 }
299
300 return _renamedNamespaces;
301 }
302 }
303
304 public static Dictionary<string, string> renamedAssemblies
305 {
306 get
307 {
308 if (_renamedAssemblies == null)
309 {
310 _renamedAssemblies = FetchRenamedAssemblies();
311 }
312
313 return _renamedAssemblies;
314 }
315 }
316
317 public static Dictionary<string, Type> renamedTypes
318 {
319 get
320 {
321 if (_renamedTypes == null)
322 {
323 // Fetch only on demand because attribute lookups are expensive
324 _renamedTypes = FetchRenamedTypes();
325 }
326
327 return _renamedTypes;
328 }
329 }
330
331 public static Dictionary<string, string> RenamedMembers(Type type)
332 {
333 Dictionary<string, string> renamedMembers;
334
335 if (!_renamedMembers.TryGetValue(type, out renamedMembers))
336 {
337 renamedMembers = FetchRenamedMembers(type);
338 _renamedMembers.Add(type, renamedMembers);
339 }
340
341 return renamedMembers;
342 }
343
344 private static Dictionary<string, string> FetchRenamedMembers(Type type)
345 {
346 Ensure.That(nameof(type)).IsNotNull(type);
347
348 var renamedMembers = new Dictionary<string, string>();
349
350 var members = type.GetExtendedMembers(Member.SupportedBindingFlags);
351
352 foreach (var member in members)
353 {
354 IEnumerable<RenamedFromAttribute> renamedFromAttributes;
355
356 try
357 {
358 renamedFromAttributes = Attribute.GetCustomAttributes(member, typeof(RenamedFromAttribute), false).Cast<RenamedFromAttribute>();
359 }
360 catch (Exception ex)
361 {
362 Debug.LogWarning($"Failed to fetch RenamedFrom attributes for member '{member}':\n{ex}");
363 continue;
364 }
365
366 var newMemberName = member.Name;
367
368 foreach (var renamedFromAttribute in renamedFromAttributes)
369 {
370 var previousMemberName = renamedFromAttribute.previousName;
371
372 if (renamedMembers.ContainsKey(previousMemberName))
373 {
374 Debug.LogWarning($"Multiple members on '{type}' indicate having been renamed from '{previousMemberName}'.\nIgnoring renamed attributes for '{member}'.");
375
376 continue;
377 }
378
379 renamedMembers.Add(previousMemberName, newMemberName);
380 }
381 }
382
383 return renamedMembers;
384 }
385
386 private static Dictionary<string, string> FetchRenamedNamespaces()
387 {
388 var renamedNamespaces = new Dictionary<string, string>();
389
390 foreach (var renamedNamespaceAttribute in GetAssemblyAttributes<RenamedNamespaceAttribute>())
391 {
392 var previousNamespaceName = renamedNamespaceAttribute.previousName;
393 var newNamespaceName = renamedNamespaceAttribute.newName;
394
395 if (renamedNamespaces.ContainsKey(previousNamespaceName))
396 {
397 Debug.LogWarning($"Multiple new names have been provided for namespace '{previousNamespaceName}'.\nIgnoring new name '{newNamespaceName}'.");
398
399 continue;
400 }
401
402 renamedNamespaces.Add(previousNamespaceName, newNamespaceName);
403 }
404
405 return renamedNamespaces;
406 }
407
408 private static Dictionary<string, string> FetchRenamedAssemblies()
409 {
410 var renamedAssemblies = new Dictionary<string, string>();
411
412 foreach (var renamedAssemblyAttribute in GetAssemblyAttributes<RenamedAssemblyAttribute>())
413 {
414 var previousAssemblyName = renamedAssemblyAttribute.previousName;
415 var newAssemblyName = renamedAssemblyAttribute.newName;
416
417 if (renamedAssemblies.ContainsKey(previousAssemblyName))
418 {
419 Debug.LogWarning($"Multiple new names have been provided for assembly '{previousAssemblyName}'.\nIgnoring new name '{newAssemblyName}'.");
420
421 continue;
422 }
423
424 renamedAssemblies.Add(previousAssemblyName, newAssemblyName);
425 }
426
427 return renamedAssemblies;
428 }
429
430 private static Dictionary<string, Type> FetchRenamedTypes()
431 {
432 var renamedTypes = new Dictionary<string, Type>();
433
434 foreach (var assembly in assemblies)
435 {
436 foreach (var type in assembly.GetTypesSafely())
437 {
438 IEnumerable<RenamedFromAttribute> renamedFromAttributes;
439
440 try
441 {
442 renamedFromAttributes = Attribute.GetCustomAttributes(type, typeof(RenamedFromAttribute), false).Cast<RenamedFromAttribute>();
443 }
444 catch (Exception ex)
445 {
446 Debug.LogWarning($"Failed to fetch RenamedFrom attributes for type '{type}':\n{ex}");
447 continue;
448 }
449
450 var newTypeName = type.FullName;
451
452 foreach (var renamedFromAttribute in renamedFromAttributes)
453 {
454 var previousTypeName = renamedFromAttribute.previousName;
455
456 if (renamedTypes.ContainsKey(previousTypeName))
457 {
458 Debug.LogWarning($"Multiple types indicate having been renamed from '{previousTypeName}'.\nIgnoring renamed attributes for '{type}'.");
459
460 continue;
461 }
462
463 renamedTypes.Add(previousTypeName, type);
464 }
465 }
466 }
467
468 return renamedTypes;
469 }
470
471 #endregion
472 }
473}