diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 550864a4..6e1f8345 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -305,7 +305,7 @@ void LogError(string str) ("DesignateThing", new[]{ typeof(Thing) }), }; - foreach (Type t in typeof(Designator).AllSubtypesAndSelf() + foreach (Type t in typeof(Designator).AllSubtypesAndSelf().TryMakeGenericTypes() .Except(typeof(Designator_MechControlGroup))) // Opens float menu, sync that instead { foreach ((string m, Type[] args) in designatorMethods) @@ -381,7 +381,7 @@ void LogError(string str) ("Kill", new[]{ typeof(DamageInfo?), typeof(Hediff) }) }; - foreach (Type t in typeof(Thing).AllSubtypesAndSelf()) + foreach (Type t in typeof(Thing).AllSubtypesAndSelf().TryMakeGenericTypes()) { // SpawnSetup is patched separately because it sets the map var spawnSetupMethod = t.GetMethod("SpawnSetup", BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); @@ -415,7 +415,7 @@ void LogError(string str) ("Tick", Type.EmptyTypes) }; - foreach (Type t in typeof(WorldObject).AllSubtypesAndSelf()) + foreach (Type t in typeof(WorldObject).AllSubtypesAndSelf().TryMakeGenericTypes()) { foreach ((string m, Type[] args) in thingMethods) { @@ -454,7 +454,7 @@ void LogError(string str) foreach (string m in windowMethods) harmony.PatchMeasure(typeof(MainTabWindow_Inspect).GetMethod(m), setMapTimePrefix, setMapTimePostfix); - foreach (var t in typeof(InspectTabBase).AllSubtypesAndSelf()) + foreach (var t in typeof(InspectTabBase).AllSubtypesAndSelf().TryMakeGenericTypes()) { var method = t.GetMethod("FillTab", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, Type.EmptyTypes, null); if (method != null && !method.IsAbstract) diff --git a/Source/Client/Syncing/Game/SyncMethods.cs b/Source/Client/Syncing/Game/SyncMethods.cs index c92d3224..e2936997 100644 --- a/Source/Client/Syncing/Game/SyncMethods.cs +++ b/Source/Client/Syncing/Game/SyncMethods.cs @@ -61,7 +61,7 @@ public static void Init() SyncMethod.Register(typeof(Building_Bed), nameof(Building_Bed.Medical)); { - var types = typeof(CompAssignableToPawn).AllSubtypesAndSelf().ToArray(); + var types = typeof(CompAssignableToPawn).AllSubtypesAndSelf().TryMakeGenericTypes().ToArray(); var assignMethods = types .Select(t => t.GetMethod(nameof(CompAssignableToPawn.TryAssignPawn), AccessTools.allDeclared, null, new[] { typeof(Pawn) }, null)) .AllNotNull(); diff --git a/Source/Client/Syncing/Handler/SyncAction.cs b/Source/Client/Syncing/Handler/SyncAction.cs index 21ad38bf..ff4354d2 100644 --- a/Source/Client/Syncing/Handler/SyncAction.cs +++ b/Source/Client/Syncing/Handler/SyncAction.cs @@ -93,7 +93,7 @@ public override void Handle(ByteReader data) public void PatchAll(string methodName) { - foreach (var type in typeof(A).AllSubtypesAndSelf()) + foreach (var type in typeof(A).AllSubtypesAndSelf().TryMakeGenericTypes()) { if (type.IsAbstract) continue; diff --git a/Source/Client/Util/TypeUtil.cs b/Source/Client/Util/TypeUtil.cs index 2d2f6b92..9907747d 100644 --- a/Source/Client/Util/TypeUtil.cs +++ b/Source/Client/Util/TypeUtil.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Verse; @@ -20,5 +21,124 @@ public static Type[] AllSubclassesNonAbstractOrdered(Type type) { .OrderBy(t => t.Name) .ToArray(); } + + /// + /// Attempts to construct generic types (by calling ) on the + /// provided types (as long as they're generic and not already constructed). It'll attempt to find + /// a type which satisfies all generic type constraints, if a type matching them exists. All + /// unsuccessfully created generic types will be omitted from the result. + /// + /// The list of types which should be made generic (if needed and possible). + /// Determines if errors caused by no types found matching constraints should be shown. + /// + /// where each provided type is: + /// + /// the same as it was if it's not generic type or was already constructed + /// omitted if the method fails (likely due to no type matching the constraints) + /// a successful result from calling (with all constraints matched) + /// + /// + public static IEnumerable TryMakeGenericTypes(this IEnumerable types, bool ignoreErrors = false) + => types.Select(t => t.TryMakeGenericType(ignoreErrors)).Where(t => t != null); + + /// + /// Attempts to construct a generic type (by calling ) on the + /// provided type (as long as it's generic and not already constructed). It'll attempt to find + /// a type which satisfies all generic type constraints, if a type matching them all exists. + /// + /// The type which should be made generic (if needed and possible). + /// Determines if errors caused by no types found matching constraints should be shown. + /// + /// + /// provided argument itself if it's not generic type or was already constructed + /// if the method fails (likely due to no type matching the constraints) + /// successful result from calling (with all constraints matched) + /// + /// + public static Type TryMakeGenericType(this Type type, bool ignoreErrors = false) + { + // Non-generic type or already constructed generic types + // don't need to be constructed, return as-is. + if (!type.IsGenericType || type.IsConstructedGenericType) + return type; + + var genericArgs = type.GetGenericArguments(); + if (genericArgs.TryGetMatchingConstraints(out var targetArgs, out var errorMessage)) + return type.MakeGenericType(targetArgs); + + // Only log errors if the type is non-abstract or has no subclasses, + // assuming abstract classes with no subclasses are unused. + if (!ignoreErrors && (!type.IsAbstract || type.AllSubclassesNonAbstract().Any())) + Log.Error($"Failed making generic type for type {type} with message: {errorMessage}"); + + return null; + } + + /// + /// Attempts to find types matching restraints of generic arguments passed to this method. + /// + /// Array of generic arguments for which the constraints should be matched and returned as . + /// Array of types matching the provided generic argument constraints, or empty array if method failed. + /// Explains why the method failed, or an empty string if successful. + /// if types matching constraints were found, otherwise . + private static bool TryGetMatchingConstraints(this IReadOnlyList genericArgs, out Type[] args, out string error) + { + if (genericArgs.EnumerableNullOrEmpty()) + { + args = Type.EmptyTypes; + error = "Trying to find constraints for generic arguments failed - the list of arguments was null or empty."; + return false; + } + + args = new Type[genericArgs.Count]; + + for (var genericIndex = 0; genericIndex < genericArgs.Count; genericIndex++) + { + var constraints = genericArgs[genericIndex].GetGenericParameterConstraints(); + if (constraints.NullOrEmpty()) + { + // No constraints, just use object as argument and skip to next generic type + args[genericIndex] = typeof(object); + continue; + } + + if (constraints.Length == 1) + { + // Only one constraint, just use it and skip to next generic type + args[genericIndex] = constraints[0]; + continue; + } + + // Start off with all subtypes/implementations (including self) of our first constraint + IEnumerable possibleMatches = constraints[0] + .IsInterface + ? constraints[0].AllImplementing().Concat(constraints[0]) + : constraints[0].AllSubtypesAndSelf(); + + // Go through each constraint (besides the first) + // and limit the possible matches based on it. + for (int constraintIndex = 1; constraintIndex < constraints.Length; constraintIndex++) + { + var current = constraints[constraintIndex]; + possibleMatches = possibleMatches.Where(t => current.IsAssignableFrom(t)); + } + + // As long as we have any result, grab and use it. + // We don't really care which one we use here. + var result = possibleMatches.FirstOrDefault(); + if (result == null) + { + error = $"Could not find type matching specific constraint: {constraints.ToStringSafeEnumerable()}"; + args = Type.EmptyTypes; + return false; + } + + // Set the result for specific argument + args[genericIndex] = result; + } + + error = string.Empty; + return true; + } } }