mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-19 15:51:48 +00:00
optimisations and utilities
This commit is contained in:
13 files changed
+284
-282
No files matched your search
@@ -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<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];
|
||||
|
||||
[ModdedPlayerOption("options.exampleOptions1.players")]
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Gets the function that determines whether the group should be visible or not.
|
||||
/// </summary>
|
||||
public virtual Func<bool> GroupVisible => () => true;
|
||||
public virtual Func<bool> GroupVisible => DelegateUtilities.AlwaysTrue;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
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<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)
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -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<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)
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -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<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>
|
||||
/// Gets the resolved name of the target member.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Resolves the target <see cref="MemberInfo"/> (property or method) and its associated singleton instance getter, if applicable.
|
||||
/// </summary>
|
||||
@@ -123,6 +229,9 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
|
||||
bool forwardParameters = true,
|
||||
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 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -185,7 +302,7 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
|
||||
string suffix,
|
||||
bool hasIndexParameter,
|
||||
Action<ILGenerator>? postProcessCall = null,
|
||||
Action<ILGenerator, int>? postProcessListCall = null)
|
||||
Action<ILGenerator>? 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));
|
||||
}
|
||||
}
|
||||
@@ -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<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)
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -115,7 +115,7 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T
|
||||
|
||||
MethodInfo? targetMethod;
|
||||
Action<ILGenerator>? postProcess = null;
|
||||
Action<ILGenerator, int>? postProcessList = null;
|
||||
Action<ILGenerator>? postProcessList = null;
|
||||
|
||||
var hasIndexParam = false;
|
||||
|
||||
@@ -126,9 +126,10 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T
|
||||
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
|
||||
{
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
@@ -26,12 +26,17 @@ namespace MiraAPI.GameOptions;
|
||||
public static class ModdedOptionsManager
|
||||
{
|
||||
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> SetterLookups = [];
|
||||
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
|
||||
private static readonly HashSet<PropertyInfo> OptionAttributes = [];
|
||||
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, ModdedStringOption> CreatedStringOptions = [];
|
||||
internal static readonly Dictionary<Type, List<AbstractOptionGroup>> 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<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
|
||||
|
||||
GetterLookups[getterOriginal!] = attribute;
|
||||
var getterOriginal = property.GetGetMethod(true)!;
|
||||
PluginSingleton<MiraApiPlugin>.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<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
|
||||
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
|
||||
SetterLookups[setterOriginal] = attribute;
|
||||
}
|
||||
|
||||
OptionAttributes.Add(property, attribute);
|
||||
attribute.HolderOption = option;
|
||||
attribute.ValueType = property.PropertyType;
|
||||
|
||||
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>();
|
||||
var visibilityFunc = visibilityAttr?.GetVisibility(group, property);
|
||||
if (visibilityFunc != null)
|
||||
{
|
||||
option.Visible = visibilityFunc;
|
||||
}
|
||||
var customAttributes = property.GetCustomAttributes(false);
|
||||
|
||||
var getterAtr = property.GetCustomAttribute<ModdedOptionGetterAttribute>();
|
||||
var getterFunc = getterAtr?.GetGetter(group, property, option);
|
||||
if (getterFunc != null)
|
||||
foreach (var attr in customAttributes)
|
||||
{
|
||||
option.Getter = getterFunc;
|
||||
}
|
||||
|
||||
var setterAtr = property.GetCustomAttribute<ModdedOptionSetterAttribute>();
|
||||
var setterFunc = setterAtr?.GetSetter(group, property, option);
|
||||
if (setterFunc != null)
|
||||
{
|
||||
option.Setter = setterFunc;
|
||||
}
|
||||
|
||||
var changedAtr = property.GetCustomAttribute<ModdedOptionChangedAttribute>();
|
||||
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<MiraApiPlugin>.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<MiraApiPlugin>.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<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
|
||||
|
||||
PluginSingleton<MiraApiPlugin>.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<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch));
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: CachedListGetterPatch);
|
||||
|
||||
var listSetterOriginal = listIndex.GetSetMethod(true);
|
||||
|
||||
if (listSetterOriginal != null)
|
||||
{
|
||||
var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch));
|
||||
}
|
||||
PluginSingleton<MiraApiPlugin>.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<ModdedOptionVisibilityAttribute>();
|
||||
var visibilityFactory = visibilityAttr?.GetListVisibility(group, property);
|
||||
Func<int, Delegate?>? visibilityFactory = null, getterFactory = null, setterFactory = null, changedFactory = null;
|
||||
|
||||
var getterAttr = property.GetCustomAttribute<ModdedOptionGetterAttribute>();
|
||||
var getterFactory = getterAttr?.GetListGetter(group, property, valueType);
|
||||
|
||||
var setterAttr = property.GetCustomAttribute<ModdedOptionSetterAttribute>();
|
||||
var setterFactory = setterAttr?.GetListSetter(group, property, valueType);
|
||||
|
||||
var changedAttr = property.GetCustomAttribute<ModdedOptionChangedAttribute>();
|
||||
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<bool>)visibilityFactory(i);
|
||||
}
|
||||
option.Visible = (Func<bool>)(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);
|
||||
}
|
||||
|
||||
@@ -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<T> : IModdedOption
|
||||
DefaultValue = defaultValue;
|
||||
Value = defaultValue;
|
||||
StringName = MiraLocaleManager.GetOrCreateLocaleString(Title);
|
||||
Visible = () => true;
|
||||
Visible = DelegateUtilities.AlwaysTrue;
|
||||
IncludeInPreset = includeInPreset;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ Outlive the timer or eliminate the crew.</string>
|
||||
<string name="Options">Options</string>
|
||||
<string name="MiraApi.Wiki.PlayersTab">Player List</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.GroupingLabel">Grouping</string>
|
||||
<string name="MiraApi.Wiki.OrderLabel">Order</string>
|
||||
|
||||
@@ -339,7 +339,7 @@ public interface ICustomRole : IOptionable
|
||||
/// <summary>
|
||||
/// Gets the function that determines whether the role should be toggled on or off in the game settings.
|
||||
/// </summary>
|
||||
Func<bool> VisibleInSettings => () => true;
|
||||
Func<bool> VisibleInSettings => DelegateUtilities.AlwaysTrue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of abilities or other similar information to display below the text of a role's wiki page.
|
||||
|
||||
@@ -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<bool></c> that always returns <c>true</c>.
|
||||
/// </summary>
|
||||
public static Func<bool> AlwaysTrue { get; } = () => true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <c>Func<bool></c> that always returns <c>false</c>.
|
||||
/// </summary>
|
||||
public static Func<bool> AlwaysFalse { get; } = () => false;
|
||||
}
|
||||
Reference in new issue
Block a user