diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs index f50dcda..e98d010 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs @@ -14,8 +14,13 @@ public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type { internal Delegate? GetChanged(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) { - if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + return null; + } var valueType = option.GetValueType(); @@ -28,8 +33,13 @@ public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type internal Func? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType) { - if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + return null; + } if (!ValidateListSignature(method, typeof(void), out var hasIndex, valueType)) return null; diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs index 6a31bf8..90e7009 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs @@ -14,8 +14,13 @@ public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type? { internal Delegate? GetGetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) { - if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + return null; + } var valueType = option.GetValueType(); @@ -28,8 +33,13 @@ public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type? internal Func? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) { - if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + return null; + } if (!ValidateListSignature(method, valueType, out var hasIndex, valueType)) return null; diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs index 2e1826c..f98fc0e 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Emit; +using MiraAPI.Utilities; namespace MiraAPI.GameOptions.Attributes; @@ -127,7 +128,7 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold } /// - /// Resolves the target (property or method) and its associated singleton instance getter, if applicable. + /// Resolves the target (property, field or method) and its associated singleton instance getter, if applicable. /// /// The option group instance the property belongs to. /// The option property decorated with this attribute. @@ -136,16 +137,19 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold /// When this method returns, contains the getter /// if the resolved member is an instance member on an ; otherwise, . /// + /// When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too. /// The resolved if found; otherwise, . [SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")] - protected MemberInfo? ResolveMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter) + protected MemberInfo? ResolveMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound) { var groupType = group.GetType(); var flags = BindingFlags.Static | BindingFlags.Public; - type = holderType ?? groupType; MemberName ??= property.Name + defaultSuffix; + type = holderType ?? groupType; + instanceGetter = null; + if (type.IsAssignableTo(typeof(AbstractOptionGroup))) { flags |= BindingFlags.Instance; @@ -158,12 +162,69 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold .GetProperty(nameof(OptionGroupSingleton<>.Instance))! .GetGetMethod(); } - else + else if (!type.IsStatic()) { - instanceGetter = null; + flags |= BindingFlags.Instance; + + var canSearchPrivates = type.GetCustomAttribute() != null; + + if (canSearchPrivates) + flags |= BindingFlags.NonPublic; + + var getterFlags = BindingFlags.Static | BindingFlags.Public | (canSearchPrivates ? BindingFlags.NonPublic : BindingFlags.Default); + + foreach (var method in type.GetMethods(getterFlags)) + { + if (method.GetParameters().Length > 0 || (method.GetCustomAttribute() == null && method.Name != "GetInstance")) + continue; + + instanceGetter = method; + break; + } + + if (instanceGetter == null) + { + foreach (var prop in type.GetProperties(getterFlags)) + { + if (prop.GetCustomAttribute() == null && prop.Name != "Instance") + continue; + + var getMethod = prop.GetGetMethod(canSearchPrivates); + + if (getMethod == null) + continue; + + instanceGetter = getMethod; + break; + } + } + + if (instanceGetter == null) + { + Error($"No valid static instance getter (method GetInstance or property Instance) found in instanced type {type.Name}. Try renaming the method/property, or decorate it with InstanceGetterAttribute."); + notFound = false; + return null; + } + + if (!instanceGetter.ReturnType.IsAssignableTo(type)) + { + Error($"Found instance getter {instanceGetter.Name}, but the return type did not match what was expected ({type.Name})."); + notFound = false; + return null; + } + } + else if (type.GetCustomAttribute() != null) + { + flags |= BindingFlags.NonPublic; } var member = type.GetMember(MemberName, flags).FirstOrDefault(); + notFound = true; + + if (member == null) + return null; + + notFound = false; if (instanceGetter == null) return member; @@ -196,10 +257,11 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold /// When this method returns, contains the getter /// if the resolved method is an instance method on an ; otherwise, . /// + /// When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too. /// The resolved if found; otherwise, . - protected MethodInfo? ResolveMethod(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter) + protected MethodInfo? ResolveMethod(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound) { - return ResolveMember(group, property, out type, out instanceGetter) as MethodInfo; + return ResolveMember(group, property, out type, out instanceGetter, out notFound) as MethodInfo; } /// diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs index 34cb2c1..9e085ba 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs @@ -14,8 +14,13 @@ public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type? { internal Delegate? GetSetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) { - if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + return null; + } var valueType = option.GetValueType(); @@ -28,8 +33,13 @@ public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type? internal Func? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) { - if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) + if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method) + { + if (notFound) + Error($"Method {MemberName} does not exist in {type.FullName}"); + return null; + } if (!ValidateListSignature(method, valueType, out var hasIndex, valueType, valueType)) return null; diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs index 6929cbf..232ffee 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs @@ -20,9 +20,11 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property) { - if (ResolveMember(group, property, out Type type, out var instanceGetter) is not { } member) + if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member) { - Error($"Member {MemberName} does not exist in {type.FullName}"); + if (notFound) + Error($"Member {MemberName} does not exist in {type.FullName}"); + return null; } @@ -107,9 +109,11 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) { - if (ResolveMember(group, property, out Type type, out var instanceGetter) is not { } member) + if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member) { - Error($"Member {MemberName} does not exist in {type.FullName}"); + if (notFound) + Error($"Member {MemberName} does not exist in {type.FullName}"); + return null; } diff --git a/MiraAPI/GameOptions/Attributes/Delegate/SearchAttributes.cs b/MiraAPI/GameOptions/Attributes/Delegate/SearchAttributes.cs new file mode 100644 index 0000000..c6d4261 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/Delegate/SearchAttributes.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// An attribute used to mark a method or property as one that returns an instance of the class it's declared in. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public sealed class InstanceGetterAttribute : Attribute; + +/// +/// An attribute used to mark a class as one whose reflection search should include private members. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class SearchPrivateAttribute : Attribute;