#nullable enable using Barotrauma.Debugging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; namespace Barotrauma { public static class ReflectionUtils { private static readonly ConcurrentDictionary> CachedNonAbstractTypes = new(); private static readonly ConcurrentDictionary> TypeSearchCache = new(); public static T GetValueFromStaticProperty(this PropertyInfo property) { if (property.GetMethod is not { IsStatic: true }) { throw new ArgumentException($"Property {property} is not static"); } var value = property.GetValue(obj: null); if (value is not T castValue) { throw new ArgumentException($"Property {property} is null or not of type {typeof(T)}"); } return castValue; } public static IEnumerable GetDerivedNonAbstract() { Type t = typeof(T); string typeName = t.FullName ?? t.Name; // search quick lookup cache if (TypeSearchCache.TryGetValue(typeName, out var value)) { return value; } // doesn't exist so let's add it. Assembly assembly = typeof(T).Assembly; if (!CachedNonAbstractTypes.ContainsKey(assembly)) { AddNonAbstractAssemblyTypes(assembly); } // build cache from registered assemblies' types. var list = CachedNonAbstractTypes.Values .SelectMany(arr => arr.Where(type => type.IsSubclassOf(t))) .ToImmutableArray(); if (list.Length == 0) { return ImmutableArray.Empty; // No types, don't add to cache } if (!TypeSearchCache.TryAdd(typeName, list)) { DebugConsoleCore.Log($"ReflectionUtils.AddNonAbstractAssemblyTypes(): Error while adding to quick lookup cache."); } return list; } /// /// Adds an assembly's Non-Abstract Types to the cache for Barotrauma's Type lookup. /// /// Assembly to be added /// Whether or not to overwrite an entry if the assembly already exists within it. public static void AddNonAbstractAssemblyTypes(Assembly assembly, bool overwrite = false) { if (CachedNonAbstractTypes.ContainsKey(assembly)) { if (!overwrite) { return; } CachedNonAbstractTypes.Remove(assembly, out _); } try { if (!CachedNonAbstractTypes.TryAdd(assembly, assembly.GetTypes().Where(t => !t.IsAbstract).ToImmutableArray())) { } else { TypeSearchCache.Clear(); // Needs to be rebuilt to include potential new types } } catch (ReflectionTypeLoadException) { } } /// /// Removes an assembly from the cache for Barotrauma's Type lookup. /// /// Assembly to remove. public static void RemoveAssemblyFromCache(Assembly assembly) { CachedNonAbstractTypes.Remove(assembly, out _); TypeSearchCache.Clear(); } /// /// Clears all cached assembly data and rebuilds types list only to include base Barotrauma types. /// public static void ResetCache() { CachedNonAbstractTypes.Clear(); CachedNonAbstractTypes.TryAdd(typeof(ReflectionUtils).Assembly, typeof(ReflectionUtils).Assembly.GetTypes().Where(t => !t.IsAbstract).ToImmutableArray()); TypeSearchCache.Clear(); } public static Type? GetType(string nameWithNamespace) { if (Type.GetType(nameWithNamespace) is Type t) { return t; } var entryAssembly = Assembly.GetEntryAssembly(); if (entryAssembly?.GetType(nameWithNamespace) is Type t2) { return t2; } return null; } public static Option ParseDerived(TInput input) where TBase : notnull where TInput : notnull { static Option none() => Option.None(); var derivedTypes = GetDerivedNonAbstract(); Option parseOfType(Type t) { //every TBase type is expected to have a method with the following signature: // public static Option Parse(TInput str) var parseFunc = t.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static); if (parseFunc is null) { return none(); } var parameters = parseFunc.GetParameters(); if (parameters.Length != 1) { return none(); } var returnType = parseFunc.ReturnType; if (!returnType.IsConstructedGenericType) { return none(); } if (returnType.GetGenericTypeDefinition() != typeof(Option<>)) { return none(); } if (returnType.GenericTypeArguments[0] != t) { return none(); } //some hacky business to convert from Option to Option when we only know T2 at runtime static Option convert(Option option) where T2 : TBase => option.Select(v => (TBase)v); Func, Option> f = convert; var genericArgs = f.Method.GetGenericArguments(); genericArgs[^1] = t; var constructedConverter = f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs); return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) }) as Option? ?? none(); } return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()); } public static string NameWithGenerics(this Type t) { if (!t.IsGenericType) { return t.Name; } string result = t.Name[..t.Name.IndexOf('`')]; result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>"; return result; } /// /// Gets a type by its name, with backwards compatibility for types that have been renamed. /// /// public static Type? GetTypeWithBackwardsCompatibility(Assembly assembly, string nameSpace, string typeName, bool throwOnError, bool ignoreCase) { var types = assembly .GetTypes() .Where(t => NameMatches(t.Namespace, nameSpace, ignoreCase)); foreach (Type type in types) { if (NameMatches(type.Name, typeName, ignoreCase)) { return type; } if (type.GetCustomAttribute() is { } knownAsAttribute) { if (NameMatches(knownAsAttribute.PreviousName, typeName, ignoreCase)) { return type; } } } if (throwOnError) { throw new TypeLoadException($"Could not find the type {typeName} in namespace {nameSpace}"); } return null; static bool NameMatches(string? name1, string? name2, bool ignoreCase) => string.Equals(name1, name2, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } /// /// The names of generic types include the arity at the end (fancy way of saying the number of parameters, e.g. GUISelectionCarousel would be GUISelectionCarousel`1) /// This method strips that part out. /// public static Identifier GetTypeNameWithoutGenericArity(Type type) { string name = type.Name; int index = name.IndexOf('`'); return (index == -1 ? name : name.Substring(0, index)).ToIdentifier(); } } }