Skip to content

Commit

Permalink
Fix #413, added methods TryMakeGenericType(s)
Browse files Browse the repository at this point in the history
The methods `TryMakeGenericType`/`TryMakeGenericTypes` will attempt to construct a generic type for a given type/collection of types. In case of non-generic types, or generic types that were already constructed they'll be returned as-is.

The method will attempt to find types that matches all generic constraints defined by the type itself, and if successful - call `Type.MakeGenericType` with the results.

In case we encounter a generic type with constraints no types can match an error will be displayed (unless `ignoreErrors` argument will be passed and set to true). The only exception is abstract classes with no subclasses, for which no error will be shown (assuming here they are unused).

The method was applied where needed. With RimWorld assembly itself, and majority of mods - this is going to do nothing. However, in case of mods that utilize generic types for the types that we end up patching - it should allow for those mods to properly work with Multiplayer without significantly breaking everything - for example, it should fix startup issues with Project Rimfactory.

As for error handling - I'm not 100% sure here. Perhaps adding `Multiplayer.loadingErrors = true` if errors are encountered? I can make changes where needed.

And finally - I've attempted to add XML documentation, as well as some comments to the code to explain it a bit more. However, I'm not completely satisfied with it... If there's any changes that should be made, let me know.
  • Loading branch information
SokyranTheDragon committed Jan 6, 2024
1 parent 7429f3a commit 685b3e1
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 6 deletions.
8 changes: 4 additions & 4 deletions Source/Client/MultiplayerStatic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Source/Client/Syncing/Game/SyncMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion Source/Client/Syncing/Handler/SyncAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
120 changes: 120 additions & 0 deletions Source/Client/Util/TypeUtil.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Verse;

Expand All @@ -20,5 +21,124 @@ public static Type[] AllSubclassesNonAbstractOrdered(Type type) {
.OrderBy(t => t.Name)
.ToArray();
}

/// <summary>
/// Attempts to construct generic types (by calling <see cref="Type.MakeGenericType"/>) 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.
/// </summary>
/// <param name="types">The list of types which should be made generic (if needed and possible).</param>
/// <param name="ignoreErrors">Determines if errors caused by no types found matching constraints should be shown.</param>
/// <returns>
/// <see cref="IEnumerable{T}"/> where each provided type is:
/// <list type="bullet">
/// <item>the same as it was if it's not generic type or was already constructed</item>
/// <item>omitted if the method fails (likely due to no type matching the constraints)</item>
/// <item>a successful result from calling <see cref="Type.MakeGenericType"/> (with all constraints matched)</item>
/// </list>
/// </returns>
public static IEnumerable<Type> TryMakeGenericTypes(this IEnumerable<Type> types, bool ignoreErrors = false)
=> types.Select(t => t.TryMakeGenericType(ignoreErrors)).Where(t => t != null);

/// <summary>
/// Attempts to construct a generic type (by calling <see cref="Type.MakeGenericType"/>) 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.
/// </summary>
/// <param name="type">The type which should be made generic (if needed and possible).</param>
/// <param name="ignoreErrors">Determines if errors caused by no types found matching constraints should be shown.</param>
/// <returns>
/// <list type="bullet">
/// <item>provided argument <paramref name="type"/> itself if it's not generic type or was already constructed</item>
/// <item><see langword="null"/> if the method fails (likely due to no type matching the constraints)</item>
/// <item>successful result from calling <see cref="Type.MakeGenericType"/> (with all constraints matched)</item>
/// </list>
/// </returns>
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;
}

/// <summary>
/// Attempts to find types matching restraints of generic arguments passed to this method.
/// </summary>
/// <param name="genericArgs">Array of generic arguments for which the constraints should be matched and returned as <paramref name="args"/>.</param>
/// <param name="args">Array of types matching the provided generic argument constraints, or empty array if method failed.</param>
/// <param name="error">Explains why the method failed, or an empty string if successful.</param>
/// <returns><see langword="true" /> if types matching constraints were found, otherwise <see langword="false" />.</returns>
private static bool TryGetMatchingConstraints(this IReadOnlyList<Type> 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<Type> 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;
}
}
}

0 comments on commit 685b3e1

Please sign in to comment.