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}