Merge pull request #112 from All-Of-Us-Mods/options-improvements

More additions to the options system
This commit is contained in:
AS authored and GitHub committed 2026-10-18 00:37:36 -04:00
commit fc7a1dc831
45 files changed
+1884 -511

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 # S1244: Floating point numbers should not be tested for equality
dotnet_diagnostic.S1244.severity = none dotnet_diagnostic.S1244.severity = none
# S2325: Methods and properties that don't access instance data should be static
dotnet_diagnostic.S2325.severity = none
+3
View File
@@ -2,3 +2,6 @@
# SA1602: Enumeration items should be documented. # SA1602: Enumeration items should be documented.
dotnet_diagnostic.SA1602.severity = none dotnet_diagnostic.SA1602.severity = none
# SA1601: Partial elements should be documented
dotnet_diagnostic.SA1601.severity = none
+52 -12
View File
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Diagnostics.CodeAnalysis;
using MiraAPI.GameOptions; using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.Attributes;
using MiraAPI.Utilities; using MiraAPI.Utilities;
@@ -6,6 +6,9 @@ using UnityEngine;
namespace MiraAPI.Example.Options; 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 class ExampleOptions : AbstractOptionGroup
{ {
public override string GroupName => "options.exampleOptions1"; public override string GroupName => "options.exampleOptions1";
@@ -13,27 +16,64 @@ public class ExampleOptions : AbstractOptionGroup
public override bool ShowInMatchInfoGuide => true; public override bool ShowInMatchInfoGuide => true;
[ModdedToggleOption("options.exampleOptions1.toggleOpt1")] [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; 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)] [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; public float NumberOpt { get; set; } = 4f;
[ModdedEnumOption("options.exampleOptions1.bestApi", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])] [ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Distance)]
public BestApi Opt { get; set; } = BestApi.MiraAPI; public int NumberOpt2 { get; set; } = 4; // Can also be int!
[ModdedEnumOption("options.exampleOptions1.bestApi", typeof(Api), ["Mira API", "Mitochondria", "Reactor"])]
public Api BestApi { get; set; } = Api.MiraAPI;
[ModdedEnumOption<Api>("options.exampleOptions1.worstApi", ["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 WorstApi { get; set; } = Api.MiraAPI;
[ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])] [ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])]
public List<ValueState> ValueStates { get; } = public ModdedArray<ValueState> ValueStates { get; set; } = [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
[ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
[ModdedPlayerOption("options.exampleOptions1.players")]
public NetworkedPlayerInfo PlayerSelected { get; set; }
public bool ToggleOpt1Visibility()
{
return true;
}
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 BestApi public enum Api
{ {
MiraAPI, MiraAPI,
Mitochondria, Mitochondria,
+2 -2
View File
@@ -7,9 +7,7 @@ namespace MiraAPI.Example.Options;
public class ExampleOptions2 : AbstractOptionGroup public class ExampleOptions2 : AbstractOptionGroup
{ {
public override string GroupName => "options.exampleOptions2"; 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 uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
public override MenuCategory ParentMenu => MenuCategory.CustomTwo; public override MenuCategory ParentMenu => MenuCategory.CustomTwo;
public override bool ShowInMatchInfoGuide => true; public override bool ShowInMatchInfoGuide => true;
@@ -28,6 +26,8 @@ public class ExampleOptions2 : AbstractOptionGroup
public ModdedEnumOption<TestingData> EnumOpt { get; } = new("options.exampleOptions2.enumOpt", 0) public ModdedEnumOption<TestingData> EnumOpt { get; } = new("options.exampleOptions2.enumOpt", 0)
{ {
ChangedEvent = x => Logger<ExamplePlugin>.Info($"changed Enum Opt to {x}"), ChangedEvent = x => Logger<ExamplePlugin>.Info($"changed Enum Opt to {x}"),
Getter = currentValue => currentValue,
Setter = (newValue, oldValue) => newValue,
}; };
} }
@@ -47,6 +47,8 @@ Eliminate every other faction.</string>
<string name="options.exampleOptions1.toggleOpt2">Toggle Opt 2</string> <string name="options.exampleOptions1.toggleOpt2">Toggle Opt 2</string>
<string name="options.exampleOptions1.numberOpt">Number Opt</string> <string name="options.exampleOptions1.numberOpt">Number Opt</string>
<string name="options.exampleOptions1.bestApi">Best API</string> <string name="options.exampleOptions1.bestApi">Best API</string>
<string name="options.exampleOptions1.worstApi">Worst API</string>
<string name="options.exampleOptions1.players">Choose A Player</string>
<string name="options.exampleOptions2">Example Options 2</string> <string name="options.exampleOptions2">Example Options 2</string>
<string name="options.exampleOptions2.youMustChoseAPlayer">YOU MUST CHOSE A PLAYER</string> <string name="options.exampleOptions2.youMustChoseAPlayer">YOU MUST CHOSE A PLAYER</string>
<string name="options.exampleOptions2.ehWhatever">Eh whatever</string> <string name="options.exampleOptions2.ehWhatever">Eh whatever</string>
+2 -1
View File
@@ -6,6 +6,7 @@ using System.Linq;
using Il2CppInterop.Runtime.Attributes; using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Roles; using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using UnityEngine; using UnityEngine;
@@ -55,7 +56,7 @@ public abstract class AbstractOptionGroup
/// <summary> /// <summary>
/// Gets the function that determines whether the group should be visible or not. /// Gets the function that determines whether the group should be visible or not.
/// </summary> /// </summary>
public virtual Func<bool> GroupVisible => () => true; public virtual Func<bool> GroupVisible => DelegateUtilities.AlwaysTrue;
/// <summary> /// <summary>
/// Gets a value indicating whether this group's options are shown in the Match Info Guide. /// Gets a value indicating whether this group's options are shown in the Match Info Guide.
@@ -0,0 +1,40 @@
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 var type, out var instanceGetter) is not { } method)
return null;
var valueType = option.GetValueType();
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)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
return null;
if (!ValidateListSignature(method, typeof(void), out var hasIndex, valueType))
return null;
var delegateType = typeof(Action<>).MakeGenericType(valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "ListChanged", hasIndex);
}
}
@@ -0,0 +1,40 @@
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 var type, out var instanceGetter) is not { } method)
return null;
var valueType = option.GetValueType();
if (!ValidateSignature(method, valueType, valueType))
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 var type, out var instanceGetter) is not { } method)
return null;
if (!ValidateListSignature(method, valueType, out var hasIndex, valueType))
return null;
var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "ListGetter", hasIndex);
}
}
@@ -0,0 +1,366 @@
using System;
using System.Collections.Generic;
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
{
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>
/// <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)
return member;
bool isStatic;
if (member is FieldInfo field)
{
isStatic = field.IsStatic;
}
else
{
var method = (member as PropertyInfo)?.GetGetMethod(true) ?? member as MethodInfo;
isStatic = method is { IsStatic: true };
}
if (isStatic)
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)
{
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();
if (!method.IsStatic && instanceGetter != null)
il.Emit(OpCodes.Call, instanceGetter);
if (forwardParameters)
{
for (byte i = 0; i < paramTypes.Length; i++)
{
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(loadOp);
}
}
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
il.Emit(callOp, method);
postProcessCall?.Invoke(il);
il.Emit(OpCodes.Ret);
var compiledDelegate = dm.CreateDelegate(delegateType);
if (postProcessCall == null)
DelegateCache[method] = compiledDelegate;
return compiledDelegate;
}
/// <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>? postProcessListCall = null)
{
if (!hasIndexParameter && postProcessListCall is null)
{
var sharedDelegate = CompileDynamicDelegate(
ownerType,
method,
instanceGetter,
returnType,
baseParamTypes,
delegateType,
suffix,
baseParamTypes.Length > 0,
postProcessCall);
return _ => sharedDelegate;
}
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 argPos = (byte)(i + 1);
var loadOp = argPos switch
{
1 => OpCodes.Ldarg_1,
2 => OpCodes.Ldarg_2,
3 => OpCodes.Ldarg_3,
_ => OpCodes.Ldarg_S,
};
if (loadOp == OpCodes.Ldarg_S)
il.Emit(loadOp, argPos);
else
il.Emit(loadOp);
}
if (hasIndexParameter)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, IndexClosureField);
}
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
il.Emit(callOp, method);
postProcessCall?.Invoke(il);
postProcessListCall?.Invoke(il);
il.Emit(OpCodes.Ret);
return index => dm.CreateDelegate(delegateType, new DelegateIndexClosure(index));
}
}
@@ -0,0 +1,40 @@
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 var type, out var instanceGetter) is not { } method)
return null;
var valueType = option.GetValueType();
if (!ValidateSignature(method, valueType, valueType, valueType))
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 var type, out var instanceGetter) is not { } method)
return null;
if (!ValidateListSignature(method, valueType, out var hasIndex, valueType, valueType))
return null;
var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "ListSetter", hasIndex);
}
}
@@ -0,0 +1,253 @@
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 if (member is FieldInfo field)
{
if (field.FieldType != typeof(bool))
{
Error($"Field {MemberName} is not a bool.");
return null;
}
var dynamicMethod = new DynamicMethod($"GetVisibility_{type.Name}_{field.Name}", typeof(bool), Type.EmptyTypes, type, true);
var il = dynamicMethod.GetILGenerator();
if (instanceGetter != null)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, instanceGetter);
il.Emit(OpCodes.Ldfld, field);
}
else
{
il.Emit(OpCodes.Ldsfld, field);
}
il.Emit(OpCodes.Ret);
return dynamicMethod.CreateDelegate<Func<bool>>();
}
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>? postProcessList = null;
var hasIndexParam = false;
if (member is PropertyInfo vProperty)
{
var isListBool = false;
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
{
isListBool = true;
postProcessList = il =>
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, IndexClosureField);
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 if (member is FieldInfo field)
{
if (field.FieldType.IsAssignableTo(typeof(IList<bool>)))
{
postProcess = il =>
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter);
};
}
else if (field.FieldType != typeof(bool))
{
Error($"Field {MemberName} is not a bool or a list of bools.");
return null;
}
var dynamicMethod = new DynamicMethod($"GetListVisibility_{type.Name}_{field.Name}", typeof(bool), [typeof(int)], type, true);
var il = dynamicMethod.GetILGenerator();
if (instanceGetter != null)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, instanceGetter);
il.Emit(OpCodes.Ldfld, field);
}
else
{
il.Emit(OpCodes.Ldsfld, field);
}
postProcess?.Invoke(il);
il.Emit(OpCodes.Ret);
var func = dynamicMethod.CreateDelegate<Func<int, bool>>();
return index => () => func(index);
}
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);
}
}
@@ -1,7 +1,6 @@
using System; using System;
using System.Reflection; using System.Reflection;
using MiraAPI.GameOptions.OptionTypes; using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes; namespace MiraAPI.GameOptions.Attributes;
@@ -17,7 +16,7 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
{ {
internal override IModdedOption CreateOption(object? value, PropertyInfo property) internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{ {
var opt = new ModdedEnumOption(Title.Translate(), (int)(value ?? 0), enumType, values); var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values);
return opt; return opt;
} }
@@ -33,7 +32,7 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
{ {
return HolderOption is ModdedEnumOption opt return HolderOption is ModdedEnumOption opt
? Enum.ToObject(enumType, opt.Value) ? Enum.ToObject(enumType, opt.Value)
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {enumType.FullName} is not a ModdedEnumOption");
} }
} }
@@ -41,10 +40,12 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
/// Attribute for creating an enum option. /// Attribute for creating an enum option.
/// </summary> /// </summary>
/// <typeparam name="T">The enum type.</typeparam> /// <typeparam name="T">The enum type.</typeparam>
/// <param name="title">The title of the option.</param>
/// <param name="values">An option list of string values to use in place of the <see langword="enum"/> name.</param>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null) public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
: ModdedOptionAttribute(title) : ModdedOptionAttribute(title)
where T : Enum where T : struct, Enum
{ {
internal override IModdedOption CreateOption(object? value, PropertyInfo property) internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{ {
@@ -64,6 +65,6 @@ public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
{ {
return HolderOption is ModdedEnumOption<T> opt return HolderOption is ModdedEnumOption<T> opt
? opt.Value ? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption"); : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).Name}>");
} }
} }
@@ -1,7 +1,6 @@
using System; using System;
using System.Reflection; using System.Reflection;
using MiraAPI.GameOptions.OptionTypes; using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
using MiraAPI.Utilities; using MiraAPI.Utilities;
namespace MiraAPI.GameOptions.Attributes; namespace MiraAPI.GameOptions.Attributes;
@@ -13,11 +12,35 @@ namespace MiraAPI.GameOptions.Attributes;
/// <param name="min">The minimum value.</param> /// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param> /// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param> /// <param name="increment">The increment.</param>
/// <param name="zeroBehavior">Determines what is shown for zero.</param>
/// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
/// <param name="suffixType">The suffix type.</param> /// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param> /// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Flag indicating whether 0 means infinity.</param> /// <param name="halfIncrements">Whether increments can be split in half (using Shift).</param>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class ModdedNumberOptionAttribute( public class ModdedNumberOptionAttribute(
string title,
float min,
float max,
float increment,
string zeroBehavior,
string negativeBehavior,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool halfIncrements = false)
: ModdedOptionAttribute(title)
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOptionAttribute"/> class.
/// </summary>
/// <param name="title">The title of the option.</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="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
public ModdedNumberOptionAttribute(
string title, string title,
float min, float min,
float max, float max,
@@ -25,33 +48,57 @@ public class ModdedNumberOptionAttribute(
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null, string? formatString = null,
bool zeroInfinity = false) bool zeroInfinity = false)
: ModdedOptionAttribute(title) : this(title, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f)
{ {
}
internal override IModdedOption CreateOption(object? value, PropertyInfo property) internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{ {
var final = value switch
{
null => min + increment,
int integer => integer,
_ => (float)value,
};
return new ModdedNumberOption( return new ModdedNumberOption(
Title.Translate(), Title,
(float)(value ?? min + increment), final,
min, min,
max, max,
increment, increment,
zeroBehavior,
negativeBehavior,
suffixType, suffixType,
formatString, formatString,
zeroInfinity); halfIncrements);
} }
/// <inheritdoc /> /// <inheritdoc />
public override void SetValue(object value) public override void SetValue(object value)
{ {
var opt = HolderOption as ModdedNumberOption; if (HolderOption is not ModdedNumberOption opt)
opt?.SetValue((float)value); return;
var final = value switch
{
int integer => integer,
_ => (float)value,
};
opt.SetValue(final);
} }
/// <inheritdoc /> /// <inheritdoc />
public override object GetValue() public override object GetValue()
{ {
return HolderOption is ModdedNumberOption opt if (HolderOption is not ModdedNumberOption opt)
? (object)opt.Value throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption");
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption");
var value = opt.Value;
if (ValueType == typeof(int))
return (int)Math.Round(value);
return value;
} }
} }
@@ -13,6 +13,7 @@ namespace MiraAPI.GameOptions.Attributes;
public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : PropertyOptionAttribute public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : PropertyOptionAttribute
{ {
internal IModdedOption? HolderOption { get; set; } internal IModdedOption? HolderOption { get; set; }
internal Type ValueType { get; set; }
/// <summary> /// <summary>
/// Gets the title of the option. /// Gets the title of the option.
@@ -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,49 @@
using System;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for a <see cref="ModdedPlayerOption"/>.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="allowNone">Whether the option includes a none option. If None is selected, value will return -1.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedPlayerOptionAttribute(
string title,
bool allowNone = true)
: ModdedOptionAttribute(title)
{
internal override IModdedOption? CreateOption(object? value, PropertyInfo property)
{
var option = new ModdedPlayerOption(Title, allowNone);
return option;
}
/// <inheritdoc />
public override object GetValue()
{
if (HolderOption is not ModdedPlayerOption opt)
throw new InvalidOperationException($"Holder option for {Title} is not a ModdedPlayerOption.");
if (ValueType == typeof(int))
return opt.Value;
return opt.SelectedPlayer!;
}
/// <inheritdoc />
public override void SetValue(object value)
{
if (HolderOption is not ModdedPlayerOption opt)
return;
var final = value switch
{
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0),
_ => (int)value,
};
opt?.SetValue(final);
}
}
@@ -0,0 +1,38 @@
using System;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
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, (string)(value ?? string.Empty), values);
return option;
}
/// <inheritdoc />
public override object GetValue()
{
return HolderOption is ModdedStringOption opt
? 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);
}
}
@@ -1,7 +1,6 @@
using System; using System;
using System.Reflection; using System.Reflection;
using MiraAPI.GameOptions.OptionTypes; using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes; namespace MiraAPI.GameOptions.Attributes;
@@ -9,12 +8,27 @@ namespace MiraAPI.GameOptions.Attributes;
/// Attribute for a <see cref="ModdedToggleOption"/>. /// Attribute for a <see cref="ModdedToggleOption"/>.
/// </summary> /// </summary>
/// <param name="title">The option title.</param> /// <param name="title">The option title.</param>
/// <param name="on">What the option displays when toggled on.</param>
/// <param name="off">What the option displays when toggled off.</param>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(title) public class ModdedToggleOptionAttribute(
string title,
string on,
string off)
: ModdedOptionAttribute(title)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOptionAttribute"/> class.
/// </summary>
/// <param name="title">The option title.</param>
public ModdedToggleOptionAttribute(string title)
: this(title, "On", "Off")
{
}
internal override IModdedOption CreateOption(object? value, PropertyInfo property) internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{ {
var toggleOpt = new ModdedToggleOption(Title.Translate(), (bool)(value ?? false)); var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), on, off);
return toggleOpt; return toggleOpt;
} }
@@ -29,7 +43,7 @@ public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(t
public override object GetValue() public override object GetValue()
{ {
return HolderOption is ModdedToggleOption opt return HolderOption is ModdedToggleOption opt
? (object)opt.Value ? opt.Value
: throw new InvalidOperationException($"Holder option for {Title} is not a ModdedToggleOption."); : throw new InvalidOperationException($"Holder option for {Title} is not a ModdedToggleOption.");
} }
} }
@@ -8,6 +8,9 @@ namespace MiraAPI.GameOptions.Attributes;
/// <summary> /// <summary>
/// Attribute for creating a list of enum options. /// Attribute for creating a list of enum options.
/// </summary> /// </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)] [AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]? values = null) public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]? values = null)
: ModdedOptionListAttribute(title) : ModdedOptionListAttribute(title)
@@ -31,7 +34,7 @@ public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]
{ {
return HolderOptionList?[idx] is ModdedEnumOption opt return HolderOptionList?[idx] is ModdedEnumOption opt
? Enum.ToObject(enumType, opt.Value) ? Enum.ToObject(enumType, opt.Value)
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); : throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {enumType.FullName} is not a ModdedEnumOption");
} }
} }
@@ -39,10 +42,12 @@ public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]
/// Attribute for creating a list of enum options. /// Attribute for creating a list of enum options.
/// </summary> /// </summary>
/// <typeparam name="T">The enum type.</typeparam> /// <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)] [AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = null) public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = null)
: ModdedOptionListAttribute(title) : ModdedOptionListAttribute(title)
where T : Enum where T : struct, Enum
{ {
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{ {
@@ -63,6 +68,6 @@ public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = n
{ {
return HolderOptionList?[idx] is ModdedEnumOption<T> opt return HolderOptionList?[idx] is ModdedEnumOption<T> opt
? opt.Value ? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption"); : throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).FullName}>");
} }
} }
@@ -7,10 +7,41 @@ using MiraAPI.Utilities;
namespace MiraAPI.GameOptions.Attributes; namespace MiraAPI.GameOptions.Attributes;
/// <summary> /// <summary>
/// A number option attribute for the modded options system. /// Attribute for creating a list of number options.
/// </summary> /// </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="zeroBehavior">Determines what is shown for zero.</param>
/// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="halfIncrements">Whether increments can be split in half (using Shift).</param>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class ModdedNumberOptionListAttribute( public class ModdedNumberOptionListAttribute(
string title,
float min,
float max,
float increment,
string zeroBehavior,
string negativeBehavior,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool halfIncrements = false)
: ModdedOptionListAttribute(title)
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOptionListAttribute"/> class.
/// </summary>
/// <param name="title">The title of the option.</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="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
public ModdedNumberOptionListAttribute(
string title, string title,
float min, float min,
float max, float max,
@@ -18,36 +49,60 @@ public class ModdedNumberOptionListAttribute(
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null, string? formatString = null,
bool zeroInfinity = false) bool zeroInfinity = false)
: ModdedOptionListAttribute(title) : this(title, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f)
{ {
}
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{ {
return new ModdedOptionList<ModdedNumberOption>(value.Count, idx => return new ModdedOptionList<ModdedNumberOption>(value.Count, idx =>
{ {
var final = value[idx] switch
{
null => min + increment,
int integer => integer,
_ => (float)value[idx]!,
};
return new( return new(
GetFormattedTitle(idx), GetFormattedTitle(idx),
(float)(value[idx] ?? min + increment), final,
min, min,
max, max,
increment, increment,
zeroBehavior,
negativeBehavior,
suffixType, suffixType,
formatString, formatString,
zeroInfinity); halfIncrements);
}); });
} }
/// <inheritdoc /> /// <inheritdoc />
public override void SetValue(int idx, object value) public override void SetValue(int idx, object value)
{ {
var opt = HolderOptionList?[idx] as ModdedNumberOption; if (HolderOptionList?[idx] is not ModdedNumberOption opt)
opt?.SetValue((float)value); return;
var final = value switch
{
int integer => integer,
_ => (float)value,
};
opt.SetValue(final);
} }
/// <inheritdoc /> /// <inheritdoc />
public override object GetValue(int idx) public override object GetValue(int idx)
{ {
return HolderOptionList?[idx] is ModdedNumberOption opt if (HolderOptionList?[idx] is not ModdedNumberOption opt)
? (object)opt.Value throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption");
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption");
var value = opt.Value;
if (ValueType == typeof(int))
return (int)Math.Round(value);
return value;
} }
} }
@@ -3,6 +3,7 @@ using System.Collections;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes; namespace MiraAPI.GameOptions.Attributes;
@@ -14,8 +15,8 @@ namespace MiraAPI.GameOptions.Attributes;
public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAttribute public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAttribute
{ {
internal IModdedOptionList? HolderOptionList { get; set; } internal IModdedOptionList? HolderOptionList { get; set; }
internal IList? Value { get; set; }
internal object? Value { get; set; } internal Type ValueType { get; set; }
/// <summary> /// <summary>
/// Gets the title of the options. /// Gets the title of the options.
@@ -31,16 +32,16 @@ public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAt
/// <param name="value">The new values as an object.</param> /// <param name="value">The new values as an object.</param>
public override void SetValue(object value) public override void SetValue(object value)
{ {
Value = value; Value = (IList)value;
var list = (IList)value;
if (list.Count != HolderOptionList!.Count) if (Value.Count != HolderOptionList!.Count)
{ {
throw new InvalidOperationException($"Value set to option list cannot change the list's length."); throw new InvalidOperationException($"Value set to option list cannot change the list's length.");
} }
for (int i = 0; i < list.Count; i++) for (int i = 0; i < Value.Count; i++)
{ {
SetValue(i, list[i]!); SetValue(i, Value[i]!);
} }
} }
@@ -77,18 +78,19 @@ public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAt
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "I don't even know if this is worth it.")] [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "I don't even know if this is worth it.")]
protected string GetFormattedTitle(int index) protected string GetFormattedTitle(int index)
{ {
Match match = Regex.Match(Title, @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout); var match = Regex.Match(Title.Translate(), @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout);
if (match.Success) if (!match.Success)
{ {
if (match.Groups.Count == 1 && match.Groups[0].Value == "0") return Title + index;
}
if (match.Value == "{0}" && !match.NextMatch().Success)
{ {
return string.Format(Title, index); return string.Format(Title, index);
} }
Error("ModdedOptionList Title cannot contain more than the first parameter for formatting."); Error($"ModdedOptionList Title of {Title} cannot contain more than the first parameter for formatting.");
}
return Title + index; return Title + index;
} }
} }
@@ -0,0 +1,52 @@
using System;
using System.Collections;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for creating a list of player options.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="allowNone">Whether the option includes a none option. If None is selected, value will return -1.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedPlayerOptionListAttribute(
string title,
bool allowNone = true)
: ModdedOptionListAttribute(title)
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
return new ModdedOptionList<ModdedPlayerOption>(value.Count, idx =>
{
return new(GetFormattedTitle(idx), allowNone);
});
}
/// <inheritdoc />
public override void SetValue(int idx, object value)
{
if (HolderOptionList?[idx] is not ModdedPlayerOption opt)
return;
var final = value switch
{
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0),
_ => (int)value,
};
opt?.SetValue(final);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
if (HolderOptionList?[idx] is not ModdedPlayerOption opt)
throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedPlayerOption");
if (ValueType == typeof(int))
return opt.Value;
return opt.SelectedPlayer!;
}
}
@@ -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
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedStringOption");
}
}
@@ -9,13 +9,28 @@ namespace MiraAPI.GameOptions.Attributes;
/// Attribute for a list of toggle options. /// Attribute for a list of toggle options.
/// </summary> /// </summary>
/// <param name="title">The option's title.</param> /// <param name="title">The option's title.</param>
/// <param name="on">What the option displays when toggled on.</param>
/// <param name="off">What the option displays when toggled off.</param>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class ModdedToggleOptionListAttribute(string title) : ModdedOptionListAttribute(title) public class ModdedToggleOptionListAttribute(
string title,
string on,
string off)
: ModdedOptionListAttribute(title)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOptionListAttribute"/> class.
/// </summary>
/// <param name="title">The option's title.</param>
public ModdedToggleOptionListAttribute(string title)
: this(title, "On", "Off")
{
}
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{ {
var optList = new ModdedOptionList<ModdedToggleOption>( var optList = new ModdedOptionList<ModdedToggleOption>(
value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false))); value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false), on, off));
return optList; return optList;
} }
@@ -30,7 +45,7 @@ public class ModdedToggleOptionListAttribute(string title) : ModdedOptionListAtt
public override object GetValue(int idx) public override object GetValue(int idx)
{ {
return HolderOptionList?[idx] is ModdedToggleOption opt return HolderOptionList?[idx] is ModdedToggleOption opt
? (object)opt.Value ? opt.Value
: throw new InvalidOperationException($"Holder option for {GetFormattedTitle(idx)} is not a ModdedToggleOption."); : throw new InvalidOperationException($"Holder option for {GetFormattedTitle(idx)} is not a ModdedToggleOption.");
} }
} }
+8
View File
@@ -0,0 +1,8 @@
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)]
internal sealed class CollectionBuilderAttribute(Type builderType, string methodName) : Attribute
{
public Type BuilderType { get; } = builderType;
public string MethodName { get; } = methodName;
}
+21
View File
@@ -57,6 +57,21 @@ public interface IModdedOption
/// </summary> /// </summary>
Func<bool> Visible { get; set; } 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> /// <summary>
/// Gets or sets a value indicating whether the option should be included with presets. /// Gets or sets a value indicating whether the option should be included with presets.
/// </summary> /// </summary>
@@ -114,6 +129,12 @@ public interface IModdedOption
/// </summary> /// </summary>
/// <param name="presetConfig">The <see cref="ConfigFile"/> representing the preset configuration.</param> /// <param name="presetConfig">The <see cref="ConfigFile"/> representing the preset configuration.</param>
void LoadFromPreset(ConfigFile presetConfig); 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> /// <summary>
+294
View File
@@ -0,0 +1,294 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace MiraAPI.GameOptions;
/// <summary>
/// A fixed-size array created specifically for modded options that automatically synchronizes with option attributes.
/// </summary>
/// <typeparam name="T">The type of the values that the array holds.</typeparam>
[CollectionBuilder(typeof(ModdedArrayBuilder), nameof(ModdedArrayBuilder.Create))]
public sealed class ModdedArray<T> : IList<T>, IReadOnlyList<T>, IList
{
private readonly T[] items;
private int version;
/// <inheritdoc/>
public int Count => items.Length;
/// <inheritdoc/>
public bool IsReadOnly => false;
/// <inheritdoc/>
bool IList.IsFixedSize => true;
/// <inheritdoc/>
bool ICollection.IsSynchronized => false;
/// <inheritdoc/>
object ICollection.SyncRoot => items.SyncRoot;
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with the provided capacity.
/// </summary>
/// <param name="capacity">The capacity of the list.</param>
public ModdedArray(int capacity)
{
items = new T[capacity];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="collection">The initial collection of values to initialise with.</param>
public ModdedArray(IEnumerable<T> collection)
{
items = [.. collection];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="span">The initial span of values to initialise with.</param>
public ModdedArray(Span<T> span)
{
items = [.. span];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="span">The initial span of values to initialise with.</param>
public ModdedArray(ReadOnlySpan<T> span)
{
items = [.. span];
}
/// <summary>
/// Gets or sets the element at the specified index, syncing with the attribute if registered.
/// </summary>
/// <param name="index">The index of the value.</param>
public T this[int index]
{
get => ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute)
? (T)attribute.GetValue(index)
: items[index];
set
{
if (ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute))
attribute.SetValue(index, value!);
else
items[index] = value;
version++;
}
}
/// <inheritdoc/>
object? IList.this[int index]
{
get => this[index];
set => this[index] = (T)value!;
}
/// <inheritdoc/>
public bool Contains(T item)
{
return Array.IndexOf(items, item) >= 0;
}
/// <inheritdoc/>
public void CopyTo(T[] array, int arrayIndex)
{
items.CopyTo(array, arrayIndex);
}
/// <inheritdoc/>
public int IndexOf(T item)
{
return Array.IndexOf(items, item);
}
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public Enumerator GetEnumerator()
{
return new(this);
}
/// <inheritdoc/>
bool IList.Contains(object? value)
{
return ((IList)items).Contains(value);
}
/// <inheritdoc/>
int IList.IndexOf(object? value)
{
return ((IList)items).IndexOf(value);
}
/// <inheritdoc/>
void ICollection.CopyTo(Array array, int index)
{
items.CopyTo(array, index);
}
/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc/>
int IList.Add(object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Clear()
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Insert(int index, object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Remove(object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.RemoveAt(int index)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Insert(int index, T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void RemoveAt(int index)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Add(T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Clear()
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public bool Remove(T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <summary>
/// Enumerates the elements of a <see cref="ModdedArray{T}"/>.
/// </summary>
public struct Enumerator : IEnumerator<T>
{
private readonly ModdedArray<T> array;
private readonly int version;
private int index;
private T? current;
internal Enumerator(ModdedArray<T> moddedArray)
{
array = moddedArray;
version = moddedArray.version;
index = 0;
current = default;
}
/// <inheritdoc/>
public readonly T Current => current!;
/// <inheritdoc/>
readonly object? IEnumerator.Current
{
get
{
if (index == 0 || index == array.Count + 1)
throw new InvalidOperationException("Enumeration has not started or has already finished.");
return Current;
}
}
/// <inheritdoc/>
public readonly void Dispose()
{
}
/// <inheritdoc/>
public bool MoveNext()
{
if (version != array.version)
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
if ((uint)index < (uint)array.Count)
{
current = array[index];
index++;
return true;
}
index = array.Count + 1;
current = default;
return false;
}
/// <inheritdoc/>
public void Reset()
{
if (version != array.version)
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
index = 0;
current = default;
}
}
}
/// <summary>
/// Provides a builder for <see cref="ModdedArray{T}"/> to support C# 12 collection expressions.
/// </summary>
public static class ModdedArrayBuilder
{
/// <summary>
/// Creates a new <see cref="ModdedArray{T}"/> from a span of values.
/// </summary>
/// <typeparam name="T">The the type of the values.</typeparam>
/// <param name="values">The values to initialise with.</param>
/// <returns>The <see cref="ModdedArray{T}"/> instance initialised with the provided values.</returns>
public static ModdedArray<T> Create<T>(ReadOnlySpan<T> values)
{
return new(values);
}
}
+119 -85
View File
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using BepInEx.Configuration; using BepInEx.Configuration;
using HarmonyLib; using HarmonyLib;
using MiraAPI.GameModes; using MiraAPI.GameModes;
@@ -24,9 +25,15 @@ namespace MiraAPI.GameOptions;
/// </summary> /// </summary>
public static class ModdedOptionsManager public static class ModdedOptionsManager
{ {
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 Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
private static readonly HashSet<PropertyInfo> OptionAttributes = [];
private static readonly HarmonyMethod CachedGetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyGetterPatch));
private static readonly HarmonyMethod CachedSetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertySetterPatch));
internal static readonly ConditionalWeakTable<object, ModdedOptionListAttribute> ListInstanceCache = [];
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = []; internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
internal static readonly Dictionary<OptionBehaviour, ModdedStringOption> CreatedStringOptions = []; internal static readonly Dictionary<OptionBehaviour, ModdedStringOption> CreatedStringOptions = [];
internal static readonly Dictionary<Type, List<AbstractOptionGroup>> GameModeOptionGroups = []; internal static readonly Dictionary<Type, List<AbstractOptionGroup>> GameModeOptionGroups = [];
@@ -61,14 +68,14 @@ public static class ModdedOptionsManager
internal static bool RegisterGroup(Type type) internal static bool RegisterGroup(Type type)
{ {
if (Activator.CreateInstance(type) is not AbstractOptionGroup group) if (TypeToGroup.ContainsKey(type))
{ {
Error($"Group {type.Name} already exists.");
return false; return false;
} }
if (TypeToGroup.ContainsKey(type)) if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
{ {
Logger<MiraApiPlugin>.Error($"Group {type.Name} already exists.");
return false; return false;
} }
@@ -78,17 +85,17 @@ public static class ModdedOptionsManager
internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo) internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo)
{ {
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
{
return false;
}
if (TypeToGroup.ContainsKey(type)) if (TypeToGroup.ContainsKey(type))
{ {
Error($"Group {type.Name} already exists."); Error($"Group {type.Name} already exists.");
return false; return false;
} }
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
{
return false;
}
if (typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType)) if (typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType))
{ {
if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList)) if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList))
@@ -116,7 +123,7 @@ public static class ModdedOptionsManager
Groups.Add(group); Groups.Add(group);
TypeToGroup.Add(type, group); TypeToGroup.Add(type, group);
typeof(OptionGroupSingleton<>).MakeGenericType(type) typeof(OptionGroupSingleton<>).MakeGenericType(type)
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! // Message suppression points to here .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
.SetValue(null, group); .SetValue(null, group);
} }
@@ -143,7 +150,7 @@ public static class ModdedOptionsManager
PropertyInfo property, PropertyInfo property,
MiraPluginInfo pluginInfo) MiraPluginInfo pluginInfo)
{ {
if (OptionAttributes.ContainsKey(property)) if (!OptionAttributes.Add(property))
{ {
Error($"Property {property.Name} already has an attribute registered."); Error($"Property {property.Name} already has an attribute registered.");
return; return;
@@ -163,22 +170,40 @@ public static class ModdedOptionsManager
return; return;
} }
var setterOriginal = property.GetSetMethod(); var getterOriginal = property.GetGetMethod(true)!;
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); GetterLookups[getterOriginal] = attribute;
var getterOriginal = property.GetGetMethod(); var setterOriginal = property.GetSetMethod(true);
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
OptionAttributes.Add(property, attribute); if (setterOriginal != null)
attribute.HolderOption = option;
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>();
var visibilityFunc = visibilityAttr?.GetVisibility(group, property);
if (visibilityFunc != null)
{ {
option.Visible = visibilityFunc; PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
SetterLookups[setterOriginal] = attribute;
}
attribute.HolderOption = option;
attribute.ValueType = property.PropertyType;
var customAttributes = property.GetCustomAttributes(false);
foreach (var attr in customAttributes)
{
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); RegisterOption(option, group, property.Name, pluginInfo);
@@ -198,9 +223,9 @@ public static class ModdedOptionsManager
return; return;
} }
Func<int, string> configName = optionList.ConfigName ?? (i => property.Name + (i + 1)); var configName = optionList.ConfigName ?? (i => property.Name + (i + 1));
for (int i = 0; i < optionList.Count; i++) for (var i = 0; i < optionList.Count; i++)
{ {
RegisterOption(optionList[i], group, configName(i), pluginInfo); RegisterOption(optionList[i], group, configName(i), pluginInfo);
} }
@@ -212,7 +237,7 @@ public static class ModdedOptionsManager
PropertyInfo property, PropertyInfo property,
MiraPluginInfo pluginInfo) MiraPluginInfo pluginInfo)
{ {
if (OptionAttributes.ContainsKey(property)) if (!OptionAttributes.Add(property))
{ {
Error($"Property {property.Name} already has an attribute registered."); Error($"Property {property.Name} already has an attribute registered.");
return; return;
@@ -247,37 +272,71 @@ public static class ModdedOptionsManager
return; return;
} }
var setterOriginal = property.GetSetMethod(); var listType = propertyList.GetType();
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); var valueType = listType.GetGenericArguments()[0];
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
var getterOriginal = property.GetGetMethod(); if (!listType.IsGenericType || listType.GetGenericTypeDefinition() != typeof(ModdedArray<>))
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); {
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); Error($"Please use ModdedList<{valueType.Name}> for modded option lists.");
return;
}
var listIndex = propertyList.GetType().GetProperty("Item")!; var getterOriginal = property.GetGetMethod(true)!;
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
GetterLookups[getterOriginal] = attribute;
var listSetterOriginal = listIndex.GetSetMethod(); var setterOriginal = property.GetSetMethod(true);
var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch));
var listGetterOriginal = listIndex.GetSetMethod(); if (setterOriginal != null)
var listGetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch)); {
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch)); PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
SetterLookups[setterOriginal] = attribute;
}
OptionAttributes.Add(property, attribute);
attribute.HolderOptionList = optionList; attribute.HolderOptionList = optionList;
attribute.Value = propertyVal; attribute.Value = propertyList;
attribute.ValueType = valueType;
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>(); ListInstanceCache.Add(propertyVal, attribute);
var visibilityFunc = visibilityAttr?.GetListVisibility(group, property);
for (int i = 0; i < optionList.Count; i++) var customAttributes = property.GetCustomAttributes(false);
Func<int, Delegate?>? visibilityFactory = null, getterFactory = null, setterFactory = null, changedFactory = null;
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]; var option = optionList[i];
if (visibilityFunc != null)
{ if (visibilityFactory != null)
option.Visible = () => visibilityFunc(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); RegisterOption(option, group, property.Name + (i + 1), pluginInfo);
} }
@@ -350,11 +409,16 @@ public static class ModdedOptionsManager
/// <param name="__originalMethod">The original setter method.</param> /// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param> /// <param name="value">The new object value.</param>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention.")] [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) public static void PropertySetterPatch(MethodBase __originalMethod, object value)
{ {
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value; if (!SetterLookups.TryGetValue(__originalMethod, out var attribute))
return;
attribute.SetValue(value); attribute.SetValue(value);
if (attribute is ModdedOptionListAttribute listAttribute && value != null)
ListInstanceCache.AddOrUpdate(value, listAttribute);
} }
/// <summary> /// <summary>
@@ -364,43 +428,13 @@ public static class ModdedOptionsManager
/// <param name="__result">The result of the property getter.</param> /// <param name="__result">The result of the property getter.</param>
/// <returns><see langword="false"/> so the original getter gets skipped.</returns> /// <returns><see langword="false"/> so the original getter gets skipped.</returns>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] [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) public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
{ {
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value; if (!GetterLookups.TryGetValue(__originalMethod, out var attribute))
return true;
__result = attribute.GetValue(); __result = attribute.GetValue();
return false; return false;
} }
/// <summary>
/// Patches the setter of a list property to update the value of the option.
/// </summary>
/// <param name="__instance">The list's instance.</param>
/// <param name="index">The index to find in the list.</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 = "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);
}
/// <summary>
/// Patches the getter of a list property to return the value of the option.
/// </summary>
/// <param name="__instance">The list's instance.</param>
/// <param name="index">The index to find in the list.</param>
/// <param name="__result">The result of the property getter.</param>
/// <returns>False so the original getter gets skipped.</returns>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[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);
return false;
}
} }
@@ -50,7 +50,11 @@ public class ModdedEnumOption : ModdedOption<int>
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}"; $"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20); stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged; stringOption.OnValueChanged = (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 // SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName; stringOption.Title = StringName;
@@ -113,7 +117,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// </summary> /// </summary>
/// <typeparam name="T">The <see cref="Enum"/> type.</typeparam> /// <typeparam name="T">The <see cref="Enum"/> type.</typeparam>
public class ModdedEnumOption<T> : ModdedOption<T> public class ModdedEnumOption<T> : ModdedOption<T>
where T : Enum where T : struct, Enum
{ {
/// <summary> /// <summary>
/// Gets the string values of the <see langword="enum"/>. /// Gets the string values of the <see langword="enum"/>.
@@ -130,7 +134,7 @@ public class ModdedEnumOption<T> : ModdedOption<T>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset = true) public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset) : base(title, defaultValue, includeInPreset)
{ {
Values = values ?? Enum.GetNames(typeof(T)); Values = values ?? Enum.GetNames<T>();
var data = ScriptableObject.CreateInstance<StringGameSetting>(); var data = ScriptableObject.CreateInstance<StringGameSetting>();
data.Title = StringName; data.Title = StringName;
@@ -149,7 +153,11 @@ public class ModdedEnumOption<T> : ModdedOption<T>
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}"; $"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20); stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged; stringOption.OnValueChanged = (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 // SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName; stringOption.Title = StringName;
@@ -176,13 +184,13 @@ public class ModdedEnumOption<T> : ModdedOption<T>
/// <inheritdoc /> /// <inheritdoc />
public override void HandleNetData(byte[] data) 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 /> /// <inheritdoc />
public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) 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 /> /// <inheritdoc />
@@ -61,10 +61,10 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="max">The maximum value.</param> /// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param> /// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param> /// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param> /// <param name="zeroBehavior">Determines what is shown for zero.</param>
/// <param name="zeroBehavior">Determines what is shown for zero. Options: ∞, #, or a word value (such as Off).</param> /// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
/// <param name="negativeBehavior">Determines what is shown for negative 1. Options: ∞, #, or a word value (such as Off).</param>
/// <param name="halfIncrements">Whether increments can be split in half.</param> /// <param name="halfIncrements">Whether increments can be split in half.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param> /// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedNumberOption( public ModdedNumberOption(
string title, string title,
@@ -85,14 +85,12 @@ public class ModdedNumberOption : ModdedOption<float>
Increment = increment; Increment = increment;
SuffixType = suffixType; SuffixType = suffixType;
ZeroWordValue = zeroBehavior; ZeroWordValue = zeroBehavior;
NegativeWordValue = negativeBehavior;
ShiftIncrement = halfIncrements; ShiftIncrement = halfIncrements;
NegativeWordValue = negativeBehavior;
Value = Mathf.Clamp(defaultValue, min, max); Value = Mathf.Clamp(defaultValue, min, max);
Data = ScriptableObject.CreateInstance<FloatGameSetting>(); var data = ScriptableObject.CreateInstance<FloatGameSetting>();
var data = (FloatGameSetting)Data;
data.Type = global::OptionTypes.Float; data.Type = global::OptionTypes.Float;
data.Title = StringName; data.Title = StringName;
data.Value = Value; data.Value = Value;
@@ -108,6 +106,8 @@ public class ModdedNumberOption : ModdedOption<float>
data.ZeroIsInfinity = ZeroInfinity; data.ZeroIsInfinity = ZeroInfinity;
data.SuffixType = (NumberSuffixes)SuffixType; data.SuffixType = (NumberSuffixes)SuffixType;
data.OptionName = FloatOptionNames.Invalid; data.OptionName = FloatOptionNames.Invalid;
Data = data;
} }
/// <summary> /// <summary>
@@ -119,8 +119,8 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="max">The maximum value.</param> /// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param> /// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param> /// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param> /// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param> /// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedNumberOption( public ModdedNumberOption(
string title, string title,
@@ -132,36 +132,8 @@ public class ModdedNumberOption : ModdedOption<float>
string? formatString = null, string? formatString = null,
bool zeroInfinity = false, bool zeroInfinity = false,
bool includeInPreset = true) bool includeInPreset = true)
: base(title, defaultValue, includeInPreset) : this(title, defaultValue, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f, includeInPreset)
{ {
Min = min;
Max = max;
Increment = increment;
SuffixType = suffixType;
ZeroWordValue = zeroInfinity ? "∞" : "#";
NegativeWordValue = "#";
ShiftIncrement = Mathf.Approximately(increment, 1f);
Value = Mathf.Clamp(defaultValue, min, max);
Data = ScriptableObject.CreateInstance<FloatGameSetting>();
var data = (FloatGameSetting)Data;
data.Type = global::OptionTypes.Float;
data.Title = StringName;
data.Value = Value;
data.Increment = Increment;
data.ValidRange = new FloatRange(Min, Max);
data.FormatString = formatString ??
(defaultValue.IsInteger() &&
Increment.IsInteger() &&
Value.IsInteger() &&
Min.IsInteger() &&
Max.IsInteger() ? "0" : "0.0");
data.ZeroIsInfinity = ZeroInfinity;
data.SuffixType = (NumberSuffixes)SuffixType;
data.OptionName = FloatOptionNames.Invalid;
} }
/// <inheritdoc /> /// <inheritdoc />
+61 -16
View File
@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic;
using BepInEx.Configuration; using BepInEx.Configuration;
using MiraAPI.Networking; using MiraAPI.Networking;
using MiraAPI.PluginLoading; using MiraAPI.PluginLoading;
using MiraAPI.Translation; using MiraAPI.Translation;
using MiraAPI.Utilities;
using Reactor.Networking.Rpc; using Reactor.Networking.Rpc;
using UnityEngine; using UnityEngine;
@@ -43,16 +45,32 @@ public abstract class ModdedOption<T> : IModdedOption
/// <summary> /// <summary>
/// Gets or sets the value of the option. /// Gets or sets the value of the option.
/// </summary> /// </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> /// <summary>
/// Gets the default value of the option. /// Gets the default value of the option.
/// </summary> /// </summary>
public T DefaultValue { get; } public T DefaultValue { get; }
/// <summary> /// <inheritdoc cref="IModdedOption.Getter"/>
/// Gets or sets the event that is invoked when the value of the option changes. public Func<T, T>? Getter { get; set; }
/// </summary>
/// <inheritdoc cref="IModdedOption.Setter"/>
public Func<T, T, T>? Setter { get; set; }
/// <inheritdoc cref="IModdedOption.ChangedEvent"/>
public Action<T>? ChangedEvent { get; set; } public Action<T>? ChangedEvent { get; set; }
/// <inheritdoc /> /// <inheritdoc />
@@ -67,6 +85,33 @@ public abstract class ModdedOption<T> : IModdedOption
/// <inheritdoc /> /// <inheritdoc />
public ConfigDefinition? ConfigDefinition { get; set; } 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> /// <summary>
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class. /// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
/// </summary> /// </summary>
@@ -80,7 +125,7 @@ public abstract class ModdedOption<T> : IModdedOption
DefaultValue = defaultValue; DefaultValue = defaultValue;
Value = defaultValue; Value = defaultValue;
StringName = MiraLocaleManager.GetOrCreateLocaleString(Title); StringName = MiraLocaleManager.GetOrCreateLocaleString(Title);
Visible = () => true; Visible = DelegateUtilities.AlwaysTrue;
IncludeInPreset = includeInPreset; IncludeInPreset = includeInPreset;
} }
@@ -96,25 +141,25 @@ public abstract class ModdedOption<T> : IModdedOption
/// <param name="sendRpc">Whether to send the value to other players.</param> /// <param name="sendRpc">Whether to send the value to other players.</param>
public void SetValue(T newValue, bool sendRpc = true) public void SetValue(T newValue, bool sendRpc = true)
{ {
var oldVal = Value; var oldVal = _value;
Value = newValue; 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 (sendRpc && AmongUsClient.Instance.AmHost)
{ {
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true) if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
{ {
entry.Value = Value; entry.Value = _value;
} }
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true); Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true);
} }
OnValueChanged(newValue); OnValueChanged(_value);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -149,6 +194,12 @@ public abstract class ModdedOption<T> : IModdedOption
} }
} }
/// <inheritdoc />
public Type GetValueType()
{
return typeof(T);
}
/// <inheritdoc /> /// <inheritdoc />
public abstract float GetFloatData(); public abstract float GetFloatData();
@@ -188,10 +239,4 @@ public abstract class ModdedOption<T> : IModdedOption
{ {
return option.Value; return option.Value;
} }
/// <inheritdoc />
public AbstractOptionGroup ParentGroup { get; set; }
/// <inheritdoc />
public virtual OptionNotifConfiguration Configuration => new(ParentGroup);
} }
@@ -44,22 +44,6 @@ public class ModdedOptionList<T> : IModdedOptionList<T>
Options = [.. options]; Options = [.. options];
} }
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public IEnumerator<T> GetEnumerator()
{
return Options.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Options.GetEnumerator();
}
IEnumerator<IModdedOption> IEnumerable<IModdedOption>.GetEnumerator()
{
return ((IEnumerable<IModdedOption>)Options).GetEnumerator();
}
/// <summary> /// <summary>
/// Indexes the option of type <typeparamref name="T"/>. /// Indexes the option of type <typeparamref name="T"/>.
/// </summary> /// </summary>
@@ -67,5 +51,24 @@ public class ModdedOptionList<T> : IModdedOptionList<T>
/// <returns>option of type <typeparamref name="T"/>.</returns> /// <returns>option of type <typeparamref name="T"/>.</returns>
public T this[int idx] => Options[idx]; public T this[int idx] => Options[idx];
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public IEnumerator<T> GetEnumerator()
{
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();
}
/// <inheritdoc cref="IReadOnlyList{T}.this"/>
IModdedOption IReadOnlyList<IModdedOption>.this[int index] => this[index]; IModdedOption IReadOnlyList<IModdedOption>.this[int index] => this[index];
} }
@@ -29,6 +29,11 @@ public class ModdedPlayerOption : ModdedOption<int>
/// </summary> /// </summary>
public bool AllowNone { get; set; } public bool AllowNone { get; set; }
/// <summary>
/// Gets the player selected by this option. May be <c>null</c> if allowed to have <c>None</c> as an option.
/// </summary>
public NetworkedPlayerInfo? SelectedPlayer => Value == -1 ? null : Values[Value];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModdedPlayerOption"/> class. /// Initializes a new instance of the <see cref="ModdedPlayerOption"/> class.
/// </summary> /// </summary>
@@ -37,15 +42,16 @@ public class ModdedPlayerOption : ModdedOption<int>
public ModdedPlayerOption(string title, bool allowNone = true) public ModdedPlayerOption(string title, bool allowNone = true)
: base(title, allowNone ? -1 : 0, false) : base(title, allowNone ? -1 : 0, false)
{ {
Data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>(); Values = [];
var data = (PlayerSelectionGameSetting)Data; AllowNone = allowNone;
Value = DefaultValue;
var data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>();
data.Title = StringName; data.Title = StringName;
data.Type = global::OptionTypes.Player; data.Type = global::OptionTypes.Player;
Values = []; Data = data;
AllowNone = allowNone;
Value = DefaultValue;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -63,7 +69,7 @@ public class ModdedPlayerOption : ModdedOption<int>
Values = GetFilteredPlayers(); Values = GetFilteredPlayers();
playerOption.SetUpFromData(Data, 20); playerOption.SetUpFromData(Data, 20);
playerOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged; playerOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
playerOption.Title = StringName; playerOption.Title = StringName;
playerOption.TitleText.text = Title.Translate(); playerOption.TitleText.text = Title.Translate();
@@ -86,7 +92,14 @@ public class ModdedPlayerOption : ModdedOption<int>
/// <returns>A list of filtered players.</returns> /// <returns>A list of filtered players.</returns>
public List<NetworkedPlayerInfo> GetFilteredPlayers() public List<NetworkedPlayerInfo> GetFilteredPlayers()
{ {
return [.. GameData.Instance.AllPlayers.ToArray().Where(x => PlayerFilter?.Invoke(x) ?? true)]; var players = GameData.Instance.AllPlayers.ToArray();
IEnumerable<NetworkedPlayerInfo> filtered = players;
if (PlayerFilter != null)
filtered = players.Where(PlayerFilter);
return [.. filtered.OrderBy(x => x.PlayerId)];
} }
/// <summary> /// <summary>
@@ -25,6 +25,28 @@ public class ModdedStringOption : ModdedOption<string>
/// </summary> /// </summary>
public StringNames[] StringNameValues { get; set; } 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;
StringNameValues = [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)];
var data = ScriptableObject.CreateInstance<StringGameSetting>();
data.Title = StringName;
data.Type = global::OptionTypes.String;
data.Values = StringNameValues;
data.Index = GetIndex(Value);
Data = data;
}
/// <summary> /// <summary>
/// Gets the string's index. /// Gets the string's index.
/// </summary> /// </summary>
@@ -61,62 +83,38 @@ public class ModdedStringOption : ModdedOption<string>
StringNameValues = [.. newStrVals]; StringNameValues = [.. newStrVals];
} }
if (Data != null) if (Data == null)
{ {
Error("Data is null.");
return;
}
var data = (StringGameSetting)Data; var data = (StringGameSetting)Data;
data.Values = StringNameValues; data.Values = StringNameValues;
data.Index = GetIndex(Value); data.Index = GetIndex(Value);
if (OptionBehaviour != null)
Warning("Data is valid");
if (OptionBehaviour == null)
{ {
Error("OptionBehaviour is null.");
return;
}
var strOpt = OptionBehaviour.TryCast<StringOption>(); var strOpt = OptionBehaviour.TryCast<StringOption>();
if (strOpt != null)
Warning("OptionBehaviour is valid");
if (strOpt == null)
{ {
Error("StringOption is null.");
return;
}
strOpt.Values = StringNameValues; strOpt.Values = StringNameValues;
strOpt.Value = GetIndex(Value); strOpt.Value = GetIndex(Value);
Warning("StringOption is valid"); 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 /> /// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
@@ -126,7 +124,11 @@ public class ModdedStringOption : ModdedOption<string>
$"{ParentMod!.OptionsTitleText}.StringOption.{TranslationController.Instance.GetString(StringName)}"; $"{ParentMod!.OptionsTitleText}.StringOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20); stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged; stringOption.OnValueChanged = (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 // SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName; stringOption.Title = StringName;
@@ -11,6 +11,37 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// </summary> /// </summary>
public class ModdedToggleOption : ModdedOption<bool> public class ModdedToggleOption : ModdedOption<bool>
{ {
/// <summary>
/// Gets or sets the display value when the option is toggled on.
/// </summary>
public string OnValue { get; set; }
/// <summary>
/// Gets or sets the display value when the option is toggled off.
/// </summary>
public string OffValue { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOption"/> class.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="on">What the option displays when toggled on.</param>
/// <param name="off">What the option displays when toggled off.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedToggleOption(string title, bool defaultValue, string on, string off, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
{
OnValue = on;
OffValue = off;
var data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
data.Title = StringName;
data.Type = global::OptionTypes.Checkbox;
Data = data;
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOption"/> class. /// Initializes a new instance of the <see cref="ModdedToggleOption"/> class.
/// </summary> /// </summary>
@@ -18,13 +49,8 @@ public class ModdedToggleOption : ModdedOption<bool>
/// <param name="defaultValue">The default value.</param> /// <param name="defaultValue">The default value.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param> /// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedToggleOption(string title, bool defaultValue, bool includeInPreset = true) public ModdedToggleOption(string title, bool defaultValue, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset) : this(title, defaultValue, "On", "Off", includeInPreset)
{ {
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
var data = (CheckboxGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.Checkbox;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -39,7 +65,7 @@ public class ModdedToggleOption : ModdedOption<bool>
toggleOption.Title = StringName; toggleOption.Title = StringName;
toggleOption.TitleText.text = Title.Translate(); toggleOption.TitleText.text = Title.Translate();
toggleOption.CheckMark.enabled = Value; toggleOption.CheckMark.enabled = Value;
toggleOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged; toggleOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
OptionBehaviour = toggleOption; OptionBehaviour = toggleOption;
@@ -76,7 +102,7 @@ public class ModdedToggleOption : ModdedOption<bool>
ModdedOptionsManager.AddSettingsChangeMessage( ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier, HudManager.Instance.Notifier,
StringName, StringName,
newValue ? "On" : "Off", (newValue ? OnValue : OffValue).Translate(),
Configuration.PopUpTextColor, Configuration.PopUpTextColor,
Configuration.PopUpIconTmp, Configuration.PopUpIconTmp,
false); false);
@@ -126,7 +126,7 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr)
currentFinalHideTime = totalFinalHideTime; currentFinalHideTime = totalFinalHideTime;
if (timerBar != null) if (timerBar != null)
{ {
Object.Destroy(timerBar); Destroy(timerBar);
} }
finalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX; finalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX;
+1 -1
View File
@@ -151,7 +151,7 @@ public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr)
var oldPos = settingsButton.transform.localPosition; var oldPos = settingsButton.transform.localPosition;
settingsButton.transform.localPosition = new Vector3(oldPos.x, oldPos.y, -100); settingsButton.transform.localPosition = new Vector3(oldPos.x, oldPos.y, -100);
var chatButton = instance.Chat.chatButton.gameObject; var chatButton = instance.Chat.chatButton.gameObject;
ClonedChatButton = Object.Instantiate(chatButton, chatButton.transform.parent); ClonedChatButton = Instantiate(chatButton, chatButton.transform.parent);
ClonedChatButton.SetActive(false); ClonedChatButton.SetActive(false);
instance.Chat.chatButtonAspectPosition = ClonedChatButton.GetComponent<AspectPosition>(); instance.Chat.chatButtonAspectPosition = ClonedChatButton.GetComponent<AspectPosition>();
chatButton.GetComponent<AspectPosition>().Destroy(); chatButton.GetComponent<AspectPosition>().Destroy();
+6
View File
@@ -67,6 +67,7 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
internal static ConfigEntry<SortingGroups> WikiSortingGroup { get; private set; } internal static ConfigEntry<SortingGroups> WikiSortingGroup { get; private set; }
internal static ConfigEntry<SortingMethod> WikiSortingMethod { get; private set; } internal static ConfigEntry<SortingMethod> WikiSortingMethod { get; private set; }
internal static ConfigEntry<SortingFilter> WikiSortingFilter { get; private set; } internal static ConfigEntry<SortingFilter> WikiSortingFilter { get; private set; }
internal static ConfigEntry<string> DistanceSuffix { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public override void Load() public override void Load()
@@ -86,6 +87,11 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
"WikiSortingFilter", "WikiSortingFilter",
SortingFilter.AllRoles, SortingFilter.AllRoles,
"Determines what roles / modifiers are displayed at once."); "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(); Harmony.PatchAll();
RoleOptionsCollectionPatch.PatchRoleMethods(Harmony); RoleOptionsCollectionPatch.PatchRoleMethods(Harmony);
UiResetEvents.Initialize(); UiResetEvents.Initialize();
+12 -12
View File
@@ -46,14 +46,14 @@ public static class SettingPatches
} }
else else
{ {
var part = value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo);
result = suffix switch result = suffix switch
{ {
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo), MiraNumberSuffixes.None => part,
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x", MiraNumberSuffixes.Multiplier => part + "x",
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%", MiraNumberSuffixes.Percent => part + "%",
_ => TranslationController.Instance.GetString( MiraNumberSuffixes.Distance => string.Concat(part, " ", MiraApiPlugin.DistanceSuffix.Value),
StringNames.GameSecondsAbbrev, _ => TranslationController.Instance.GetString(StringNames.GameSecondsAbbrev, part),
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
}; };
} }
} }
@@ -63,14 +63,14 @@ public static class SettingPatches
} }
else else
{ {
var part = value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo);
result = suffix switch result = suffix switch
{ {
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo), MiraNumberSuffixes.None => part,
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x", MiraNumberSuffixes.Multiplier => part + "x",
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%", MiraNumberSuffixes.Percent => part + "%",
_ => TranslationController.Instance.GetString( MiraNumberSuffixes.Distance => string.Concat(part, " ", MiraApiPlugin.DistanceSuffix.Value),
StringNames.GameSecondsAbbrev, _ => TranslationController.Instance.GetString(StringNames.GameSecondsAbbrev, part),
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
}; };
} }
+10 -6
View File
@@ -313,9 +313,12 @@ public sealed class MiraPluginManager
foreach (var property in type.GetProperties()) foreach (var property in type.GetProperties())
{ {
if (property.GetCustomAttribute<MiraIgnoreAttribute>() != null)
continue;
if (property.GetMethod?.IsStatic == true) if (property.GetMethod?.IsStatic == true)
{ {
Error($"Option property {property.Name} in {type.Name} must not be static."); Error($"Non-ignored option property {property.Name} in {type.Name} must not be static.");
continue; continue;
} }
@@ -325,6 +328,12 @@ public sealed class MiraPluginManager
continue; continue;
} }
if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList)))
{
ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo);
continue;
}
var attribute = property.GetCustomAttribute<ModdedOptionAttribute>(); var attribute = property.GetCustomAttribute<ModdedOptionAttribute>();
if (attribute != null) if (attribute != null)
{ {
@@ -332,11 +341,6 @@ public sealed class MiraPluginManager
continue; continue;
} }
if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList)))
{
ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo);
}
var listAttr = property.GetCustomAttribute<ModdedOptionListAttribute>(); var listAttr = property.GetCustomAttribute<ModdedOptionListAttribute>();
if (listAttr != null) if (listAttr != null)
{ {
-3
View File
@@ -10,9 +10,6 @@ namespace MiraAPI.Presets;
/// <summary> /// <summary>
/// Represents a preset of game options that can be applied to the game. /// Represents a preset of game options that can be applied to the game.
/// </summary> /// </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="name">The name of the preset.</param>
/// <param name="plugin">The plugin associated with the preset.</param> /// <param name="plugin">The plugin associated with the preset.</param>
/// <param name="presetConfig">The configuration file for the preset.</param> /// <param name="presetConfig">The configuration file for the preset.</param>
+8
View File
@@ -71,4 +71,12 @@ Outlive the timer or eliminate the crew.</string>
<string name="MiraApi.Wiki.PreviousPage">Previous</string> <string name="MiraApi.Wiki.PreviousPage">Previous</string>
<string name="MiraApi.Wiki.NextPage">Next</string> <string name="MiraApi.Wiki.NextPage">Next</string>
<string name="MiraApi.Wiki.NoResultsFound">No Results Found</string> <string name="MiraApi.Wiki.NoResultsFound">No Results Found</string>
<string name="On">On</string>
<string name="Off">Off</string>
<string name="Enabled">Enabled</string>
<string name="Disabled">Disabled</string>
<string name="Active">Active</string>
<string name="Inactive">Inactive</string>
<string name="Activated">Activated</string>
<string name="Deactivated">Deactivated</string>
</resources> </resources>
+1 -1
View File
@@ -339,7 +339,7 @@ public interface ICustomRole : IOptionable
/// <summary> /// <summary>
/// Gets the function that determines whether the role should be toggled on or off in the game settings. /// Gets the function that determines whether the role should be toggled on or off in the game settings.
/// </summary> /// </summary>
Func<bool> VisibleInSettings => () => true; Func<bool> VisibleInSettings => DelegateUtilities.AlwaysTrue;
/// <summary> /// <summary>
/// Gets the list of abilities or other similar information to display below the text of a role's wiki page. /// Gets the list of abilities or other similar information to display below the text of a role's wiki page.
+19
View File
@@ -0,0 +1,19 @@
using System;
namespace MiraAPI.Utilities;
/// <summary>
/// A utility class for using delegates.
/// </summary>
public static class DelegateUtilities
{
/// <summary>
/// Gets a <c>Func&lt;bool&gt;</c> that always returns <c>true</c>.
/// </summary>
public static Func<bool> AlwaysTrue { get; } = () => true;
/// <summary>
/// Gets a <c>Func&lt;bool&gt;</c> that always returns <c>false</c>.
/// </summary>
public static Func<bool> AlwaysFalse { get; } = () => false;
}
+5
View File
@@ -24,4 +24,9 @@ public enum MiraNumberSuffixes
/// Percentage suffix (%). /// Percentage suffix (%).
/// </summary> /// </summary>
Percent, Percent,
/// <summary>
/// Distance suffix (u).
/// </summary>
Distance,
} }