optimisations and utilities

This commit is contained in:
Az committed 2026-10-17 15:52:53 -04:00
1 parent 6364f8eae6
commit 5106efe2ac
13 files changed
+284 -282

No files matched your search

+2 -3
View File
@@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using MiraAPI.GameOptions; using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.Attributes;
using MiraAPI.Utilities; using MiraAPI.Utilities;
@@ -43,7 +42,7 @@ public class ExampleOptions : AbstractOptionGroup
public Api WorstApi { get; set; } = Api.MiraAPI; public Api WorstApi { get; set; } = Api.MiraAPI;
[ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])] [ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])]
public List<ValueState> ValueStates { get; set; } = public ModdedList<ValueState> ValueStates { get; set; } =
[ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None]; [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
[ModdedPlayerOption("options.exampleOptions1.players")] [ModdedPlayerOption("options.exampleOptions1.players")]
+2 -1
View File
@@ -6,6 +6,7 @@ using System.Linq;
using Il2CppInterop.Runtime.Attributes; using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Roles; using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using UnityEngine; using UnityEngine;
@@ -55,7 +56,7 @@ public abstract class AbstractOptionGroup
/// <summary> /// <summary>
/// Gets the function that determines whether the group should be visible or not. /// Gets the function that determines whether the group should be visible or not.
/// </summary> /// </summary>
public virtual Func<bool> GroupVisible => () => true; public virtual Func<bool> GroupVisible => DelegateUtilities.AlwaysTrue;
/// <summary> /// <summary>
/// Gets a value indicating whether this group's options are shown in the Match Info Guide. /// Gets a value indicating whether this group's options are shown in the Match Info Guide.
@@ -14,73 +14,25 @@ public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type
{ {
internal Delegate? GetChanged(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) internal Delegate? GetChanged(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{ {
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null; 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(); var valueType = option.GetValueType();
if (parameters[0].ParameterType != valueType) if (!ValidateSignature(method, typeof(void), valueType))
{
Error($"{MemberName}'s parameter must be of the type {valueType.Name}.");
return null; return null;
}
var delegateType = typeof(Action<>).MakeGenericType(valueType); var delegateType = typeof(Action<>).MakeGenericType(valueType);
return CompileDynamicDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "Changed"); return CompileDynamicDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "Changed");
} }
internal Func<int, Delegate>? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType) internal Func<int, Delegate?>? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{ {
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null; return null;
}
if (method.ReturnType != typeof(void)) if (!ValidateListSignature(method, typeof(void), out var hasIndex, valueType))
{
Error($"{MemberName}'s return type is not void.");
return null; 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); var delegateType = typeof(Action<>).MakeGenericType(valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "ListChanged", hasIndex); return CompileDynamicListDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "ListChanged", hasIndex);
@@ -14,73 +14,25 @@ public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type?
{ {
internal Delegate? GetGetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) internal Delegate? GetGetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{ {
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null; return null;
}
var valueType = option.GetValueType(); var valueType = option.GetValueType();
if (method.ReturnType != valueType) if (!ValidateSignature(method, valueType, valueType))
{
Error($"{MemberName}'s return type is not {valueType.Name}.");
return null; 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); var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType);
return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "Getter"); return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "Getter");
} }
internal Func<int, Delegate>? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) internal Func<int, Delegate?>? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{ {
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null; return null;
}
if (method.ReturnType != valueType) if (!ValidateListSignature(method, valueType, out var hasIndex, valueType))
{
Error($"{MemberName}'s return type is not {valueType.Name}.");
return null; 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); var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "ListGetter", hasIndex); return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "ListGetter", hasIndex);
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@@ -15,11 +16,116 @@ namespace MiraAPI.GameOptions.Attributes;
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public abstract class ModdedOptionMemberAttribute(string? memberName, Type? holderType, string defaultSuffix) : Attribute public abstract class ModdedOptionMemberAttribute(string? memberName, Type? holderType, string defaultSuffix) : Attribute
{ {
private static readonly Dictionary<MethodInfo, Delegate> DelegateCache = [];
/// <summary>
/// A helper class to bind a specific list index to a dynamically compiled delegate.
/// </summary>
/// <param name="index">The index to bind.</param>
public sealed class DelegateIndexClosure(int index)
{
/// <summary>
/// The bound index.
/// </summary>
[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;
}
/// <summary>
/// Exposes the index field so child attributes can emit instructions to read it.
/// </summary>
protected static readonly FieldInfo IndexClosureField = typeof(DelegateIndexClosure).GetField(nameof(DelegateIndexClosure.Index))!;
/// <summary> /// <summary>
/// Gets the resolved name of the target member. /// Gets the resolved name of the target member.
/// </summary> /// </summary>
protected string? MemberName { get; private set; } = memberName; protected string? MemberName { get; private set; } = memberName;
/// <summary>
/// Validates that a method matches the expected return type and parameter types.
/// </summary>
/// <param name="method">The method to validate.</param>
/// <param name="expectedReturnType">The expected return type of the method.</param>
/// <param name="expectedParamTypes">The expected types of the parameters of the method.</param>
/// <returns><c>true</c> if the method is valid; <c>false</c> otherwise.</returns>
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;
}
/// <summary>
/// Validates that a list method matches the expected return type, base parameter types, and an optional integer index.
/// </summary>
/// <param name="method">The method to validate.</param>
/// <param name="expectedReturnType">The expected return type of the method.</param>
/// <param name="hasIndex">When the method returns, a flag that indicates if the method has an index parameter.</param>
/// <param name="expectedParamTypes">The expected types of the parameters of the method.</param>
/// <returns><c>true</c> if the method is valid; <c>false</c> otherwise.</returns>
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;
}
/// <summary> /// <summary>
/// Resolves the target <see cref="MemberInfo"/> (property or method) and its associated singleton instance getter, if applicable. /// Resolves the target <see cref="MemberInfo"/> (property or method) and its associated singleton instance getter, if applicable.
/// </summary> /// </summary>
@@ -123,6 +229,9 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
bool forwardParameters = true, bool forwardParameters = true,
Action<ILGenerator>? postProcessCall = null) Action<ILGenerator>? 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 dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}", returnType, paramTypes, ownerType, true);
var il = dm.GetILGenerator(); var il = dm.GetILGenerator();
@@ -133,16 +242,19 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
{ {
for (byte i = 0; i < paramTypes.Length; i++) for (byte i = 0; i < paramTypes.Length; i++)
{ {
if (i == 0) var loadOp = i switch
il.Emit(OpCodes.Ldarg_0); {
else if (i == 1) 0 => OpCodes.Ldarg_0,
il.Emit(OpCodes.Ldarg_1); 1 => OpCodes.Ldarg_1,
else if (i == 2) 2 => OpCodes.Ldarg_2,
il.Emit(OpCodes.Ldarg_2); 3 => OpCodes.Ldarg_3,
else if (i == 3) _ => OpCodes.Ldarg_S,
il.Emit(OpCodes.Ldarg_3); };
if (loadOp == OpCodes.Ldarg_S)
il.Emit(loadOp, i);
else 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); il.Emit(OpCodes.Ret);
return dm.CreateDelegate(delegateType); var compiledDelegate = dm.CreateDelegate(delegateType);
if (postProcessCall == null)
DelegateCache[method] = compiledDelegate;
return compiledDelegate;
} }
/// <summary> /// <summary>
@@ -185,7 +302,7 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
string suffix, string suffix,
bool hasIndexParameter, bool hasIndexParameter,
Action<ILGenerator>? postProcessCall = null, Action<ILGenerator>? postProcessCall = null,
Action<ILGenerator, int>? postProcessListCall = null) Action<ILGenerator>? postProcessListCall = null)
{ {
if (!hasIndexParameter && postProcessListCall is null) if (!hasIndexParameter && postProcessListCall is null)
{ {
@@ -203,40 +320,47 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
return _ => sharedDelegate; 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 argPos = (byte)(i + 1);
var il = dm.GetILGenerator(); var loadOp = argPos switch
if (!method.IsStatic && instanceGetter != null)
il.Emit(OpCodes.Call, instanceGetter);
for (byte i = 0; i < baseParamTypes.Length; i++)
{ {
if (i == 0) 1 => OpCodes.Ldarg_1,
il.Emit(OpCodes.Ldarg_0); 2 => OpCodes.Ldarg_2,
else if (i == 1) 3 => OpCodes.Ldarg_3,
il.Emit(OpCodes.Ldarg_1); _ => OpCodes.Ldarg_S,
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);
}
if (hasIndexParameter) if (loadOp == OpCodes.Ldarg_S)
il.Emit(OpCodes.Ldc_I4, index); il.Emit(loadOp, argPos);
else
il.Emit(loadOp);
}
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt; if (hasIndexParameter)
il.Emit(callOp, method); {
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, IndexClosureField);
}
postProcessCall?.Invoke(il); var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
postProcessListCall?.Invoke(il, index); 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));
} }
} }
@@ -14,73 +14,25 @@ public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type?
{ {
internal Delegate? GetSetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option) internal Delegate? GetSetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{ {
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null; return null;
}
var valueType = option.GetValueType(); var valueType = option.GetValueType();
if (method.ReturnType != valueType) if (!ValidateSignature(method, valueType, valueType, valueType))
{
Error($"{MemberName}'s return type is not {valueType.Name}.");
return null; 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); var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType);
return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "Setter"); return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "Setter");
} }
internal Func<int, Delegate>? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType) internal Func<int, Delegate?>? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{ {
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method) if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null; return null;
}
if (method.ReturnType != valueType) if (!ValidateListSignature(method, valueType, out var hasIndex, valueType, valueType))
{
Error($"{MemberName}'s return type is not {valueType.Name}.");
return null; 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); var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "ListSetter", hasIndex); return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "ListSetter", hasIndex);
@@ -105,7 +105,7 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T
return (Func<bool>)CompileDynamicDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func<bool>), "Visibility", false, postProcess); return (Func<bool>)CompileDynamicDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func<bool>), "Visibility", false, postProcess);
} }
internal Func<int, Delegate>? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) internal Func<int, Delegate?>? 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) is not { } member)
{ {
@@ -115,7 +115,7 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T
MethodInfo? targetMethod; MethodInfo? targetMethod;
Action<ILGenerator>? postProcess = null; Action<ILGenerator>? postProcess = null;
Action<ILGenerator, int>? postProcessList = null; Action<ILGenerator>? postProcessList = null;
var hasIndexParam = false; var hasIndexParam = false;
@@ -126,9 +126,10 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>))) if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
{ {
isListBool = true; 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); il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter);
}; };
} }
+9
View File
@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace MiraAPI.GameOptions;
/// <summary>
/// A simple list that extends <see cref="List{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the values that the list holds.</typeparam>
public sealed class ModdedList<T> : List<T>;
+65 -73
View File
@@ -26,12 +26,17 @@ namespace MiraAPI.GameOptions;
public static class ModdedOptionsManager public static class ModdedOptionsManager
{ {
private static readonly ConditionalWeakTable<object, ModdedOptionListAttribute> ListInstanceCache = []; private static readonly ConditionalWeakTable<object, ModdedOptionListAttribute> ListInstanceCache = [];
private static readonly Dictionary<PropertyInfo, PropertyOptionAttribute> OptionAttributes = [];
private static readonly Dictionary<MethodBase, PropertyOptionAttribute> GetterLookups = []; private static readonly Dictionary<MethodBase, PropertyOptionAttribute> GetterLookups = [];
private static readonly Dictionary<MethodBase, PropertyOptionAttribute> SetterLookups = []; private static readonly Dictionary<MethodBase, PropertyOptionAttribute> SetterLookups = [];
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = []; private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
private static readonly HashSet<PropertyInfo> OptionAttributes = [];
private static readonly HashSet<Type> PatchedListTypes = []; private static readonly HashSet<Type> 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<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = []; internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
internal static readonly Dictionary<OptionBehaviour, ModdedStringOption> CreatedStringOptions = []; internal static readonly Dictionary<OptionBehaviour, ModdedStringOption> CreatedStringOptions = [];
internal static readonly Dictionary<Type, List<AbstractOptionGroup>> GameModeOptionGroups = []; internal static readonly Dictionary<Type, List<AbstractOptionGroup>> GameModeOptionGroups = [];
@@ -148,7 +153,7 @@ public static class ModdedOptionsManager
PropertyInfo property, PropertyInfo property,
MiraPluginInfo pluginInfo) MiraPluginInfo pluginInfo)
{ {
if (OptionAttributes.ContainsKey(property)) if (!OptionAttributes.Add(property))
{ {
Error($"Property {property.Name} already has an attribute registered."); Error($"Property {property.Name} already has an attribute registered.");
return; return;
@@ -168,52 +173,40 @@ public static class ModdedOptionsManager
return; return;
} }
var getterOriginal = property.GetGetMethod(true); var getterOriginal = property.GetGetMethod(true)!;
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); GetterLookups[getterOriginal] = attribute;
GetterLookups[getterOriginal!] = attribute;
var setterOriginal = property.GetSetMethod(true); var setterOriginal = property.GetSetMethod(true);
if (setterOriginal != null) if (setterOriginal != null)
{ {
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
SetterLookups[setterOriginal] = attribute; SetterLookups[setterOriginal] = attribute;
} }
OptionAttributes.Add(property, attribute);
attribute.HolderOption = option; attribute.HolderOption = option;
attribute.ValueType = property.PropertyType; attribute.ValueType = property.PropertyType;
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>(); var customAttributes = property.GetCustomAttributes(false);
var visibilityFunc = visibilityAttr?.GetVisibility(group, property);
if (visibilityFunc != null)
{
option.Visible = visibilityFunc;
}
var getterAtr = property.GetCustomAttribute<ModdedOptionGetterAttribute>(); foreach (var attr in customAttributes)
var getterFunc = getterAtr?.GetGetter(group, property, option);
if (getterFunc != null)
{ {
option.Getter = getterFunc; switch (attr)
} {
case ModdedOptionVisibilityAttribute visibilityAttr:
var setterAtr = property.GetCustomAttribute<ModdedOptionSetterAttribute>(); option.Visible = visibilityAttr.GetVisibility(group, property) ?? DelegateUtilities.AlwaysTrue;
var setterFunc = setterAtr?.GetSetter(group, property, option); break;
if (setterFunc != null) case ModdedOptionGetterAttribute getterAttr:
{ option.Getter = getterAttr.GetGetter(group, property, option);
option.Setter = setterFunc; break;
} case ModdedOptionSetterAttribute setterAttr:
option.Setter = setterAttr.GetSetter(group, property, option);
var changedAtr = property.GetCustomAttribute<ModdedOptionChangedAttribute>(); break;
var changedFunc = changedAtr?.GetChanged(group, property, option); case ModdedOptionChangedAttribute changedAttr:
if (changedFunc != null) option.ChangedEvent = changedAttr.GetChanged(group, property, option);
{ break;
option.ChangedEvent = changedFunc; }
} }
RegisterOption(option, group, property.Name, pluginInfo); RegisterOption(option, group, property.Name, pluginInfo);
@@ -247,7 +240,7 @@ public static class ModdedOptionsManager
PropertyInfo property, PropertyInfo property,
MiraPluginInfo pluginInfo) MiraPluginInfo pluginInfo)
{ {
if (OptionAttributes.ContainsKey(property)) if (!OptionAttributes.Add(property))
{ {
Error($"Property {property.Name} already has an attribute registered."); Error($"Property {property.Name} already has an attribute registered.");
return; return;
@@ -282,85 +275,84 @@ public static class ModdedOptionsManager
return; return;
} }
var getterOriginal = property.GetGetMethod(true); var listType = propertyList.GetType();
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); var valueType = listType.GetGenericArguments()[0];
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
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<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
GetterLookups[getterOriginal] = attribute;
var setterOriginal = property.GetSetMethod(true); var setterOriginal = property.GetSetMethod(true);
if (setterOriginal != null) if (setterOriginal != null)
{ {
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
SetterLookups[setterOriginal] = attribute; SetterLookups[setterOriginal] = attribute;
} }
var listType = propertyList.GetType();
if (PatchedListTypes.Add(listType)) if (PatchedListTypes.Add(listType))
{ {
var listIndex = listType.GetProperty("Item")!; var listIndex = listType.GetProperty("Item")!;
var listGetterOriginal = listIndex.GetGetMethod(true); var listGetterOriginal = listIndex.GetGetMethod(true);
var listGetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch)); PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: CachedListGetterPatch);
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch));
var listSetterOriginal = listIndex.GetSetMethod(true); var listSetterOriginal = listIndex.GetSetMethod(true);
if (listSetterOriginal != null) if (listSetterOriginal != null)
{ PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listSetterOriginal, postfix: CachedListSetterPatch);
var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch));
}
} }
OptionAttributes.Add(property, attribute);
attribute.HolderOptionList = optionList; attribute.HolderOptionList = optionList;
attribute.Value = propertyList; attribute.Value = propertyList;
attribute.ValueType = listType.GetGenericArguments()[0]; attribute.ValueType = valueType;
ListInstanceCache.Add(propertyVal, attribute); ListInstanceCache.Add(propertyVal, attribute);
var valueType = property.PropertyType.GetGenericArguments()[0]; var customAttributes = property.GetCustomAttributes(false);
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>(); Func<int, Delegate?>? visibilityFactory = null, getterFactory = null, setterFactory = null, changedFactory = null;
var visibilityFactory = visibilityAttr?.GetListVisibility(group, property);
var getterAttr = property.GetCustomAttribute<ModdedOptionGetterAttribute>(); foreach (var attr in customAttributes)
var getterFactory = getterAttr?.GetListGetter(group, property, valueType); {
switch (attr)
var setterAttr = property.GetCustomAttribute<ModdedOptionSetterAttribute>(); {
var setterFactory = setterAttr?.GetListSetter(group, property, valueType); case ModdedOptionVisibilityAttribute visibilityAttr:
visibilityFactory = visibilityAttr.GetListVisibility(group, property);
var changedAttr = property.GetCustomAttribute<ModdedOptionChangedAttribute>(); break;
var changedFactory = changedAttr?.GetListChanged(group, property, valueType); 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++) for (var i = 0; i < optionList.Count; i++)
{ {
var option = optionList[i]; var option = optionList[i];
if (visibilityFactory != null) if (visibilityFactory != null)
{ option.Visible = (Func<bool>)(visibilityFactory(i) ?? DelegateUtilities.AlwaysTrue);
option.Visible = (Func<bool>)visibilityFactory(i);
}
if (getterFactory != null) if (getterFactory != null)
{
option.Getter = getterFactory(i); option.Getter = getterFactory(i);
}
if (setterFactory != null) if (setterFactory != null)
{
option.Setter = setterFactory(i); option.Setter = setterFactory(i);
}
if (changedFactory != null) if (changedFactory != null)
{
option.ChangedEvent = changedFactory(i); option.ChangedEvent = changedFactory(i);
}
RegisterOption(option, group, property.Name + (i + 1), pluginInfo); RegisterOption(option, group, property.Name + (i + 1), pluginInfo);
} }
@@ -4,6 +4,7 @@ using BepInEx.Configuration;
using MiraAPI.Networking; using MiraAPI.Networking;
using MiraAPI.PluginLoading; using MiraAPI.PluginLoading;
using MiraAPI.Translation; using MiraAPI.Translation;
using MiraAPI.Utilities;
using Reactor.Networking.Rpc; using Reactor.Networking.Rpc;
using UnityEngine; using UnityEngine;
@@ -124,7 +125,7 @@ public abstract class ModdedOption<T> : IModdedOption
DefaultValue = defaultValue; DefaultValue = defaultValue;
Value = defaultValue; Value = defaultValue;
StringName = MiraLocaleManager.GetOrCreateLocaleString(Title); StringName = MiraLocaleManager.GetOrCreateLocaleString(Title);
Visible = () => true; Visible = DelegateUtilities.AlwaysTrue;
IncludeInPreset = includeInPreset; IncludeInPreset = includeInPreset;
} }
+1 -1
View File
@@ -57,7 +57,7 @@ Outlive the timer or eliminate the crew.</string>
<string name="Options">Options</string> <string name="Options">Options</string>
<string name="MiraApi.Wiki.PlayersTab">Player List</string> <string name="MiraApi.Wiki.PlayersTab">Player List</string>
<string name="MiraApi.Wiki.SettingsTab">Game Settings</string> <string name="MiraApi.Wiki.SettingsTab">Game Settings</string>
<string name="MiraApi.Wiki.SearchPlaceholder">Search...</string> <string name="MiraApi.Wiki.SearchPlaceholder">Search...</string>
<string name="MiraApi.Wiki.FilterLabel">Filter</string> <string name="MiraApi.Wiki.FilterLabel">Filter</string>
<string name="MiraApi.Wiki.GroupingLabel">Grouping</string> <string name="MiraApi.Wiki.GroupingLabel">Grouping</string>
<string name="MiraApi.Wiki.OrderLabel">Order</string> <string name="MiraApi.Wiki.OrderLabel">Order</string>
+1 -1
View File
@@ -339,7 +339,7 @@ public interface ICustomRole : IOptionable
/// <summary> /// <summary>
/// Gets the function that determines whether the role should be toggled on or off in the game settings. /// Gets the function that determines whether the role should be toggled on or off in the game settings.
/// </summary> /// </summary>
Func<bool> VisibleInSettings => () => true; Func<bool> VisibleInSettings => DelegateUtilities.AlwaysTrue;
/// <summary> /// <summary>
/// Gets the list of abilities or other similar information to display below the text of a role's wiki page. /// Gets the list of abilities or other similar information to display below the text of a role's wiki page.
+19
View File
@@ -0,0 +1,19 @@
using System;
namespace MiraAPI.Utilities;
/// <summary>
/// A utility class for using delegates.
/// </summary>
public static class DelegateUtilities
{
/// <summary>
/// Gets a <c>Func&lt;bool&gt;</c> that always returns <c>true</c>.
/// </summary>
public static Func<bool> AlwaysTrue { get; } = () => true;
/// <summary>
/// Gets a <c>Func&lt;bool&gt;</c> that always returns <c>false</c>.
/// </summary>
public static Func<bool> AlwaysFalse { get; } = () => false;
}