diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index b5745e5..9e2e1a9 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Utilities; @@ -43,7 +42,7 @@ public class ExampleOptions : AbstractOptionGroup public Api WorstApi { get; set; } = Api.MiraAPI; [ModdedEnumOptionList("Value State {0}", ["None", "Primary", "Secondary"])] - public List ValueStates { get; set; } = + public ModdedList ValueStates { get; set; } = [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None]; [ModdedPlayerOption("options.exampleOptions1.players")] diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 52d8fdf..589f705 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -6,6 +6,7 @@ using System.Linq; using Il2CppInterop.Runtime.Attributes; using MiraAPI.Modifiers; using MiraAPI.Roles; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -55,7 +56,7 @@ public abstract class AbstractOptionGroup /// /// Gets the function that determines whether the group should be visible or not. /// - public virtual Func GroupVisible => () => true; + public virtual Func GroupVisible => DelegateUtilities.AlwaysTrue; /// /// Gets a value indicating whether this group's options are shown in the Match Info Guide. diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs index 0d75a11..f50dcda 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionChangedAttribute.cs @@ -14,73 +14,25 @@ public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type { internal Delegate? GetChanged(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) { - if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) - { - Error($"Method {MemberName} does not exist in {type.FullName}"); + if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) return null; - } - - if (method.ReturnType != typeof(void)) - { - Error($"{MemberName}'s return type is not void."); - return null; - } - - var parameters = method.GetParameters(); - - if (parameters.Length != 1) - { - Error($"{MemberName} must have exactly one parameter."); - return null; - } var valueType = option.GetValueType(); - if (parameters[0].ParameterType != valueType) - { - Error($"{MemberName}'s parameter must be of the type {valueType.Name}."); + if (!ValidateSignature(method, typeof(void), valueType)) return null; - } var delegateType = typeof(Action<>).MakeGenericType(valueType); return CompileDynamicDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "Changed"); } - internal Func? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType) + internal Func? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType) { - if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) - { - Error($"Method {MemberName} does not exist in {type.FullName}"); + if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) return null; - } - if (method.ReturnType != typeof(void)) - { - Error($"{MemberName}'s return type is not void."); + if (!ValidateListSignature(method, typeof(void), out var hasIndex, valueType)) return null; - } - - var parameters = method.GetParameters(); - - if (parameters.Length is < 1 or > 2) - { - Error($"{MemberName} must have one parameter ({valueType.Name}) or two parameters ({valueType.Name}, int)."); - return null; - } - - if (parameters[0].ParameterType != valueType) - { - Error($"{MemberName}'s first parameter must be of the type {valueType.Name}."); - return null; - } - - var hasIndex = parameters.Length == 2; - - if (hasIndex && parameters[1].ParameterType != typeof(int)) - { - Error($"{MemberName}'s second parameter must be an int (the list index)."); - return null; - } var delegateType = typeof(Action<>).MakeGenericType(valueType); return CompileDynamicListDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "ListChanged", hasIndex); diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs index 3434951..6a31bf8 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionGetterAttribute.cs @@ -14,73 +14,25 @@ public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type? { internal Delegate? GetGetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) { - if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) - { - Error($"Method {MemberName} does not exist in {type.FullName}"); + if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) return null; - } var valueType = option.GetValueType(); - if (method.ReturnType != valueType) - { - Error($"{MemberName}'s return type is not {valueType.Name}."); + if (!ValidateSignature(method, valueType, valueType)) return null; - } - - var parameters = method.GetParameters(); - - if (parameters.Length != 1) - { - Error($"{MemberName} must have exactly one parameter."); - return null; - } - - if (parameters[0].ParameterType != valueType) - { - Error($"{MemberName}'s parameter must be of the type {valueType.Name}."); - return null; - } var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType); return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "Getter"); } - internal Func? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) + internal Func? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) { - if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) - { - Error($"Method {MemberName} does not exist in {type.FullName}"); + if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) return null; - } - if (method.ReturnType != valueType) - { - Error($"{MemberName}'s return type is not {valueType.Name}."); + if (!ValidateListSignature(method, valueType, out var hasIndex, valueType)) return null; - } - - var parameters = method.GetParameters(); - - if (parameters.Length is < 1 or > 2) - { - Error($"{MemberName} must have one parameter ({valueType.Name}) or two parameters ({valueType.Name}, int)."); - return null; - } - - if (parameters[0].ParameterType != valueType) - { - Error($"{MemberName}'s first parameter must be of the type {valueType.Name}."); - return null; - } - - var hasIndex = parameters.Length == 2; - - if (hasIndex && parameters[1].ParameterType != typeof(int)) - { - Error($"{MemberName}'s second parameter must be an int (the list index)."); - return null; - } var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType); return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "ListGetter", hasIndex); diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs index 43742ba..2e1826c 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionMemberAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -15,11 +16,116 @@ namespace MiraAPI.GameOptions.Attributes; [AttributeUsage(AttributeTargets.Property)] public abstract class ModdedOptionMemberAttribute(string? memberName, Type? holderType, string defaultSuffix) : Attribute { + private static readonly Dictionary DelegateCache = []; + + /// + /// A helper class to bind a specific list index to a dynamically compiled delegate. + /// + /// The index to bind. + public sealed class DelegateIndexClosure(int index) + { + /// + /// The bound index. + /// + [SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "No need to be private.")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] + public readonly int Index = index; + } + + /// + /// Exposes the index field so child attributes can emit instructions to read it. + /// + protected static readonly FieldInfo IndexClosureField = typeof(DelegateIndexClosure).GetField(nameof(DelegateIndexClosure.Index))!; + /// /// Gets the resolved name of the target member. /// protected string? MemberName { get; private set; } = memberName; + /// + /// Validates that a method matches the expected return type and parameter types. + /// + /// The method to validate. + /// The expected return type of the method. + /// The expected types of the parameters of the method. + /// true if the method is valid; false otherwise. + protected bool ValidateSignature(MethodInfo method, Type expectedReturnType, params Type[] expectedParamTypes) + { + if (method.ReturnType != expectedReturnType) + { + Error($"{MemberName}'s return type must be {expectedReturnType.Name}."); + return false; + } + + var parameters = method.GetParameters(); + + if (parameters.Length != expectedParamTypes.Length) + { + Error($"{MemberName} must have exactly {expectedParamTypes.Length} parameter(s)."); + return false; + } + + for (var i = 0; i < expectedParamTypes.Length; i++) + { + if (parameters[i].ParameterType == expectedParamTypes[i]) + continue; + + Error($"{MemberName}'s parameter at index {i} must be {expectedParamTypes[i].Name}."); + return false; + } + + return true; + } + + /// + /// Validates that a list method matches the expected return type, base parameter types, and an optional integer index. + /// + /// The method to validate. + /// The expected return type of the method. + /// When the method returns, a flag that indicates if the method has an index parameter. + /// The expected types of the parameters of the method. + /// true if the method is valid; false otherwise. + protected bool ValidateListSignature(MethodInfo method, Type expectedReturnType, out bool hasIndex, params Type[] expectedParamTypes) + { + hasIndex = false; + + if (method.ReturnType != expectedReturnType) + { + Error($"{MemberName}'s return type must be {expectedReturnType.Name}."); + return false; + } + + var parameters = method.GetParameters(); + + if (parameters.Length < expectedParamTypes.Length || parameters.Length > expectedParamTypes.Length + 1) + { + Error($"{MemberName} must have {expectedParamTypes.Length} parameter(s), optionally with an extra trailing int parameter."); + return false; + } + + for (var i = 0; i < expectedParamTypes.Length; i++) + { + if (parameters[i].ParameterType == expectedParamTypes[i]) + continue; + + Error($"{MemberName}'s parameter at index {i} must be {expectedParamTypes[i].Name}."); + return false; + } + + if (parameters.Length == expectedParamTypes.Length + 1) + { + if (parameters[^1].ParameterType != typeof(int)) + { + Error($"{MemberName}'s final parameter must be an int (the list index)."); + return false; + } + + hasIndex = true; + } + + return true; + } + /// /// Resolves the target (property or method) and its associated singleton instance getter, if applicable. /// @@ -123,6 +229,9 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold bool forwardParameters = true, Action? postProcessCall = null) { + if (postProcessCall == null && DelegateCache.TryGetValue(method, out var cachedDelegate)) + return cachedDelegate; + var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}", returnType, paramTypes, ownerType, true); var il = dm.GetILGenerator(); @@ -133,16 +242,19 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold { for (byte i = 0; i < paramTypes.Length; i++) { - if (i == 0) - il.Emit(OpCodes.Ldarg_0); - else if (i == 1) - il.Emit(OpCodes.Ldarg_1); - else if (i == 2) - il.Emit(OpCodes.Ldarg_2); - else if (i == 3) - il.Emit(OpCodes.Ldarg_3); + var loadOp = i switch + { + 0 => OpCodes.Ldarg_0, + 1 => OpCodes.Ldarg_1, + 2 => OpCodes.Ldarg_2, + 3 => OpCodes.Ldarg_3, + _ => OpCodes.Ldarg_S, + }; + + if (loadOp == OpCodes.Ldarg_S) + il.Emit(loadOp, i); else - il.Emit(OpCodes.Ldarg_S, i); + il.Emit(loadOp); } } @@ -153,7 +265,12 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold il.Emit(OpCodes.Ret); - return dm.CreateDelegate(delegateType); + var compiledDelegate = dm.CreateDelegate(delegateType); + + if (postProcessCall == null) + DelegateCache[method] = compiledDelegate; + + return compiledDelegate; } /// @@ -185,7 +302,7 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold string suffix, bool hasIndexParameter, Action? postProcessCall = null, - Action? postProcessListCall = null) + Action? postProcessListCall = null) { if (!hasIndexParameter && postProcessListCall is null) { @@ -203,40 +320,47 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold return _ => sharedDelegate; } - return index => + var fullParamTypes = new Type[baseParamTypes.Length + 1]; + fullParamTypes[0] = typeof(DelegateIndexClosure); + Array.Copy(baseParamTypes, 0, fullParamTypes, 1, baseParamTypes.Length); + + var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}_List", returnType, fullParamTypes, ownerType, true); + var il = dm.GetILGenerator(); + + if (!method.IsStatic && instanceGetter != null) + il.Emit(OpCodes.Call, instanceGetter); + + for (var i = 0; i < baseParamTypes.Length; i++) { - var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}_{index}", returnType, baseParamTypes, ownerType, true); - var il = dm.GetILGenerator(); - - if (!method.IsStatic && instanceGetter != null) - il.Emit(OpCodes.Call, instanceGetter); - - for (byte i = 0; i < baseParamTypes.Length; i++) + var argPos = (byte)(i + 1); + var loadOp = argPos switch { - if (i == 0) - il.Emit(OpCodes.Ldarg_0); - else if (i == 1) - il.Emit(OpCodes.Ldarg_1); - else if (i == 2) - il.Emit(OpCodes.Ldarg_2); - else if (i == 3) - il.Emit(OpCodes.Ldarg_3); - else - il.Emit(OpCodes.Ldarg_S, i); - } + 1 => OpCodes.Ldarg_1, + 2 => OpCodes.Ldarg_2, + 3 => OpCodes.Ldarg_3, + _ => OpCodes.Ldarg_S, + }; - if (hasIndexParameter) - il.Emit(OpCodes.Ldc_I4, index); + if (loadOp == OpCodes.Ldarg_S) + il.Emit(loadOp, argPos); + else + il.Emit(loadOp); + } - var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt; - il.Emit(callOp, method); + if (hasIndexParameter) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, IndexClosureField); + } - postProcessCall?.Invoke(il); - postProcessListCall?.Invoke(il, index); + var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt; + il.Emit(callOp, method); - il.Emit(OpCodes.Ret); + postProcessCall?.Invoke(il); + postProcessListCall?.Invoke(il); - return dm.CreateDelegate(delegateType); - }; + il.Emit(OpCodes.Ret); + + return index => dm.CreateDelegate(delegateType, new DelegateIndexClosure(index)); } } diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs index 370c7f9..34cb2c1 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionSetterAttribute.cs @@ -14,73 +14,25 @@ public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type? { internal Delegate? GetSetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) { - if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) - { - Error($"Method {MemberName} does not exist in {type.FullName}"); + if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) return null; - } var valueType = option.GetValueType(); - if (method.ReturnType != valueType) - { - Error($"{MemberName}'s return type is not {valueType.Name}."); + if (!ValidateSignature(method, valueType, valueType, valueType)) return null; - } - - var parameters = method.GetParameters(); - - if (parameters.Length != 2) - { - Error($"{MemberName} must have exactly two parameters."); - return null; - } - - if (parameters[0].ParameterType != valueType || parameters[1].ParameterType != valueType) - { - Error($"{MemberName}'s parameters must both be of the type {valueType.Name}."); - return null; - } var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType); return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "Setter"); } - internal Func? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) + internal Func? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) { - if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) - { - Error($"Method {MemberName} does not exist in {type.FullName}"); + if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method) return null; - } - if (method.ReturnType != valueType) - { - Error($"{MemberName}'s return type is not {valueType.Name}."); + if (!ValidateListSignature(method, valueType, out var hasIndex, valueType, valueType)) return null; - } - - var parameters = method.GetParameters(); - - if (parameters.Length is < 2 or > 3) - { - Error($"{MemberName} must have two parameters ({valueType.Name}, {valueType.Name}) or three parameters ({valueType.Name}, {valueType.Name}, int)."); - return null; - } - - if (parameters[0].ParameterType != valueType || parameters[1].ParameterType != valueType) - { - Error($"{MemberName}'s first two parameters must both be of the type {valueType.Name}."); - return null; - } - - var hasIndex = parameters.Length == 3; - - if (hasIndex && parameters[2].ParameterType != typeof(int)) - { - Error($"{MemberName}'s third parameter must be an int (the list index)."); - return null; - } var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType); return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "ListSetter", hasIndex); diff --git a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs index 72c35b0..6929cbf 100644 --- a/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/Delegate/ModdedOptionVisibilityAttribute.cs @@ -105,7 +105,7 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T return (Func)CompileDynamicDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func), "Visibility", false, postProcess); } - internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) + internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) { if (ResolveMember(group, property, out Type type, out var instanceGetter) is not { } member) { @@ -115,7 +115,7 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T MethodInfo? targetMethod; Action? postProcess = null; - Action? postProcessList = null; + Action? postProcessList = null; var hasIndexParam = false; @@ -126,9 +126,10 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T if (vProperty.PropertyType.IsAssignableTo(typeof(IList))) { isListBool = true; - postProcessList = (il, index) => + postProcessList = il => { - il.Emit(OpCodes.Ldc_I4, index); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, IndexClosureField); il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter); }; } diff --git a/MiraAPI/GameOptions/ModdedList.cs b/MiraAPI/GameOptions/ModdedList.cs new file mode 100644 index 0000000..b28b1cb --- /dev/null +++ b/MiraAPI/GameOptions/ModdedList.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MiraAPI.GameOptions; + +/// +/// A simple list that extends . +/// +/// The type of the values that the list holds. +public sealed class ModdedList : List; diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 21c9f1a..b30aa1c 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -26,12 +26,17 @@ namespace MiraAPI.GameOptions; public static class ModdedOptionsManager { private static readonly ConditionalWeakTable ListInstanceCache = []; - private static readonly Dictionary OptionAttributes = []; private static readonly Dictionary GetterLookups = []; private static readonly Dictionary SetterLookups = []; private static readonly Dictionary TypeToGroup = []; + private static readonly HashSet OptionAttributes = []; private static readonly HashSet PatchedListTypes = []; + private static readonly HarmonyMethod CachedGetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyGetterPatch)); + private static readonly HarmonyMethod CachedSetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertySetterPatch)); + private static readonly HarmonyMethod CachedListGetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyListGetterPatch)); + private static readonly HarmonyMethod CachedListSetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyListSetterPatch)); + internal static readonly Dictionary CreatedPlayerOptions = []; internal static readonly Dictionary CreatedStringOptions = []; internal static readonly Dictionary> GameModeOptionGroups = []; @@ -148,7 +153,7 @@ public static class ModdedOptionsManager PropertyInfo property, MiraPluginInfo pluginInfo) { - if (OptionAttributes.ContainsKey(property)) + if (!OptionAttributes.Add(property)) { Error($"Property {property.Name} already has an attribute registered."); return; @@ -168,52 +173,40 @@ public static class ModdedOptionsManager return; } - var getterOriginal = property.GetGetMethod(true); - var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); - PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); - - GetterLookups[getterOriginal!] = attribute; + var getterOriginal = property.GetGetMethod(true)!; + PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch); + GetterLookups[getterOriginal] = attribute; var setterOriginal = property.GetSetMethod(true); if (setterOriginal != null) { - var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); - PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); - + PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch); SetterLookups[setterOriginal] = attribute; } - OptionAttributes.Add(property, attribute); attribute.HolderOption = option; attribute.ValueType = property.PropertyType; - var visibilityAttr = property.GetCustomAttribute(); - var visibilityFunc = visibilityAttr?.GetVisibility(group, property); - if (visibilityFunc != null) - { - option.Visible = visibilityFunc; - } + var customAttributes = property.GetCustomAttributes(false); - var getterAtr = property.GetCustomAttribute(); - var getterFunc = getterAtr?.GetGetter(group, property, option); - if (getterFunc != null) + foreach (var attr in customAttributes) { - option.Getter = getterFunc; - } - - var setterAtr = property.GetCustomAttribute(); - var setterFunc = setterAtr?.GetSetter(group, property, option); - if (setterFunc != null) - { - option.Setter = setterFunc; - } - - var changedAtr = property.GetCustomAttribute(); - var changedFunc = changedAtr?.GetChanged(group, property, option); - if (changedFunc != null) - { - option.ChangedEvent = changedFunc; + switch (attr) + { + case ModdedOptionVisibilityAttribute visibilityAttr: + option.Visible = visibilityAttr.GetVisibility(group, property) ?? DelegateUtilities.AlwaysTrue; + break; + case ModdedOptionGetterAttribute getterAttr: + option.Getter = getterAttr.GetGetter(group, property, option); + break; + case ModdedOptionSetterAttribute setterAttr: + option.Setter = setterAttr.GetSetter(group, property, option); + break; + case ModdedOptionChangedAttribute changedAttr: + option.ChangedEvent = changedAttr.GetChanged(group, property, option); + break; + } } RegisterOption(option, group, property.Name, pluginInfo); @@ -247,7 +240,7 @@ public static class ModdedOptionsManager PropertyInfo property, MiraPluginInfo pluginInfo) { - if (OptionAttributes.ContainsKey(property)) + if (!OptionAttributes.Add(property)) { Error($"Property {property.Name} already has an attribute registered."); return; @@ -282,85 +275,84 @@ public static class ModdedOptionsManager return; } - var getterOriginal = property.GetGetMethod(true); - var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); - PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); + var listType = propertyList.GetType(); + var valueType = listType.GetGenericArguments()[0]; - GetterLookups[getterOriginal!] = attribute; + if (!listType.IsGenericType || listType.GetGenericTypeDefinition() != typeof(ModdedList<>)) + { + Error($"Please use ModdedList<{valueType.Name}> for modded option lists."); + return; + } + + var getterOriginal = property.GetGetMethod(true)!; + PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch); + GetterLookups[getterOriginal] = attribute; var setterOriginal = property.GetSetMethod(true); if (setterOriginal != null) { - var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); - PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); - + PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch); SetterLookups[setterOriginal] = attribute; } - var listType = propertyList.GetType(); - if (PatchedListTypes.Add(listType)) { var listIndex = listType.GetProperty("Item")!; var listGetterOriginal = listIndex.GetGetMethod(true); - var listGetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch)); - PluginSingleton.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch)); + PluginSingleton.Instance.Harmony.Patch(listGetterOriginal, prefix: CachedListGetterPatch); var listSetterOriginal = listIndex.GetSetMethod(true); if (listSetterOriginal != null) - { - var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch)); - PluginSingleton.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch)); - } + PluginSingleton.Instance.Harmony.Patch(listSetterOriginal, postfix: CachedListSetterPatch); } - OptionAttributes.Add(property, attribute); attribute.HolderOptionList = optionList; attribute.Value = propertyList; - attribute.ValueType = listType.GetGenericArguments()[0]; + attribute.ValueType = valueType; ListInstanceCache.Add(propertyVal, attribute); - var valueType = property.PropertyType.GetGenericArguments()[0]; + var customAttributes = property.GetCustomAttributes(false); - var visibilityAttr = property.GetCustomAttribute(); - var visibilityFactory = visibilityAttr?.GetListVisibility(group, property); + Func? visibilityFactory = null, getterFactory = null, setterFactory = null, changedFactory = null; - var getterAttr = property.GetCustomAttribute(); - var getterFactory = getterAttr?.GetListGetter(group, property, valueType); - - var setterAttr = property.GetCustomAttribute(); - var setterFactory = setterAttr?.GetListSetter(group, property, valueType); - - var changedAttr = property.GetCustomAttribute(); - var changedFactory = changedAttr?.GetListChanged(group, property, valueType); + foreach (var attr in customAttributes) + { + switch (attr) + { + case ModdedOptionVisibilityAttribute visibilityAttr: + visibilityFactory = visibilityAttr.GetListVisibility(group, property); + break; + case ModdedOptionGetterAttribute getterAttr: + getterFactory = getterAttr.GetListGetter(group, property, valueType); + break; + case ModdedOptionSetterAttribute setterAttr: + setterFactory = setterAttr.GetListSetter(group, property, valueType); + break; + case ModdedOptionChangedAttribute changedAttr: + changedFactory = changedAttr.GetListChanged(group, property, valueType); + break; + } + } for (var i = 0; i < optionList.Count; i++) { var option = optionList[i]; if (visibilityFactory != null) - { - option.Visible = (Func)visibilityFactory(i); - } + option.Visible = (Func)(visibilityFactory(i) ?? DelegateUtilities.AlwaysTrue); if (getterFactory != null) - { option.Getter = getterFactory(i); - } if (setterFactory != null) - { option.Setter = setterFactory(i); - } if (changedFactory != null) - { option.ChangedEvent = changedFactory(i); - } RegisterOption(option, group, property.Name + (i + 1), pluginInfo); } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 2342fc9..e2486ae 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -4,6 +4,7 @@ using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; using MiraAPI.Translation; +using MiraAPI.Utilities; using Reactor.Networking.Rpc; using UnityEngine; @@ -124,7 +125,7 @@ public abstract class ModdedOption : IModdedOption DefaultValue = defaultValue; Value = defaultValue; StringName = MiraLocaleManager.GetOrCreateLocaleString(Title); - Visible = () => true; + Visible = DelegateUtilities.AlwaysTrue; IncludeInPreset = includeInPreset; } diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index ed67fd2..be6809d 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -57,7 +57,7 @@ Outlive the timer or eliminate the crew. Options Player List Game Settings - Search... + Search... Filter Grouping Order diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 88b1f85..26dd0cf 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -339,7 +339,7 @@ public interface ICustomRole : IOptionable /// /// Gets the function that determines whether the role should be toggled on or off in the game settings. /// - Func VisibleInSettings => () => true; + Func VisibleInSettings => DelegateUtilities.AlwaysTrue; /// /// Gets the list of abilities or other similar information to display below the text of a role's wiki page. diff --git a/MiraAPI/Utilities/DelegateUtilities.cs b/MiraAPI/Utilities/DelegateUtilities.cs new file mode 100644 index 0000000..2ac4acf --- /dev/null +++ b/MiraAPI/Utilities/DelegateUtilities.cs @@ -0,0 +1,19 @@ +using System; + +namespace MiraAPI.Utilities; + +/// +/// A utility class for using delegates. +/// +public static class DelegateUtilities +{ + /// + /// Gets a Func<bool> that always returns true. + /// + public static Func AlwaysTrue { get; } = () => true; + + /// + /// Gets a Func<bool> that always returns false. + /// + public static Func AlwaysFalse { get; } = () => false; +}