improve options system

This commit is contained in:
Az committed 2026-10-15 01:41:56 -04:00
1 parent 56cc7661c7
commit a31b9e29b9
24 files changed
+1052 -296

No files matched your search

+3
View File
@@ -101,3 +101,6 @@ dotnet_diagnostic.IDE0046.severity = none
# S1244: Floating point numbers should not be tested for equality
dotnet_diagnostic.S1244.severity = none
# S2325: Methods and properties that don't access instance data should be static
dotnet_diagnostic.S2325.severity = none
+45 -9
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Utilities;
@@ -6,33 +7,68 @@ using UnityEngine;
namespace MiraAPI.Example.Options;
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Demo.")]
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Read above.")]
[SuppressMessage("Minor Code Smell", "S3400:Methods should not return constants", Justification = "Read above.")]
public class ExampleOptions : AbstractOptionGroup
{
public override string GroupName => "options.exampleOptions1";
public override Color GroupColor => Color.green;
[ModdedToggleOption("options.exampleOptions1.toggleOpt1")]
public bool ToggleOpt { get; set; } = false;
[ModdedOptionVisibility] // Automatically resolves to using ToggleOpt1Visibility, can also target toggle options
public bool ToggleOpt1 { get; set; } = false;
[ModdedToggleOption("options.exampleOptions1.toggleOpt1")]
[ModdedToggleOption("options.exampleOptions1.toggleOpt2")]
[ModdedOptionVisibility(nameof(ToggleOpt1))] // ToggleOpt2 will be visible only when ToggleOpt1 is true
public bool ToggleOpt2 { get; set; } = true;
[ModdedToggleOption("Toggle Opt 3")]
[ModdedOptionVisibility(nameof(ToggleOpt2))] // ToggleOpt3 will be visible only when ToggleOpt2 is true
public bool ToggleOpt3 { get; set; } = true;
[ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)]
[ModdedOptionVisibility(nameof(NumberOptVisibility))] // Can also target static methods
public float NumberOpt { get; set; } = 4f;
[ModdedEnumOption("options.exampleOptions1.bestApi", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])]
public BestApi Opt { get; set; } = BestApi.MiraAPI;
[ModdedEnumOption("options.exampleOptions1.bestApi", typeof(Api), ["Mira API", "Mitochondria", "Reactor"])]
// These three can target both instance and static methods
[ModdedOptionGetter(nameof(GetApi))] // Can also automatically resolve to {Option Name}Getter
[ModdedOptionSetter(nameof(SetApi))] // Can also automatically resolve to {Option Name}Setter
[ModdedOptionChanged(nameof(ApiChanged))] // Can also automatically resolve to {Option Name}Changed
public Api BestApi { get; set; } = Api.MiraAPI;
[ModdedEnumOption<Api>("options.exampleOptions1.worstApi", ["Mira API", "Mitochondria", "Reactor"])]
public Api WorstApi { get; set; } = Api.MiraAPI;
[ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])]
public List<ValueState> ValueStates { get; } =
[ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
public bool ToggleOpt1Visibility()
{
return true;
}
public enum BestApi
public static bool NumberOptVisibility()
{
return true;
}
public static Api GetApi(Api currentValue)
{
return currentValue;
}
public static Api SetApi(Api newValue, Api oldValue)
{
return default;
}
public static void ApiChanged(Api currentValue)
{
// Something
}
}
public enum Api
{
MiraAPI,
Mitochondria,
+2 -2
View File
@@ -7,9 +7,7 @@ namespace MiraAPI.Example.Options;
public class ExampleOptions2 : AbstractOptionGroup
{
public override string GroupName => "options.exampleOptions2";
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
public override MenuCategory ParentMenu => MenuCategory.CustomTwo;
public ModdedPlayerOption PlayerOption2 { get; } = new("options.exampleOptions2.youMustChoseAPlayer", false);
@@ -27,6 +25,8 @@ public class ExampleOptions2 : AbstractOptionGroup
public ModdedEnumOption<TestingData> EnumOpt { get; } = new("options.exampleOptions2.enumOpt", 0)
{
ChangedEvent = x => Logger<ExamplePlugin>.Info($"changed Enum Opt to {x}"),
Getter = currentValue => currentValue,
Setter = (newValue, oldValue) => newValue,
};
}
@@ -0,0 +1,88 @@
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to set the <see cref="IModdedOption.ChangedEvent"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <param name="methodName">The method to get the changed event from.</param>
/// <param name="holderType">The type the method is in.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type? holderType = null)
: ModdedOptionMemberAttribute(methodName, holderType, "Changed")
{
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}");
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}.");
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)
{
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (method.ReturnType != typeof(void))
{
Error($"{MemberName}'s return type is not void.");
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);
}
}
@@ -0,0 +1,88 @@
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to set the <see cref="IModdedOption.Getter"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <param name="methodName">The method to get the getter from.</param>
/// <param name="holderType">The type the method is in.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type? holderType = null)
: ModdedOptionMemberAttribute(methodName, holderType, "Getter")
{
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}");
return null;
}
var valueType = option.GetValueType();
if (method.ReturnType != valueType)
{
Error($"{MemberName}'s return type is not {valueType.Name}.");
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)
{
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (method.ReturnType != valueType)
{
Error($"{MemberName}'s return type is not {valueType.Name}.");
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);
}
}
@@ -0,0 +1,226 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Base attribute for resolving and compiling option delegate members (<see cref="IModdedOption.Visible"/>, <see cref="IModdedOption.Getter"/>, <see cref="IModdedOption.Setter"/>, and <see cref="IModdedOption.ChangedEvent"/>).
/// </summary>
/// <param name="memberName">The name of the target member, or <see langword="null"/> to use the property name with <paramref name="defaultSuffix"/>.</param>
/// <param name="holderType">The type containing the target member, or <see langword="null"/> to use the declaring <see cref="AbstractOptionGroup"/> type.</param>
/// <param name="defaultSuffix">The default suffix appended to the property name when <paramref name="memberName"/> is not specified.</param>
[AttributeUsage(AttributeTargets.Property)]
public abstract class ModdedOptionMemberAttribute(string? memberName, Type? holderType, string defaultSuffix) : Attribute
{
/// <summary>
/// Gets the resolved name of the target member.
/// </summary>
protected string? MemberName { get; private set; } = memberName;
/// <summary>
/// Resolves the target <see cref="MemberInfo"/> (property or method) and its associated singleton instance getter, if applicable.
/// </summary>
/// <param name="group">The option group instance the property belongs to.</param>
/// <param name="property">The option property decorated with this attribute.</param>
/// <param name="type">When this method returns, contains the resolved <see cref="Type"/> expected to hold the target member.</param>
/// <param name="instanceGetter">
/// When this method returns, contains the <see cref="OptionGroupSingleton{T}.Instance"/> getter <see cref="MethodInfo"/>
/// if the resolved member is an instance member on an <see cref="AbstractOptionGroup"/>; otherwise, <see langword="null"/>.
/// </param>
/// <returns>The resolved <see cref="MemberInfo"/> if found; otherwise, <see langword="null"/>.</returns>
[SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")]
protected MemberInfo? ResolveMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter)
{
var groupType = group.GetType();
var flags = BindingFlags.Static | BindingFlags.Public;
type = holderType ?? groupType;
MemberName ??= $"{property.Name}{defaultSuffix}";
if (type.IsAssignableTo(typeof(AbstractOptionGroup)))
{
flags |= BindingFlags.Instance;
if (type == groupType)
flags |= BindingFlags.NonPublic;
instanceGetter = typeof(OptionGroupSingleton<>)
.MakeGenericType(type)
.GetProperty(nameof(OptionGroupSingleton<>.Instance))!
.GetGetMethod();
}
else
{
instanceGetter = null;
}
var member = type.GetMember(MemberName, flags).FirstOrDefault();
if (instanceGetter != null)
{
var method = (member as PropertyInfo)?.GetGetMethod(true) ?? member as MethodInfo;
if (method is { IsStatic: true })
instanceGetter = null;
}
return member;
}
/// <summary>
/// Resolves the target <see cref="MethodInfo"/> and its associated singleton instance getter, if applicable.
/// </summary>
/// <param name="group">The option group instance the property belongs to.</param>
/// <param name="property">The option property decorated with this attribute.</param>
/// <param name="type">When this method returns, contains the resolved <see cref="Type"/> expected to hold the target method.</param>
/// <param name="instanceGetter">
/// When this method returns, contains the <see cref="OptionGroupSingleton{T}.Instance"/> getter <see cref="MethodInfo"/>
/// if the resolved method is an instance method on an <see cref="AbstractOptionGroup"/>; otherwise, <see langword="null"/>.
/// </param>
/// <returns>The resolved <see cref="MethodInfo"/> if found; otherwise, <see langword="null"/>.</returns>
protected MethodInfo? ResolveMethod(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter)
{
return ResolveMember(group, property, out type, out instanceGetter) as MethodInfo;
}
/// <summary>
/// Compiles a <see cref="DynamicMethod"/> using IL generation to invoke the resolved static or instance <paramref name="method"/>.
/// </summary>
/// <param name="ownerType">The <see cref="Type"/> that owns the generated <see cref="DynamicMethod"/> and holds the target method.</param>
/// <param name="method">The target <see cref="MethodInfo"/> to invoke.</param>
/// <param name="instanceGetter">
/// The static getter <see cref="MethodInfo"/> used to push the target instance onto the evaluation stack for instance methods,
/// or <see langword="null"/> if <paramref name="method"/> is static.
/// </param>
/// <param name="returnType">The return <see cref="Type"/> of the generated delegate (or <see cref="void"/> for actions).</param>
/// <param name="paramTypes">The parameter types accepted by the generated delegate.</param>
/// <param name="delegateType">The concrete delegate <see cref="Type"/> to create (e.g., <see cref="Func{T, TResult}"/> or <see cref="Action{T}"/>).</param>
/// <param name="suffix">A descriptive suffix appended to the generated <see cref="DynamicMethod"/> name for debugging and stack traces.</param>
/// <param name="forwardParameters">Whether to push the delegate parameters onto the stack before invoking <paramref name="method"/>.</param>
/// <param name="postProcessCall">An optional callback to emit additional IL instructions before returning.</param>
/// <returns>A compiled <see cref="Delegate"/> of type <paramref name="delegateType"/> bound to the generated IL method.</returns>
protected static Delegate CompileDynamicDelegate(
Type ownerType,
MethodInfo method,
MethodInfo? instanceGetter,
Type returnType,
Type[] paramTypes,
Type delegateType,
string suffix,
bool forwardParameters = true,
Action<ILGenerator>? postProcessCall = null)
{
var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}", returnType, paramTypes, ownerType, true);
var il = dm.GetILGenerator();
if (!method.IsStatic && instanceGetter != null)
il.Emit(OpCodes.Call, instanceGetter);
if (forwardParameters)
{
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);
else
il.Emit(OpCodes.Ldarg_S, i);
}
}
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
il.Emit(callOp, method);
postProcessCall?.Invoke(il);
il.Emit(OpCodes.Ret);
return dm.CreateDelegate(delegateType);
}
/// <summary>
/// Compiles a factory function that provides a <see cref="Delegate"/> for each index in an option list.
/// If <paramref name="hasIndexParameter"/> is <see langword="false"/> and <paramref name="postProcessListCall"/> is <see langword="null"/>,
/// a single <see cref="Delegate"/> is compiled once and reused for all indices.
/// </summary>
/// <param name="ownerType">The <see cref="Type"/> that owns the generated <see cref="DynamicMethod"/> and holds the target method.</param>
/// <param name="method">The target <see cref="MethodInfo"/> to invoke.</param>
/// <param name="instanceGetter">
/// The static getter <see cref="MethodInfo"/> used to push the target instance onto the evaluation stack for instance methods,
/// or <see langword="null"/> if <paramref name="method"/> is static.
/// </param>
/// <param name="returnType">The return <see cref="Type"/> of the generated delegate (or <see cref="void"/> for actions).</param>
/// <param name="baseParamTypes">The parameter types accepted by the generated delegate.</param>
/// <param name="delegateType">The concrete delegate <see cref="Type"/> to create.</param>
/// <param name="suffix">A descriptive suffix appended to the generated <see cref="DynamicMethod"/> name for debugging and stack traces.</param>
/// <param name="hasIndexParameter">Whether the target <paramref name="method"/> takes the list <see langword="int"/> index as its final parameter.</param>
/// <param name="postProcessCall">An optional index-independent callback to emit additional IL instructions after <paramref name="method"/> is called.</param>
/// <param name="postProcessListCall">An optional index-dependent callback to emit additional IL instructions after <paramref name="method"/> is called.</param>
/// <returns>A factory function that returns a compiled <see cref="Delegate"/> of type <paramref name="delegateType"/> for a given list index.</returns>
protected static Func<int, Delegate> CompileDynamicListDelegate(
Type ownerType,
MethodInfo method,
MethodInfo? instanceGetter,
Type returnType,
Type[] baseParamTypes,
Type delegateType,
string suffix,
bool hasIndexParameter,
Action<ILGenerator>? postProcessCall = null,
Action<ILGenerator, int>? postProcessListCall = null)
{
if (!hasIndexParameter && postProcessListCall is null)
{
var sharedDelegate = CompileDynamicDelegate(
ownerType,
method,
instanceGetter,
returnType,
baseParamTypes,
delegateType,
suffix,
forwardParameters: baseParamTypes.Length > 0,
postProcessCall: postProcessCall);
return _ => sharedDelegate;
}
return index =>
{
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++)
{
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);
}
if (hasIndexParameter)
il.Emit(OpCodes.Ldc_I4, index);
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
il.Emit(callOp, method);
postProcessCall?.Invoke(il);
postProcessListCall?.Invoke(il, index);
il.Emit(OpCodes.Ret);
return dm.CreateDelegate(delegateType);
};
}
}
@@ -0,0 +1,88 @@
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to set the <see cref="IModdedOption.Setter"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <param name="methodName">The method to get the setter from.</param>
/// <param name="holderType">The type the method is in.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type? holderType = null)
: ModdedOptionMemberAttribute(methodName, holderType, "Setter")
{
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}");
return null;
}
var valueType = option.GetValueType();
if (method.ReturnType != valueType)
{
Error($"{MemberName}'s return type is not {valueType.Name}.");
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)
{
if (ResolveMethod(group, property, out Type type, out var instanceGetter) is not { } method)
{
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (method.ReturnType != valueType)
{
Error($"{MemberName}'s return type is not {valueType.Name}.");
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);
}
}
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to set the <see cref="IModdedOption.Visible"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <param name="memberName">The member to get the visibility function from.</param>
/// <param name="holderType">The type the member is in.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, Type? holderType = null)
: ModdedOptionMemberAttribute(memberName, holderType, "Visibility")
{
private static readonly MethodInfo ModdedOptionBoolValueGetter = typeof(ModdedOption<bool>).GetProperty(nameof(ModdedOption<>.Value))!.GetGetMethod()!;
private static readonly MethodInfo ListBoolIndexerGetter = typeof(IList<bool>).GetProperty("Item")!.GetGetMethod()!;
internal Func<bool>? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (ResolveMember(group, property, out Type type, out var instanceGetter) is not { } member)
{
Error($"Member {MemberName} does not exist in {type.FullName}");
return null;
}
MethodInfo? targetMethod;
Action<ILGenerator>? postProcess = null;
if (member is PropertyInfo vProperty)
{
if (vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
{
postProcess = il => il.Emit(OpCodes.Callvirt, ModdedOptionBoolValueGetter);
}
else if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)))
{
Error($"Property {MemberName} is not a bool or a bool ModdedOption.");
return null;
}
if (vProperty.GetIndexParameters().Length > 0)
{
Error($"Property {MemberName} cannot be an indexer.");
return null;
}
targetMethod = vProperty.GetGetMethod(true);
if (targetMethod is null)
{
Error($"Property {MemberName} does not have a getter.");
return null;
}
}
else if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
{
Error($"Method {MemberName} does not return a bool.");
return null;
}
if (vMethod.GetParameters().Length > 0)
{
Error($"Method {MemberName} has too many parameters.");
return null;
}
targetMethod = vMethod;
}
else
{
Error($"Member {MemberName} of {type.FullName} is not valid for Visible function.");
return null;
}
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)
{
if (ResolveMember(group, property, out Type type, out var instanceGetter) is not { } member)
{
Error($"Member {MemberName} does not exist in {type.FullName}");
return null;
}
MethodInfo? targetMethod;
Action<ILGenerator>? postProcess = null;
Action<ILGenerator, int>? postProcessList = null;
var hasIndexParam = false;
if (member is PropertyInfo vProperty)
{
var isListBool = false;
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
{
isListBool = true;
postProcessList = (il, index) =>
{
il.Emit(OpCodes.Ldc_I4, index);
il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter);
};
}
else if (vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
{
postProcess = il => il.Emit(OpCodes.Callvirt, ModdedOptionBoolValueGetter);
}
else if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)))
{
Error($"Property {MemberName} is not a bool value or indexer, a list of bools, or a bool ModdedOption.");
return null;
}
var indexParams = vProperty.GetIndexParameters();
if (indexParams.Length > 1)
{
Error($"Indexer {MemberName} has too many parameters.");
return null;
}
if (isListBool && indexParams.Length != 0)
{
Error($"Property {MemberName} cannot be an indexer of lists of bools.");
return null;
}
if (indexParams.Length == 1)
{
if (indexParams[0].ParameterType != typeof(int))
{
Error($"Indexer {MemberName}'s parameter is not an int.");
return null;
}
hasIndexParam = true;
}
targetMethod = vProperty.GetGetMethod(true);
if (targetMethod is null)
{
Error($"Property {MemberName} does not have a getter.");
return null;
}
}
else if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
{
Error($"Method {MemberName} does not return a bool.");
return null;
}
var paramList = vMethod.GetParameters();
if (paramList.Length > 1)
{
Error($"Method {MemberName} has too many parameters.");
return null;
}
if (paramList.Length == 1)
{
if (paramList[0].ParameterType != typeof(int))
{
Error($"Method {MemberName}'s parameter is not an int.");
return null;
}
hasIndexParam = true;
}
targetMethod = vMethod;
}
else
{
Error($"Member {MemberName} of {type.FullName} is not valid for Visibility function.");
return null;
}
return CompileDynamicListDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func<bool>), "ListVisibility", hasIndexParam, postProcess, postProcessList);
}
}
@@ -41,10 +41,11 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
/// Attribute for creating an enum option.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <param name="title">The title of the option.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
: ModdedOptionAttribute(title)
where T : Enum
where T : struct, Enum
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
@@ -1,201 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to modify a property with a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="ModdedOptionVisibilityAttribute"/> class.
/// </remarks>
/// <param name="holderType">The type the member is in.</param>
/// <param name="memberName">The member to get the visibility function from.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionVisibilityAttribute(Type? holderType = null, string? memberName = null) : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOptionVisibilityAttribute"/> class.
/// </summary>
/// <param name="memberName">The member to get the visibility function from.</param>
public ModdedOptionVisibilityAttribute(string memberName)
: this(null, memberName)
{
}
internal Func<bool>? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member)
{
Error($"Member {memberName} does not exist in {type.FullName}");
return null;
}
if (member is PropertyInfo vProperty)
{
if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)) &&
!vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
{
Error($"Property {memberName} is not a bool or a bool ModdedOption.");
return null;
}
var indexParams = vProperty.GetIndexParameters();
if (indexParams.Length > 0)
{
Error($"Property {memberName} cannot be an indexer.");
return null;
}
return () => (bool)vProperty.GetValue(instanceGet?.Invoke())!;
}
if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
{
Error($"Method {memberName} does not return a bool.");
return null;
}
var paramList = vMethod.GetParameters();
if (paramList.Length > 0)
{
Error($"Method {memberName} has too many parameters.");
return null;
}
return () => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!;
}
Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
return null;
}
internal Func<int, bool>? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member)
{
Error($"Member {memberName} does not exist in {type.FullName}");
return null;
}
if (member is PropertyInfo vProperty)
{
if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)) &&
!vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)) &&
!vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
{
Error($"Property {memberName} is not a bool value or indexer, a list of bools, or a bool ModdedOption.");
return null;
}
var indexParams = vProperty.GetIndexParameters();
if (indexParams.Length > 1)
{
Error($"Indexer {memberName} has too many parameters.");
return null;
}
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
{
if (indexParams.Length != 0)
{
Error($"Property {memberName} cannot be an indexer of lists of bools.");
return null;
}
return i => ((IList<bool>)vProperty.GetValue(instanceGet?.Invoke())!)[i];
}
if (indexParams.Length == 0)
{
return _ => (bool)vProperty.GetValue(instanceGet?.Invoke())!;
}
if (indexParams[0].ParameterType != typeof(int))
{
Error($"Indexer {memberName}'s parameter is not an int.");
return null;
}
return i => (bool)vProperty.GetValue(instanceGet?.Invoke(), [i])!;
}
if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
{
Error($"Method {memberName} does not return a bool.");
return null;
}
var paramList = vMethod.GetParameters();
if (paramList.Length > 1)
{
Error($"Method {memberName} has too many parameters.");
return null;
}
if (paramList.Length == 0)
{
return _ => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!;
}
if (paramList[0].ParameterType != typeof(int))
{
Error($"Method {memberName}'s parameter is not an int.");
return null;
}
return i => (bool)vMethod.Invoke(instanceGet?.Invoke(), [i])!;
}
Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
return null;
}
[SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")]
private MemberInfo? GetVisibilityMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out Func<object>? instanceGet)
{
Type groupType = group.GetType();
BindingFlags flags = BindingFlags.Static | BindingFlags.Public;
instanceGet = null;
type = holderType ?? groupType;
memberName ??= $"{property.Name}Visible";
if (type.IsAssignableTo(typeof(AbstractOptionGroup)))
{
flags |= BindingFlags.Instance;
if (type == groupType)
{
flags |= BindingFlags.NonPublic;
instanceGet = () => group;
}
else
{
var instanceProp = typeof(OptionGroupSingleton<>).MakeGenericType(type)
.GetProperty(nameof(OptionGroupSingleton<>.Instance))!;
instanceGet = () => instanceProp.GetValue(null)!;
}
}
MemberInfo? member = type.GetMember(memberName, flags).FirstOrDefault();
if (instanceGet != null)
{
MethodInfo? method = (member as PropertyInfo)?.GetGetMethod()
?? member as MethodInfo;
if (method != null && method.IsStatic)
{
instanceGet = null;
}
}
return member;
}
}
@@ -0,0 +1,39 @@
using System;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for a <see cref="ModdedStringOption"/>.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="values">An option list of string value.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedStringOptionAttribute(
string title,
string[] values)
: ModdedOptionAttribute(title)
{
internal override IModdedOption? CreateOption(object? value, PropertyInfo property)
{
var option = new ModdedStringOption(Title.Translate(), (string)(value ?? string.Empty), values);
return option;
}
/// <inheritdoc />
public override object GetValue()
{
return HolderOption is ModdedStringOption opt
? (object)opt.Value
: throw new InvalidOperationException($"Holder option for {Title} is not a ModdedStringOption.");
}
/// <inheritdoc />
public override void SetValue(object value)
{
var opt = HolderOption as ModdedStringOption;
opt?.SetValue((string)value);
}
}
@@ -8,6 +8,9 @@ namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for creating a list of enum options.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="enumType">The <see cref="Enum"/> type.</param>
/// <param name="values">An option list of string values to use in place of the <see langword="enum"/> name.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]? values = null)
: ModdedOptionListAttribute(title)
@@ -39,10 +42,12 @@ public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]
/// Attribute for creating a list of enum options.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <param name="title">The option's title.</param>
/// <param name="values">The option's possible values.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = null)
: ModdedOptionListAttribute(title)
where T : Enum
where T : struct, Enum
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
@@ -7,8 +7,15 @@ using MiraAPI.Utilities;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// A number option attribute for the modded options system.
/// Attribute for creating a list of number options.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Flag indicating whether 0 means infinity.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedNumberOptionListAttribute(
string title,
@@ -0,0 +1,42 @@
using System;
using System.Collections;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for creating a list of string options.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="values">An option list of string value.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedStringOptionListAttribute(
string title,
string[] values)
: ModdedOptionListAttribute(title)
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
return new ModdedOptionList<ModdedStringOption>(value.Count, idx =>
{
return new ModdedStringOption(Title.Translate(), (string)(value[idx] ?? string.Empty), values);
});
}
/// <inheritdoc />
public override void SetValue(int idx, object value)
{
var opt = HolderOptionList?[idx] as ModdedStringOption;
opt?.SetValue((string)value);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
return HolderOptionList?[idx] is ModdedStringOption opt
? (object)opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedStringOption");
}
}
+21
View File
@@ -57,6 +57,21 @@ public interface IModdedOption
/// </summary>
Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets a getter event for the option, allowing for dynamic values based on external circumstances.
/// </summary>
Delegate? Getter { get; set; }
/// <summary>
/// Gets or sets a setter event for the option, allowing for dynamic value changes based on external circumstances.
/// </summary>
Delegate? Setter { get; set; }
/// <summary>
/// Gets or sets the event that is invoked when the value of the option changes.
/// </summary>
Delegate? ChangedEvent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option should be included with presets.
/// </summary>
@@ -114,6 +129,12 @@ public interface IModdedOption
/// </summary>
/// <param name="presetConfig">The <see cref="ConfigFile"/> representing the preset configuration.</param>
void LoadFromPreset(ConfigFile presetConfig);
/// <summary>
/// Gets the type of the value that the option represents.
/// </summary>
/// <returns>The type of the option value.</returns>
Type GetValueType();
}
/// <summary>
+75 -14
View File
@@ -26,6 +26,7 @@ public static class ModdedOptionsManager
{
private static readonly Dictionary<PropertyInfo, PropertyOptionAttribute> OptionAttributes = [];
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
private static readonly HashSet<Type> PatchedListTypes = [];
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
internal static readonly Dictionary<OptionBehaviour, ModdedStringOption> CreatedStringOptions = [];
@@ -181,6 +182,27 @@ public static class ModdedOptionsManager
option.Visible = visibilityFunc;
}
var getterAtr = property.GetCustomAttribute<ModdedOptionGetterAttribute>();
var getterFunc = getterAtr?.GetGetter(group, property, option);
if (getterFunc != null)
{
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;
}
RegisterOption(option, group, property.Name, pluginInfo);
}
@@ -255,28 +277,61 @@ public static class ModdedOptionsManager
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
var listIndex = propertyList.GetType().GetProperty("Item")!;
var listType = propertyList.GetType();
if (PatchedListTypes.Add(listType))
{
var listIndex = listType.GetProperty("Item")!;
var listSetterOriginal = listIndex.GetSetMethod();
var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch));
var listGetterOriginal = listIndex.GetSetMethod();
var listGetterOriginal = listIndex.GetGetMethod();
var listGetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch));
}
OptionAttributes.Add(property, attribute);
attribute.HolderOptionList = optionList;
attribute.Value = propertyVal;
var valueType = property.PropertyType.GetGenericArguments()[0];
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>();
var visibilityFunc = visibilityAttr?.GetListVisibility(group, property);
for (int i = 0; i < optionList.Count; i++)
var visibilityFactory = visibilityAttr?.GetListVisibility(group, property);
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);
for (var i = 0; i < optionList.Count; i++)
{
var option = optionList[i];
if (visibilityFunc != null)
if (visibilityFactory != null)
{
option.Visible = () => visibilityFunc(i);
option.Visible = (Func<bool>)visibilityFactory(i);
}
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);
@@ -350,7 +405,7 @@ public static class ModdedOptionsManager
/// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention.")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Harmony naming convention.")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
{
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
@@ -364,7 +419,7 @@ public static class ModdedOptionsManager
/// <param name="__result">The result of the property getter.</param>
/// <returns><see langword="false"/> so the original getter gets skipped.</returns>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Harmony naming convention.")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
{
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
@@ -382,9 +437,10 @@ public static class ModdedOptionsManager
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static void PropertyListSetterPatch(object __instance, int index, object value)
{
var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
attribute.SetValue(index, value);
var match = OptionAttributes.Values
.OfType<ModdedOptionListAttribute>()
.FirstOrDefault(list => ReferenceEquals(list.Value, __instance));
match?.SetValue(index, value);
}
/// <summary>
@@ -398,9 +454,14 @@ public static class ModdedOptionsManager
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static bool PropertyListGetterPatch(object __instance, int index, ref object __result)
{
var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
__result = attribute.GetValue(index);
var match = OptionAttributes.Values
.OfType<ModdedOptionListAttribute>()
.FirstOrDefault(list => ReferenceEquals(list.Value, __instance));
if (match is null)
return true;
__result = match.GetValue(index);
return false;
}
}
@@ -113,7 +113,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// </summary>
/// <typeparam name="T">The <see cref="Enum"/> type.</typeparam>
public class ModdedEnumOption<T> : ModdedOption<T>
where T : Enum
where T : struct, Enum
{
/// <summary>
/// Gets the string values of the <see langword="enum"/>.
@@ -130,7 +130,7 @@ public class ModdedEnumOption<T> : ModdedOption<T>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
{
Values = values ?? Enum.GetNames(typeof(T));
Values = values ?? Enum.GetNames<T>();
var data = ScriptableObject.CreateInstance<StringGameSetting>();
data.Title = StringName;
@@ -151,6 +151,10 @@ public class ModdedEnumOption<T> : ModdedOption<T>
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
// Changing the button text to make them more distinct from number options
stringOption.MinusBtn.ChangeButtonText("<");
stringOption.PlusBtn.ChangeButtonText(">");
// SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName;
stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray<StringNames>(0);
@@ -176,13 +180,13 @@ public class ModdedEnumOption<T> : ModdedOption<T>
/// <inheritdoc />
public override void HandleNetData(byte[] data)
{
SetValue((T)Enum.Parse(typeof(T), Encoding.Unicode.GetString(data)));
SetValue(Enum.Parse<T>(Encoding.Unicode.GetString(data)));
}
/// <inheritdoc />
public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return (T)Enum.Parse(typeof(T), optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo));
return Enum.Parse<T>(optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo));
}
/// <inheritdoc />
+59 -15
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using BepInEx.Configuration;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
@@ -43,16 +44,32 @@ public abstract class ModdedOption<T> : IModdedOption
/// <summary>
/// Gets or sets the value of the option.
/// </summary>
public T Value { get; protected set; }
public T Value
{
get => Getter != null ? Getter(_value) : _value;
protected set
{
if (Setter != null)
value = Setter(value, _value);
_value = value;
}
}
private T _value;
/// <summary>
/// Gets the default value of the option.
/// </summary>
public T DefaultValue { get; }
/// <summary>
/// Gets or sets the event that is invoked when the value of the option changes.
/// </summary>
/// <inheritdoc cref="IModdedOption.Getter"/>
public Func<T, T>? Getter { get; set; }
/// <inheritdoc cref="IModdedOption.Setter"/>
public Func<T, T, T>? Setter { get; set; }
/// <inheritdoc cref="IModdedOption.ChangedEvent"/>
public Action<T>? ChangedEvent { get; set; }
/// <inheritdoc />
@@ -67,6 +84,33 @@ public abstract class ModdedOption<T> : IModdedOption
/// <inheritdoc />
public ConfigDefinition? ConfigDefinition { get; set; }
/// <inheritdoc />
public AbstractOptionGroup ParentGroup { get; set; }
/// <inheritdoc />
public virtual OptionNotifConfiguration Configuration => new(ParentGroup);
/// <inheritdoc />
Delegate? IModdedOption.Getter
{
get => Getter;
set => Getter = value as Func<T, T>;
}
/// <inheritdoc />
Delegate? IModdedOption.Setter
{
get => Setter;
set => Setter = value as Func<T, T, T>;
}
/// <inheritdoc />
Delegate? IModdedOption.ChangedEvent
{
get => ChangedEvent;
set => ChangedEvent = value as Action<T>;
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
/// </summary>
@@ -96,25 +140,25 @@ public abstract class ModdedOption<T> : IModdedOption
/// <param name="sendRpc">Whether to send the value to other players.</param>
public void SetValue(T newValue, bool sendRpc = true)
{
var oldVal = Value;
var oldVal = _value;
Value = newValue;
if (Value?.Equals(oldVal) == false)
if (!EqualityComparer<T>.Default.Equals(_value, oldVal))
{
ChangedEvent?.Invoke(Value);
ChangedEvent?.Invoke(_value);
}
if (sendRpc && AmongUsClient.Instance.AmHost)
{
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
{
entry.Value = Value;
entry.Value = _value;
}
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true);
}
OnValueChanged(newValue);
OnValueChanged(_value);
}
/// <inheritdoc />
@@ -149,6 +193,12 @@ public abstract class ModdedOption<T> : IModdedOption
}
}
/// <inheritdoc />
public Type GetValueType()
{
return typeof(T);
}
/// <inheritdoc />
public abstract float GetFloatData();
@@ -188,10 +238,4 @@ public abstract class ModdedOption<T> : IModdedOption
{
return option.Value;
}
/// <inheritdoc />
public AbstractOptionGroup ParentGroup { get; set; }
/// <inheritdoc />
public virtual OptionNotifConfiguration Configuration => new(ParentGroup);
}
@@ -50,11 +50,13 @@ public class ModdedOptionList<T> : IModdedOptionList<T>
return Options.GetEnumerator();
}
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
IEnumerator IEnumerable.GetEnumerator()
{
return Options.GetEnumerator();
}
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
IEnumerator<IModdedOption> IEnumerable<IModdedOption>.GetEnumerator()
{
return ((IEnumerable<IModdedOption>)Options).GetEnumerator();
@@ -67,5 +69,6 @@ public class ModdedOptionList<T> : IModdedOptionList<T>
/// <returns>option of type <typeparamref name="T"/>.</returns>
public T this[int idx] => Options[idx];
/// <inheritdoc cref="IReadOnlyList{T}.this"/>
IModdedOption IReadOnlyList<IModdedOption>.this[int index] => this[index];
}
@@ -25,6 +25,28 @@ public class ModdedStringOption : ModdedOption<string>
/// </summary>
public StringNames[] StringNameValues { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedStringOption"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as a string.</param>
/// <param name="values">An option list of string values to use.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
{
Values = values;
Data = ScriptableObject.CreateInstance<StringGameSetting>();
var data = (StringGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.String;
StringNameValues = [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)];
data.Values = StringNameValues;
data.Index = GetIndex(Value);
}
/// <summary>
/// Gets the string's index.
/// </summary>
@@ -61,62 +83,38 @@ public class ModdedStringOption : ModdedOption<string>
StringNameValues = [.. newStrVals];
}
if (Data != null)
if (Data == null)
{
Error("Data is null.");
return;
}
var data = (StringGameSetting)Data;
data.Values = StringNameValues;
data.Index = GetIndex(Value);
if (OptionBehaviour != null)
Warning("Data is valid");
if (OptionBehaviour == null)
{
Error("OptionBehaviour is null.");
return;
}
var strOpt = OptionBehaviour.TryCast<StringOption>();
if (strOpt != null)
Warning("OptionBehaviour is valid");
if (strOpt == null)
{
Error("StringOption is null.");
return;
}
strOpt.Values = StringNameValues;
strOpt.Value = GetIndex(Value);
Warning("StringOption is valid");
}
else
{
Error("StringOption is null.");
}
Warning("OptionBehaviour is valid");
}
else
{
Error("OptionBehaviour is null.");
}
Warning("Data is valid");
}
else
{
Error("Data is null.");
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedStringOption"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as a string.</param>
/// <param name="values">An option list of string values to use.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
{
Values = values;
Data = ScriptableObject.CreateInstance<StringGameSetting>();
var data = (StringGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.String;
StringNameValues = [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)];
data.Values = StringNameValues;
data.Index = GetIndex(Value);
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
@@ -128,6 +126,10 @@ public class ModdedStringOption : ModdedOption<string>
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
// Changing the button text to make them more distinct from number options
stringOption.MinusBtn.ChangeButtonText("<");
stringOption.PlusBtn.ChangeButtonText(">");
// SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName;
stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray<StringNames>(0);
+6
View File
@@ -67,6 +67,7 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
internal static ConfigEntry<SortingGroups> WikiSortingGroup { get; private set; }
internal static ConfigEntry<SortingMethod> WikiSortingMethod { get; private set; }
internal static ConfigEntry<SortingFilter> WikiSortingFilter { get; private set; }
internal static ConfigEntry<string> DistanceSuffix { get; private set; }
/// <inheritdoc />
public override void Load()
@@ -86,6 +87,11 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
"WikiSortingFilter",
SortingFilter.AllRoles,
"Determines what roles / modifiers are displayed at once.");
DistanceSuffix = Config.Bind(
"Display",
"Distance",
"unit(s)",
"Dictates the unit of distance used in numeric option displays representing a distance.");
Harmony.PatchAll();
RoleOptionsCollectionPatch.PatchRoleMethods(Harmony);
UiResetEvents.Initialize();
@@ -51,6 +51,7 @@ public static class SettingPatches
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
MiraNumberSuffixes.Distance => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + MiraApiPlugin.DistanceSuffix.Value,
_ => TranslationController.Instance.GetString(
StringNames.GameSecondsAbbrev,
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
@@ -68,6 +69,7 @@ public static class SettingPatches
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
MiraNumberSuffixes.Distance => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + MiraApiPlugin.DistanceSuffix.Value,
_ => TranslationController.Instance.GetString(
StringNames.GameSecondsAbbrev,
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
-3
View File
@@ -10,9 +10,6 @@ namespace MiraAPI.Presets;
/// <summary>
/// Represents a preset of game options that can be applied to the game.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="OptionPreset"/> class with the specified name and configuration file.
/// </remarks>
/// <param name="name">The name of the preset.</param>
/// <param name="plugin">The plugin associated with the preset.</param>
/// <param name="presetConfig">The configuration file for the preset.</param>
+5
View File
@@ -24,4 +24,9 @@ public enum MiraNumberSuffixes
/// Percentage suffix (%).
/// </summary>
Percent,
/// <summary>
/// Distance suffix (m).
/// </summary>
Distance,
}