mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-19 15:51:48 +00:00
Merge pull request #112 from All-Of-Us-Mods/options-improvements
More additions to the options system
This commit is contained in:
45 files changed
+1882
-509
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 enum BestApi
|
public static bool NumberOptVisibility()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Api GetApi(Api currentValue)
|
||||||
|
{
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Api SetApi(Api newValue, Api oldValue)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApiChanged(Api currentValue)
|
||||||
|
{
|
||||||
|
// Something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Api
|
||||||
{
|
{
|
||||||
MiraAPI,
|
MiraAPI,
|
||||||
Mitochondria,
|
Mitochondria,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)]),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MiraAPI.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A utility class for using delegates.
|
||||||
|
/// </summary>
|
||||||
|
public static class DelegateUtilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <c>Func<bool></c> that always returns <c>true</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static Func<bool> AlwaysTrue { get; } = () => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <c>Func<bool></c> that always returns <c>false</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static Func<bool> AlwaysFalse { get; } = () => false;
|
||||||
|
}
|
||||||
@@ -24,4 +24,9 @@ public enum MiraNumberSuffixes
|
|||||||
/// Percentage suffix (%).
|
/// Percentage suffix (%).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Percent,
|
Percent,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Distance suffix (u).
|
||||||
|
/// </summary>
|
||||||
|
Distance,
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user