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.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")]
+2 -1
View File
@@ -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);
};
}
+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
{
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;
}
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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.
+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;
}