using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using UnityEngine;
using UnityObject = UnityEngine.Object;
#if UNITY_6000_5_OR_NEWER
using UnityEngine.Assemblies;
#endif
namespace Unity.VisualScripting
{
public static class Codebase
{
static Codebase()
{
using (ProfilingUtility.SampleBlock("Codebase initialization"))
{
_assemblies = new List<Assembly>();
_runtimeAssemblies = new List<Assembly>();
_editorAssemblies = new List<Assembly>();
_ludiqAssemblies = new List<Assembly>();
_ludiqRuntimeAssemblies = new List<Assembly>();
_ludiqEditorAssemblies = new List<Assembly>();
_types = new List<Type>();
_runtimeTypes = new List<Type>();
_editorTypes = new List<Type>();
_ludiqTypes = new List<Type>();
_ludiqRuntimeTypes = new List<Type>();
_ludiqEditorTypes = new List<Type>();
#if UNITY_6000_5_OR_NEWER
foreach (var assembly in CurrentAssemblies.GetLoadedAssemblies())
#else
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
#endif
{
try
{
#if NET_4_6
if (assembly.IsDynamic)
{
continue;
}
#endif
_assemblies.Add(assembly);
var isRuntimeAssembly = IsRuntimeAssembly(assembly);
var isEditorAssembly = IsEditorAssembly(assembly, new HashSet<string>());
var isLudiqRuntimeDependentAssembly = IsLudiqRuntimeDependentAssembly(assembly);
var isLudiqEditorDependentAssembly = IsLudiqEditorDependentAssembly(assembly);
var isLudiqAssembly = isLudiqRuntimeDependentAssembly || isLudiqEditorDependentAssembly;
var isLudiqEditorAssembly = isLudiqEditorDependentAssembly;
var isLudiqRuntimeAssembly = isLudiqRuntimeDependentAssembly && !isLudiqEditorDependentAssembly;
if (isRuntimeAssembly)
{
_runtimeAssemblies.Add(assembly);
}
if (isEditorAssembly)
{
_editorAssemblies.Add(assembly);
}
if (isLudiqAssembly)
{
_ludiqAssemblies.Add(assembly);
}
if (isLudiqEditorAssembly)
{
_ludiqEditorAssemblies.Add(assembly);
}
if (isLudiqRuntimeAssembly)
{
_ludiqRuntimeAssemblies.Add(assembly);
}
foreach (var type in assembly.GetTypesSafely())
{
_types.Add(type);
if (isRuntimeAssembly)
{
_runtimeTypes.Add(type);
}
if (isEditorAssembly)
{
_editorTypes.Add(type);
}
if (isLudiqAssembly)
{
_ludiqTypes.Add(type);
}
if (isLudiqEditorAssembly)
{
_ludiqEditorTypes.Add(type);
}
if (isLudiqRuntimeAssembly)
{
_ludiqRuntimeTypes.Add(type);
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to analyze assembly '{assembly}':\n{ex}");
}
}
assemblies = _assemblies.AsReadOnly();
runtimeAssemblies = _runtimeAssemblies.AsReadOnly();
editorAssemblies = _editorAssemblies.AsReadOnly();
ludiqAssemblies = _ludiqAssemblies.AsReadOnly();
ludiqRuntimeAssemblies = _ludiqRuntimeAssemblies.AsReadOnly();
ludiqEditorAssemblies = _ludiqEditorAssemblies.AsReadOnly();
types = _types.AsReadOnly();
runtimeTypes = _runtimeTypes.AsReadOnly();
editorTypes = _editorTypes.AsReadOnly();
ludiqTypes = _ludiqTypes.AsReadOnly();
ludiqRuntimeTypes = _ludiqRuntimeTypes.AsReadOnly();
ludiqEditorTypes = _ludiqEditorTypes.AsReadOnly();
}
}
private static readonly List<Assembly> _assemblies;
private static readonly List<Assembly> _runtimeAssemblies;
private static readonly List<Assembly> _editorAssemblies;
private static readonly List<Assembly> _ludiqAssemblies;
private static readonly List<Assembly> _ludiqRuntimeAssemblies;
private static readonly List<Assembly> _ludiqEditorAssemblies;
private static readonly List<Type> _types;
private static readonly List<Type> _runtimeTypes;
private static readonly List<Type> _editorTypes;
private static readonly List<Type> _ludiqTypes;
private static readonly List<Type> _ludiqRuntimeTypes;
private static readonly List<Type> _ludiqEditorTypes;
private static List<Assembly> _settingsAssemblies;
private static List<Type> _settingsAssembliesTypes;
private static List<Type> _settingsTypes;
private static readonly Dictionary<Assembly, bool> _editorAssemblyCache = new Dictionary<Assembly, bool>();
private static readonly Dictionary<Assembly, string> _assemblyNameCache = new Dictionary<Assembly, string>();
#region Serialization
public static string SerializeType(Type type)
{
return RuntimeCodebase.SerializeType(type);
}
public static bool TryDeserializeType(string typeName, out Type type)
{
return RuntimeCodebase.TryDeserializeType(typeName, out type);
}
public static Type DeserializeType(string typeName)
{
return RuntimeCodebase.DeserializeType(typeName);
}
private const char memberDataSeparator = ';';
public static string SerializeMember(Member member)
{
// Format: "targetType;name;paramType1;paramType2"
// Example: "UnityEngine.Transform;rotate;UnityEngine.Vector3"
var sb = new StringBuilder();
sb.Append(SerializeType(member.targetType));
sb.Append(memberDataSeparator);
sb.Append(member.name);
if (member.parameterTypes != null)
{
sb.Append(memberDataSeparator);
for (int i = 0; i < member.parameterTypes.Length; i++)
{
sb.Append(SerializeType(member.parameterTypes[i]));
if (i < member.parameterTypes.Length - 1)
{
sb.Append(memberDataSeparator);
}
}
}
return sb.ToString();
}
public static Member DeserializeMember(string memberData)
{
try
{
Ensure.That(nameof(memberData)).IsNotNullOrEmpty(memberData);
var parts = memberData.Split(memberDataSeparator);
if (parts.Length < 2)
{
throw new SerializationException("Malformed member data string.");
}
var targetType = DeserializeType(parts[0]);
var name = parts[1];
Type[] parameterTypes;
if (parts.Length == 2)
{
parameterTypes = null;
}
else if (parts.Length == 3 && string.IsNullOrEmpty(parts[2]))
{
parameterTypes = Empty<Type>.array;
}
else
{
parameterTypes = new Type[parts.Length - 2];
for (int i = 2; i < parts.Length; i++)
{
parameterTypes[i - 2] = DeserializeType(parts[i]);
}
}
return new Member(targetType, name, parameterTypes);
}
catch (Exception ex)
{
throw new SerializationException($"Unable to find member: '{memberData}'.", ex);
}
}
#endregion
public static event Action settingsChanged;
// NETUP: IReadOnlyCollection
public static ReadOnlyCollection<Assembly> assemblies { get; private set; }
// not used
public static ReadOnlyCollection<Assembly> runtimeAssemblies { get; private set; }
// not used
public static ReadOnlyCollection<Assembly> editorAssemblies { get; private set; }
public static ReadOnlyCollection<Assembly> ludiqAssemblies { get; private set; }
public static ReadOnlyCollection<Assembly> ludiqRuntimeAssemblies { get; private set; }
public static ReadOnlyCollection<Assembly> ludiqEditorAssemblies { get; private set; }
public static ReadOnlyCollection<Assembly> settingsAssemblies { get; private set; }
public static ReadOnlyCollection<Type> types { get; private set; }
public static ReadOnlyCollection<Type> runtimeTypes { get; private set; }
public static ReadOnlyCollection<Type> editorTypes { get; private set; }
// not used
public static ReadOnlyCollection<Type> ludiqTypes { get; private set; }
public static ReadOnlyCollection<Type> ludiqRuntimeTypes { get; private set; }
public static ReadOnlyCollection<Type> ludiqEditorTypes { get; private set; }
public static ReadOnlyCollection<Type> settingsAssembliesTypes { get; private set; }
public static ReadOnlyCollection<Type> settingsTypes { get; private set; }
public static ReadOnlyCollection<Type> GetTypeSet(TypeSet typeSet)
{
switch (typeSet)
{
case TypeSet.AllTypes:
return types;
case TypeSet.RuntimeTypes:
return runtimeTypes;
case TypeSet.SettingsTypes:
return settingsTypes;
case TypeSet.SettingsAssembliesTypes:
return settingsAssembliesTypes;
default:
throw new UnexpectedEnumValueException<TypeSet>(typeSet);
}
}
public static ReadOnlyCollection<Type> GetTypeSetFromAttribute(IAttributeProvider attributeProvider, TypeSet fallback = TypeSet.SettingsTypes)
{
var typeset = GetTypeSet(attributeProvider.GetAttribute<TypeSetAttribute>()?.typeSet ?? fallback);
return typeset;
}
private static bool IsUnityEditorAssembly(Assembly assembly)
{
// Cache because .GetName() is expensive
if (!_assemblyNameCache.TryGetValue(assembly, out var name))
{
name = assembly.GetName().Name;
_assemblyNameCache.Add(assembly, name);
}
return IsUnityEditorAssembly(name);
}
private static bool IsUnityEditorAssembly(string name)
{
return
name == "Assembly-CSharp-Editor" ||
name == "Assembly-CSharp-Editor-firstpass" ||
name == "UnityEditor" ||
name == "UnityEditor.CoreModule";
}
private static bool IsSpecialCaseRuntimeAssembly(string assemblyName)
{
return assemblyName == "UnityEngine.UI" || // has a reference to UnityEditor.CoreModule
assemblyName == "Unity.TextMeshPro"; // has a reference to UnityEditor.TextCoreFontEngineModule
}
private static bool IsEditorAssembly(Assembly assembly, HashSet<string> visited)
{
// assembly.GetName() is surprisingly expensive, keep a cache
if (_editorAssemblyCache.TryGetValue(assembly, out var isEditor))
{
return isEditor;
}
var name = assembly.GetName().Name;
if (visited.Contains(name))
{
return false;
}
visited.Add(name);
if (IsSpecialCaseRuntimeAssembly(name))
{
_editorAssemblyCache.Add(assembly, false);
return false;
}
if (Attribute.IsDefined(assembly, typeof(AssemblyIsEditorAssembly)))
{
_editorAssemblyCache.Add(assembly, true);
return true;
}
if (IsUserAssembly(name))
{
_editorAssemblyCache.Add(assembly, false);
return false;
}
if (IsUnityEditorAssembly(name))
{
_editorAssemblyCache.Add(assembly, true);
return true;
}
AssemblyName[] listOfAssemblyNames = assembly.GetReferencedAssemblies();
foreach (var dependencyName in listOfAssemblyNames)
{
try
{
Assembly dependency = Assembly.Load(dependencyName);
if (IsEditorAssembly(dependency, visited))
{
_editorAssemblyCache.Add(assembly, true);
return true;
}
}
catch (Exception e)
{
var loadedFromDirectory =
TryLoadAssemblyReferenceFromAssemblyDirectory(dependencyName, assembly, out var dependency);
if (loadedFromDirectory)
{
if (IsEditorAssembly(dependency, visited))
{
_editorAssemblyCache.Add(assembly, true);
return true;
}
}
else
{
Debug.LogWarning($"(Visual Scripting) Unable to load {assembly.GetName().Name}'s referenced assembly {dependencyName.Name} while scanning for Editor assemblies, skipping over this referenced assembly. Exception Information :\n{e.GetType().Name}\n{e.Message}");
}
}
}
_editorAssemblyCache.Add(assembly, false);
return false;
}
/// <summary>
/// This is used to load an assembly reference (dependency) that is located in the same folder as the parent assembly
/// but not loaded by the Editor's AppDomain. The out param dependency can be null if this fails.
/// </summary>
/// <remarks>
/// This is primarily used for Android builds, see Bug UVSB_2594 and UUM-98843.
/// Ex: Unity.Android.Gradle.dll references NiceIO. NiceIO is not a "scripting" assembly that is explicitly
/// loaded by Unity and is instead loaded dynamically at runtime when some code is jitted that requires the
/// assembly to be loaded. This means the assembly is not in the search path, but is in the same directory as
/// Unity.Android.Gradle.dll. Trying to call Assembly.Load("NiceIO") would fail, but this method would succeed.
/// </remarks>
private static bool TryLoadAssemblyReferenceFromAssemblyDirectory(AssemblyName referenceName, Assembly assembly, out Assembly dependency)
{
dependency = null;
#if UNITY_6000_5_OR_NEWER
var assemblyLocation = assembly.GetLoadedAssemblyPath();
#else
var assemblyLocation = assembly.Location;
#endif
var directory = System.IO.Path.GetDirectoryName(assemblyLocation);
if (directory == null)
return false;
var dependencyPath = System.IO.Path.Combine(directory, referenceName.Name + ".dll");
if (!System.IO.File.Exists(dependencyPath))
return false;
try
{
#if UNITY_6000_5_OR_NEWER
dependency = CurrentAssemblies.LoadFromPath(dependencyPath);
#else
dependency = Assembly.LoadFrom(dependencyPath);
#endif
return true;
}
catch (Exception)
{
return false;
}
}
private static bool IsUserAssembly(string name)
{
return
name == "Assembly-CSharp" ||
name == "Assembly-CSharp-firstpass";
}
private static bool IsRuntimeAssembly(Assembly assembly)
{
// User assemblies refer to the editor when they include
// a using UnityEditor / #if UNITY_EDITOR, but they should still
// be considered runtime.
return !IsEditorAssembly(assembly, new HashSet<string>());
}
private static bool IsLudiqRuntimeDependentAssembly(Assembly assembly)
{
if (assembly.GetName().Name == "Unity.VisualScripting.Core")
{
return true;
}
foreach (var dependency in assembly.GetReferencedAssemblies())
{
if (dependency.Name == "Unity.VisualScripting.Core" || dependency.Name == "Unity.VisualScripting.Flow" || dependency.Name == "Unity.VisualScripting.State")
{
return true;
}
}
return false;
}
private static bool IsLudiqEditorDependentAssembly(Assembly assembly)
{
if (assembly.GetName().Name == "Unity.VisualScripting.Core.Editor")
{
return true;
}
foreach (var dependency in assembly.GetReferencedAssemblies())
{
if (dependency.Name == "Unity.VisualScripting.Core.Editor")
{
return true;
}
}
return false;
}
// Need to keep this to avoid breaking the public API
public static bool IsEditorType(Type type)
{
var rootNamespace = type.RootNamespace();
return IsEditorAssembly(type.Assembly, new HashSet<string>()) ||
rootNamespace == "UnityEditor" ||
rootNamespace == "UnityEditorInternal";
}
// Need to keep this to avoid breaking the public API
public static bool IsRuntimeType(Type type)
{
return !IsEditorType(type) && !IsInternalType(type);
}
internal static bool IsUnityEditorType(Type type)
{
var rootNamespace = type.RootNamespace();
return rootNamespace == "UnityEditor" || rootNamespace == "UnityEditorInternal" ||
Attribute.IsDefined(type.Assembly, typeof(AssemblyIsEditorAssembly)) ||
IsUnityEditorAssembly(type.Assembly);
}
public static bool IsInternalType(Type type)
{
var rootNamespace = type.RootNamespace();
return rootNamespace == "UnityEngineInternal" ||
rootNamespace == "UnityEditorInternal";
}
private static string RootNamespace(this Type type)
{
return type.Namespace?.PartBefore('.');
}
public static void UpdateSettings()
{
using (ProfilingUtility.SampleBlock("Codebase settings update"))
{
var typeOptionsHashSet = new HashSet<Type>(BoltCore.Configuration.typeOptions);
var assemblyOptionsHashSet = new HashSet<LooseAssemblyName>(BoltCore.Configuration.assemblyOptions);
_settingsAssemblies = new List<Assembly>();
_settingsAssembliesTypes = new List<Type>();
_settingsTypes = new List<Type>();
foreach (var assembly in _assemblies)
{
var couldHaveIncludeInSettingsAttribute = ludiqAssemblies.Contains(assembly);
// It's important not to provide types outside the settings assemblies,
// because only those assemblies will be added to the linker to preserve stripping.
if (IncludeInSettings(assembly, assemblyOptionsHashSet))
{
_settingsAssemblies.Add(assembly);
foreach (var type in assembly.GetTypesSafely())
{
// Apparently void can be returned somehow:
// http://support.ludiq.io/topics/483
if (type == typeof(void))
{
continue;
}
_settingsAssembliesTypes.Add(type);
// For optimization, we bypass [IncludeInSettings] for assemblies
// that could logically never have it.
if (IncludeInSettings(type, couldHaveIncludeInSettingsAttribute, typeOptionsHashSet))
{
_settingsTypes.Add(type);
}
}
}
}
settingsAssemblies = _settingsAssemblies.AsReadOnly();
settingsAssembliesTypes = _settingsAssembliesTypes.AsReadOnly();
settingsTypes = _settingsTypes.AsReadOnly();
settingsChanged?.Invoke();
}
}
private static bool IncludeInSettings(Assembly a, HashSet<LooseAssemblyName> assemblyOptionsHashSet)
{
var includeInSettings = assemblyOptionsHashSet.Contains(a.GetName().Name);
return includeInSettings;
}
private static bool IncludeInSettings(Type t, bool couldHaveAttribute, HashSet<Type> typeOptionsHashSet)
{
// User-defined settings types
if (typeOptionsHashSet.Contains(t))
{
return true;
}
// Include non-runtime, non-internal enum and class deriving from UnityObject
// check the attribute last, attempt to early-out
if ((t.IsEnum || typeof(UnityObject).IsAssignableFrom(t)) && (GetAttributeInclude() ?? true))
{
return !IsUnityEditorType(t) && !IsInternalType(t);
}
return GetAttributeInclude() ?? false;
bool? GetAttributeInclude()
{
// Attribute.IsDefined is way faster than GetAttribute
return couldHaveAttribute && Attribute.IsDefined(t, typeof(IncludeInSettingsAttribute))
? (bool?)t.GetAttribute<IncludeInSettingsAttribute>().include
: null;
}
}
public static CodebaseSubset Subset(IEnumerable<Type> types, MemberFilter memberFilter, TypeFilter memberTypeFilter = null)
{
return CodebaseSubset.Get(types, memberFilter, memberTypeFilter);
}
public static CodebaseSubset Subset(IEnumerable<Type> typeSet, TypeFilter typeFilter, MemberFilter memberFilter, TypeFilter memberTypeFilter = null)
{
return CodebaseSubset.Get(typeSet, typeFilter, memberFilter, memberTypeFilter);
}
// For Testing
internal static void ClearCache()
{
_editorAssemblyCache.Clear();
_assemblyNameCache.Clear();
}
}
}