SmartlyDressedGames / Unturned-3.x-Community

Community portion of the Unturned-3.x repo. If you have access to the source code you can find it here:
https://github.com/SmartlyDressedGames/Unturned-3.x/
85 stars 18 forks source link

Annoying functionality change to Type.IsAssignableFrom in unity 2021 #4171

Closed DanielWillett closed 10 months ago

DanielWillett commented 11 months ago

Since the new Unity update, dealing with 'optional' features in modules is so much more difficult now because of the IsAssignableFrom call in Module.initialize, which appears to now try to load the type completely (which it didn't do in Unity 2019).

This also applies to any other non-nexus assemblies you have listed (although this is less of a problem with the auto-assembly searching now), all classes in each library is loaded which means almost any reference those libraries make also has to be present, even if you're not using the feature.

Could not load file or assembly 'UnturnedUITools, Version=1.2.0.0, Culture=neutral, PublicKeyToken=5e66f8e265922cfe' or one of its dependencies.
at SDG.Framework.Modules.Module.initialize () [0x00066] in <593e8d5b45504fb88b02563e7dffa4a8>:0

TypeLoadException: Could not load type of field 'DevkitServer.Compat.UIExtensionManagerCompat+<>c:<>9__11_0' (1) due to: Could not load file or assembly 'UnturnedUITools, Version=1.2.0.0, Culture=neutral, PublicKeyToken=5e66f8e265922cfe' or one of its dependencies.
[2023-10-28 08:42:40] System.RuntimeTypeHandle.CanCastTo (System.RuntimeType type, System.RuntimeType target) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.RuntimeType.IsAssignableFrom (System.Type c) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
SDG.Framework.Modules.Module.initialize () (at <593e8d5b45504fb88b02563e7dffa4a8>:0)
SDG.Framework.Modules.Module.set_isEnabled (System.Boolean value) (at <593e8d5b45504fb88b02563e7dffa4a8>:0)
SDG.Framework.Modules.ModuleHook.initializeModules () (at <593e8d5b45504fb88b02563e7dffa4a8>:0)
SDG.Framework.Modules.ModuleHook.start () (at <593e8d5b45504fb88b02563e7dffa4a8>:0)
SDG.Unturned.Setup.Awake () (at <593e8d5b45504fb88b02563e7dffa4a8>:0)

While, yes I know what this error means and I should just add the library, there's no reason that library should have to be there for the module to load, just that type will not be able to load (which in this case is fine).

All I propose is making your own method for checking types:

public static bool NelsonsIsAssignableFrom(Type thisType, Type type)
{
  try
  {
     return thisType.IsAssignableFrom(type);
  }
  catch
  {
     return false;
  }
}

and replacing it in here image

Alternatively maybe we could get some kind of ignore attribute for types but I think the other option is more fullproof.

sunnamed434 commented 11 months ago

Maybe mark type with [Browsable(false)] (should be displayed in a Properties window like the one used in Visual Studio's designer environment, used in Windows Forms), to not add any extra API and make it a global standard in Unturned, for example, I see it useful in RocketMod and OpenMod also

sunnamed434 commented 11 months ago

Or instead if its not a problem to add a new API then:

/// <summary>
/// Specifies that a member with this attribute should be ignored when performing reflection activities.
/// </summary>
/// <remarks>
/// This attribute can be applied to members in order to exclude them from reflection-related operations, such as searching types in an assembly or invoking methods at runtime.
/// Refer to the example below for usage:
///
/// ```csharp
/// [ReflectionIgnore]
/// public class IgnoredClass
/// { }
///
/// // In another part of your code
/// var types = assembly.GetTypes().Where(type => !type.GetCustomAttributes(typeof(ReflectionIgnoreAttribute), false).Any());
/// ```
/// </remarks>
[AttributeUsage(AttributeTargets.All)]
public sealed class ReflectionIgnoreAttribute : Attribute
{
}
DanielWillett commented 11 months ago

Attribute.IsDefined(type, typeof(ReflectionIgnoreAttribute)) also works, but yeah something like this would also be very nice. Imo a new attribute is a bit cleaner than using Browsable.

DanielWillett commented 11 months ago

Update: it seems GetCustomAttribute and IsDefined have the same problem.

Seems like this is because all attributes are gotten when you call GetCustomAttribute (not just the type you asked for), and if one is in a library that isn't referenced it will throw an exception (this appears to be the same in .NET Framework and mono).

These could be completely unnecessary attributes like XML, JSON, Entity Framework attributes, that you may not need to use the class.

https://github.com/Unity-Technologies/mono/blob/0de6cfb99250bed564aa35914151bcd91d7ba0e3/mcs/class/referencesource/mscorlib/system/attribute.cs#L644

GetCustomAttribute:

FileNotFoundException: Could not load file or assembly 'System.Runtime.Serialization, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' or one of its dependencies.
System.MonoCustomAttrs.GetCustomAttributesBase (System.Reflection.ICustomAttributeProvider obj, System.Type attributeType, System.Boolean inheritedOnly) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.MonoCustomAttrs.GetCustomAttributes (System.Reflection.ICustomAttributeProvider obj, System.Type attributeType, System.Boolean inherit) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.RuntimeType.GetCustomAttributes (System.Type attributeType, System.Boolean inherit) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.Attribute.GetCustomAttributes (System.Reflection.MemberInfo element, System.Type type, System.Boolean inherit) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.Attribute.GetCustomAttribute (System.Reflection.MemberInfo element, System.Type attributeType, System.Boolean inherit) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.Attribute.GetCustomAttribute (System.Reflection.MemberInfo element, System.Type attributeType) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
DevkitServer.API.Accessor.GetPriority (System.Type type) (at <c80404aebfcb4c838ecb4ee88a3e66d2>:0)
DevkitServer.API.Accessor.SortTypesByPriorityHandler (System.Type a, System.Type b) (at <c80404aebfcb4c838ecb4ee88a3e66d2>:0)
(shortened a bit)

IsDefined:

FileNotFoundException: Could not load file or assembly 'System.Runtime.Serialization, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' or one of its dependencies.
System.MonoCustomAttrs.IsDefined (System.Reflection.ICustomAttributeProvider obj, System.Type attributeType, System.Boolean inherit) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.RuntimeType.IsDefined (System.Type attributeType, System.Boolean inherit) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.Attribute.IsDefined (System.Reflection.MemberInfo element, System.Type attributeType, System.Boolean inherit) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
System.Attribute.IsDefined (System.Reflection.MemberInfo element, System.Type attributeType) (at <47fc8c70fa834cbf8141d7c1a7589125>:0)
DevkitServer.API.Accessor.GetPriority (System.Type type) (at <248d0303599945b985516323f1e237e3>:0)
DevkitServer.API.Accessor.SortTypesByPriorityHandler (System.Type a, System.Type b) (at <248d0303599945b985516323f1e237e3>:0)

A Solution:

public static bool IsIgnored(this Type type)
{
  try
  {
    return Attribute.IsDefined(type, typeof(ReflectionIgnoreAttribute));
  }
  catch (TypeLoadException)
  {
    return false;
  }
  catch (FileNotFoundException)
  {
    return false;
  }
}

public static List<Type> GetTypesSafe(IEnumerable<Assembly> assemblies, bool removeIgnored = true)
{
  List<Type?> types = new List<Type?>();
  foreach (Assembly assembly in assmeblies)
  {
    try
    {
      types.AddRange(assembly.GetTypes());
    }
    catch (FileNotFoundException ex)
    {
      CommandWindow.Log($"Unable to get any types from assembly {assembly.FullName}. Missing dependency: {ex.FileName}.");
    }
    catch (ReflectionTypeLoadException ex)
    {
      types.AddRange(ex.Types);
    }
  }

  types.RemoveAll(x => x == null || removeIgnored && x.IsIgnored());

  return types!;
}

public static bool IsAssignableFromSafe(this Type type, Type subType)
{
  try
  {
    return type.IsAssignableFrom(subType);
  }
  catch (TypeLoadException)
  {
    return false;
  }
  catch (FileNotFoundException)
  {
    return false;
  }
}
SDGNelson commented 11 months ago

Sounds like the easiest approach is the IsAssignableFrom adjustment. Do you think there's anywhere else the game should catch IsAssignableFrom exceptions?

DanielWillett commented 11 months ago

Thank you

Do you think there's anywhere else the game should catch IsAssignableFrom exceptions?

I don't believe so.