diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index aff6ccf..c0b8af9 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -1,6 +1,7 @@ using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Utilities; +using System.Collections.Generic; using UnityEngine; namespace MiraAPI.Example.Options; @@ -16,11 +17,19 @@ public class ExampleOptions : AbstractOptionGroup [ModdedToggleOption("options.exampleOptions1.toggleOpt1")] public bool ToggleOpt2 { get; set; } = true; + [ModdedToggleOption("Toggle Opt 3")] + [ModdedOptionVisiblity(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)] public float NumberOpt { get; set; } = 4f; [ModdedEnumOption("options.exampleOptions1.bestApi", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])] public BestApi Opt { get; set; } = BestApi.MiraAPI; + + [ModdedEnumOptionList("Value State {0}", ["None", "Primary", "Secondary"])] + public List ValueStates { get; } = + [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None]; } public enum BestApi @@ -29,3 +38,10 @@ public enum BestApi Mitochondria, Reactor, } + +public enum ValueState +{ + None, + Primary, + Secondary, +} diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs index 6acba04..24a061c 100644 --- a/MiraAPI.Example/Options/ExampleOptions2.cs +++ b/MiraAPI.Example/Options/ExampleOptions2.cs @@ -16,6 +16,9 @@ public class ExampleOptions2 : AbstractOptionGroup public ModdedPlayerOption PlayerOption { get; } = new("options.exampleOptions2.ehWhatever"); public ModdedToggleOption ToggleOpt1 { get; } = new("options.exampleOptions2.toggleOption1", false); + public ModdedOptionList PlayerNums { get; } = new(5, i => + new($"PlayerNum{i}", 2, 0, 5, 1, Utilities.MiraNumberSuffixes.None)); + public ModdedToggleOption ToggleOpt2 { get; } = new("options.exampleOptions2.toggleOption2", false) { Visible = () => OptionGroupSingleton.Instance.ToggleOpt1, // implicit cast from ModdedToggleOption to bool diff --git a/MiraAPI/Events/MiraEventManager.cs b/MiraAPI/Events/MiraEventManager.cs index 30ffb74..5aa3c8c 100644 --- a/MiraAPI/Events/MiraEventManager.cs +++ b/MiraAPI/Events/MiraEventManager.cs @@ -17,7 +17,7 @@ public static class MiraEventManager /// The instance. /// Type of Event. /// If there was an event handler invoked for this event, return . Otherwise, return . - public static bool InvokeEvent(T eventInstance) where T : MiraEvent + public static bool InvokeEvent(this T eventInstance) where T : MiraEvent { EventWrappers.TryGetValue(typeof(T), out var handlers); if (handlers == null || handlers.Count == 0) @@ -46,7 +46,7 @@ public static class MiraEventManager /// The instance. /// The type to use for handler lookup. /// If there was an event handler invoked for this event, return . Otherwise, return . - public static bool InvokeEvent(MiraEvent eventInstance, Type type) + public static bool InvokeEvent(this MiraEvent eventInstance, Type type) { EventWrappers.TryGetValue(type, out var handlers); if (handlers == null || handlers.Count == 0) diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index 8dc4b26..383cb49 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -33,3 +33,33 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); } } + +/// +/// Attribute for creating an enum option. +/// +/// The enum type. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionAttribute(string title, string[]? values = null) + : ModdedOptionAttribute(title) where T : Enum +{ + internal override IModdedOption CreateOption(object? value, PropertyInfo property) + { + var opt = new ModdedEnumOption(Title, (T)(value ?? 0), values); + return opt; + } + + /// + public override void SetValue(object value) + { + var opt = HolderOption as ModdedEnumOption; + opt?.SetValue((T)value); + } + + /// + public override object GetValue() + { + return HolderOption is ModdedEnumOption opt + ? opt.Value + : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index 3d48453..98194f8 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -10,14 +10,14 @@ namespace MiraAPI.GameOptions.Attributes; /// Optional parameter to specify a role Type. /// Optional parameter to specify a game mode Type. [AttributeUsage(AttributeTargets.Property)] -public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : Attribute +public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : PropertyOptionAttribute { internal IModdedOption? HolderOption { get; set; } /// /// Gets the title of the option. /// - public string Title { get; private set; } = title; + public string Title => title; /// /// Gets the role type of the option. @@ -29,17 +29,5 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null, /// protected Type? ModeType { get; private set; } = modeType; - /// - /// Sets the value of the option. - /// - /// The new value as an . - public abstract void SetValue(object value); - - /// - /// Gets the value of the option. - /// - /// The value of the option as an . - public abstract object GetValue(); - internal abstract IModdedOption? CreateOption(object? value, PropertyInfo property); } diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs new file mode 100644 index 0000000..3e4475d --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs @@ -0,0 +1,195 @@ +using MiraAPI.GameOptions.OptionTypes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute to modify a property with a or . +/// +/// +/// Initializes a new instance of the class. +/// +/// The type the member is in. +/// The member to get the visibility function from. +[AttributeUsage(AttributeTargets.Property)] +public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, string? memberName = null) : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The member to get the visibility function from. + public ModdedOptionVisiblityAttribute(string memberName) + : this(null, memberName) + { + } + + internal Func? 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))) + { + 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? 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)) && + !vProperty.PropertyType.IsAssignableTo(typeof(IList))) + { + 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))) + { + if (indexParams.Length != 0) + { + Error($"Property {memberName} cannot be an indexer of lists of bools."); + return null; + } + + return i => ((IList)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; + } + +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + private MemberInfo? GetVisibilityMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out Func? 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; + } +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs new file mode 100644 index 0000000..dde2cfb --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs @@ -0,0 +1,67 @@ +using MiraAPI.GameOptions.OptionTypes; +using System; +using System.Collections; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for creating a list of enum options. +/// +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]? values = null) + : ModdedOptionListAttribute(title) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList( + value.Count, idx => new(GetFormattedTitle(idx), (int)(value[idx] ?? 0), enumType, values)); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedEnumOption; + opt?.SetValue((int)value); + } + + /// + public override object GetValue(int idx) + { + return HolderOptionList?[idx] is ModdedEnumOption opt + ? Enum.ToObject(enumType, opt.Value) + : throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); + } +} + +/// +/// Attribute for creating a list of enum options. +/// +/// The enum type. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionListAttribute(string title, string[]? values = null) + : ModdedOptionListAttribute(title) where T : Enum +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList>( + value.Count, idx => new(GetFormattedTitle(idx), (T)(value[idx] ?? 0), values)); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedEnumOption; + opt?.SetValue((T)value); + } + + /// + public override object GetValue(int idx) + { + return HolderOptionList?[idx] is ModdedEnumOption opt + ? opt.Value + : throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs new file mode 100644 index 0000000..96e9729 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs @@ -0,0 +1,55 @@ +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; +using System; +using System.Collections; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// A number option attribute for the modded options system. +/// +[AttributeUsage(AttributeTargets.Property)] +public class ModdedNumberOptionListAttribute( + string title, + float min, + float max, + float increment = 1, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null, + bool zeroInfinity = false) + : ModdedOptionListAttribute(title) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + return new ModdedOptionList(value.Count, idx => + { + return new( + GetFormattedTitle(idx), + (float)(value[idx] ?? min + increment), + min, + max, + increment, + suffixType, + formatString, + zeroInfinity); + }); + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedNumberOption; + opt?.SetValue((float)value); + } + + /// + public override object GetValue(int idx) + { + if (HolderOptionList?[idx] is ModdedNumberOption opt) + { + return opt.Value; + } + throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs new file mode 100644 index 0000000..f109ffc --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Represents an attribute that is used to define a list of modded options. +/// +/// The title of the options. +[AttributeUsage(AttributeTargets.Property)] +public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAttribute +{ + internal IModdedOptionList? HolderOptionList { get; set; } + + internal object? Value { get; set; } + + /// + /// Gets the title of the options. + /// If the string contains a format parameter, e.g., {0}, then the option's index will be given to it. + /// The string cannot contain more than one parameter. + /// If no parameter is given, the title will simply append the index to the end of it. + /// + public string Title => title; + + /// + /// Sets the value of all the options. + /// + /// The new values as an object. + public override void SetValue(object value) + { + Value = value; + var list = (IList)value; + if (list.Count != HolderOptionList!.Count) + { + throw new InvalidOperationException($"Value set to option list cannot change the list's length."); + } + + for (int i = 0; i < list!.Count; i++) + { + SetValue(i, list[i]!); + } + } + + /// + /// Sets the value of the specific option. + /// + /// The option to set. + /// The new value as an object. + public abstract void SetValue(int idx, object value); + + /// + /// Gets the value of all the options. + /// + /// The value of the options as an object. + public override object GetValue() + { + return Value!; + } + + /// + /// Gets the value of the specific option. + /// + /// The option to set. + /// The value of the option as an object. + public abstract object GetValue(int idx); + + internal abstract IModdedOptionList? CreateOptionList(IList value, PropertyInfo property); + + /// + /// Gets the formatted with the option's index. + /// + /// The option's index. + /// The formatted option's title. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "I don't even know if this is worth it.")] + protected string GetFormattedTitle(int index) + { + Match match = Regex.Match(Title, @"\{(\d+)(?::[^}]+)?\}"); + + if (match.Success) + { + if (match.Groups.Count == 1 && match.Groups[0].Value == "0") + { + return string.Format(Title, index); + } + Error("ModdedOptionList Title cannot contain more than the first parameter for formatting."); + } + return Title + index; + } +} diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs new file mode 100644 index 0000000..5209bcc --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs @@ -0,0 +1,38 @@ +using MiraAPI.GameOptions.OptionTypes; +using System; +using System.Collections; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for a list of toggle options. +/// +/// The option's title. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedToggleOptionListAttribute(string title) : ModdedOptionListAttribute(title) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList( + value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false))); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedToggleOption; + opt?.SetValue((bool)value); + } + + /// + public override object GetValue(int idx) + { + if (HolderOptionList?[idx] is ModdedToggleOption opt) + { + return opt.Value; + } + throw new InvalidOperationException($"Holder option for {GetFormattedTitle(idx)} is not a ModdedToggleOption."); + } +} diff --git a/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs new file mode 100644 index 0000000..643d7b7 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Represents an attribute that is used to intercept a property's getter and setter. +/// +[AttributeUsage(AttributeTargets.Property)] +public abstract class PropertyOptionAttribute : Attribute +{ + /// + /// Sets the value of the option. + /// + /// The new value as an object. + public abstract void SetValue(object value); + + /// + /// Gets the value of the option. + /// + /// The value of the option as an object. + public abstract object GetValue(); +} diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 646c22b..099194e 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; @@ -114,3 +115,10 @@ public interface IModdedOption /// The representing the preset configuration. void LoadFromPreset(ConfigFile presetConfig); } + +/// +/// Interface for list of modded options. +/// +public interface IModdedOptionList : IReadOnlyList +{ +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index d91d079..601287a 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -24,7 +25,7 @@ namespace MiraAPI.GameOptions; /// public static class ModdedOptionsManager { - private static readonly Dictionary OptionAttributes = []; + private static readonly Dictionary OptionAttributes = []; private static readonly Dictionary TypeToGroup = []; internal static readonly Dictionary CreatedPlayerOptions = []; @@ -180,9 +181,111 @@ public static class ModdedOptionsManager OptionAttributes.Add(property, attribute); attribute.HolderOption = option; + var visibilityAttr = property.GetCustomAttribute(); + var visibilityFunc = visibilityAttr?.GetVisibility(group, property); + if (visibilityFunc != null) + { + option.Visible = visibilityFunc; + } + RegisterOption(option, group, property.Name, pluginInfo); } + internal static void RegisterPropertyOptionList(Type type, PropertyInfo property, MiraPluginInfo pluginInfo) + { + if (!TypeToGroup.TryGetValue(type, out var group)) + { + Error($"Failed to get group for {type.Name}"); + return; + } + + if (property.GetValue(group) is not IModdedOptionList optionList) + { + Error($"Failed to get option list for {property.Name}"); + return; + } + + for (int i = 0; i < optionList.Count; i++) + { + RegisterOption(optionList[i], group, property.Name + i, pluginInfo); + } + } + + internal static void RegisterAttributeOptionList( + Type type, + ModdedOptionListAttribute attribute, + PropertyInfo property, + MiraPluginInfo pluginInfo) + { + if (OptionAttributes.ContainsKey(property)) + { + Error($"Property {property.Name} already has an attribute registered."); + return; + } + + if (!TypeToGroup.TryGetValue(type, out var group)) + { + Error($"Failed to get group for {type.Name}"); + return; + } + + var propertyVal = property.GetValue(group); + + if (propertyVal == null) + { + Error("Cannot initialize option list with null value."); + return; + } + + var propertyList = (IList)propertyVal; + var optionList = attribute.CreateOptionList(propertyList, property); + + if (optionList == null) + { + Error($"Failed to get option for {property.Name}"); + return; + } + if (propertyList.Count != optionList.Count) + { + Error("Mismatch in count between values and created options."); + return; + } + + var setterOriginal = property.GetSetMethod(); + var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); + PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); + + var getterOriginal = property.GetGetMethod(); + var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); + PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); + + var listIndex = propertyList.GetType().GetProperty("Item")!; + + var listSetterOriginal = listIndex.GetSetMethod(); + var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch)); + PluginSingleton.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch)); + + var listGetterOriginal = listIndex.GetSetMethod(); + var listGetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch)); + PluginSingleton.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch)); + + OptionAttributes.Add(property, attribute); + attribute.HolderOptionList = optionList; + attribute.Value = propertyVal; + + var visibilityAttr = property.GetCustomAttribute(); + var visibilityFunc = visibilityAttr?.GetListVisibility(group, property); + for (int i = 0; i < optionList.Count; i++) + { + var option = optionList[i]; + if (visibilityFunc != null) + { + option.Visible = () => visibilityFunc(i); + } + RegisterOption(option, group, property.Name + i, pluginInfo); + } + } + internal static void RegisterOption( IModdedOption option, AbstractOptionGroup group, @@ -273,4 +376,38 @@ public static class ModdedOptionsManager __result = attribute.GetValue(); return false; } + + /// + /// Patches the setter of a list property to update the value of the option. + /// + /// The list's instance. + /// The index to find in the list. + /// The new object value. +#pragma warning disable CA1707 + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] + public static void PropertyListSetterPatch(object __instance, int index, object value) +#pragma warning restore CA1707 + { + var attribute = (ModdedOptionListAttribute)OptionAttributes.First( + pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value; + attribute.SetValue(index, value); + } + + /// + /// Patches the getter of a list property to return the value of the option. + /// + /// The list's instance. + /// The index to find in the list. + /// The result of the property getter. + /// False so the original getter gets skipped. +#pragma warning disable CA1707 + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] + public static bool PropertyListGetterPatch(object __instance, int index, ref object __result) +#pragma warning restore CA1707 + { + var attribute = (ModdedOptionListAttribute)OptionAttributes.First( + pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value; + __result = attribute.GetValue(index); + return false; + } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 5fa0ceb..2291291 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -15,8 +15,6 @@ namespace MiraAPI.GameOptions.OptionTypes; /// The value type. public abstract class ModdedOption : IModdedOption { - private IMiraPlugin? _parentMod; - /// public uint Id { get; } @@ -32,13 +30,13 @@ public abstract class ModdedOption : IModdedOption /// public IMiraPlugin? ParentMod { - get => _parentMod; + get; set { - if (_parentMod != null || value == null) return; - _parentMod = value; + if (field != null || value == null) return; + field = value; - var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue); + var entry = field.GetConfigFile().Bind(ConfigDefinition, DefaultValue); Value = entry.Value; } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs new file mode 100644 index 0000000..5a764b2 --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace MiraAPI.GameOptions.OptionTypes; + +/// +/// Represents a modded option list. +/// +/// The type of options. +public class ModdedOptionList : IModdedOptionList where T : IModdedOption +{ + /// + public int Count { get; } + + /// + /// Gets the list of options. + /// + public IReadOnlyList Options { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The option list's length. + /// The option factory to instantiate the options from. + public ModdedOptionList(int count, Func optionFactory) + { + Count = count; + Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The options list. + public ModdedOptionList(IEnumerable options) + { + Count = options.Count(); + Options = options.ToArray(); + } + + /// + public IEnumerator GetEnumerator() + { + return ((IEnumerable)Options).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Options.GetEnumerator(); + } + + /// + /// Indexes the option of type . + /// + /// The option's index. + /// option of type . + public T this[int idx] => Options[idx]; + + IModdedOption IReadOnlyList.this[int index] => this[index]; +} diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index d5d8956..208c142 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -643,7 +643,7 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB /// public virtual void ResetTarget() { - Target = null; SetOutline(false); + Target = null; } } diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 930cc29..267e5f8 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -128,4 +128,12 @@ public abstract class BaseModifier : IOptionable /// /// if the player can vent, otherwise. for no effect. public virtual bool? CanVent() => null; + + /// + /// Removes this modifier instance from the player. + /// + public void RemoveSelf() + { + ModifierComponent?.RemoveModifier(this); + } } diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 1d7e5d7..d34580b 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -204,6 +204,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return GetModifiers(type, predicate); } + /// + /// Gets a collection of s by type, if the type is an interface. + /// + /// The predicate to check the by. + /// The Type of the interface of the . + /// An of s of type . + [HideFromIl2Cpp] + public IEnumerable GetModifiersOfType(Func? predicate = null) where T : class + { + return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x)); + } + /// /// Tries to get a . /// @@ -259,6 +271,20 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return modifier != null; } + /// + /// Tries to get a by its type, if the type is an interface. + /// + /// The of type or null. + /// The predicate to check the by. + /// The Type of the interface of the . + /// if the of type was found, otherwise. + [HideFromIl2Cpp] + public bool TryGetModifierOfType([NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : class + { + modifier = GetModifierOfType(predicate); + return modifier != null; + } + /// /// Gets a . /// @@ -309,6 +335,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return ActiveModifiers.Find(x => x.UniqueId == modifierGuid); } + /// + /// Gets a by its type, if the type is an interface. + /// + /// The predicate to check the by. + /// The Type of the interface of the . + /// The of type if it is found, otherwise. + [HideFromIl2Cpp] + public T? GetModifierOfType(Func? predicate = null) where T : class + { + return GetModifiersOfType(predicate).FirstOrDefault(); + } + /// /// Removes a from the player. /// @@ -390,6 +428,77 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) RemoveModifier(modifier); } + /// + /// Tries to remove a from the player. + /// + /// The type. + /// The predicate to check the by. + /// if the is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Func? predicate = null) where T : BaseModifier + { + return TryGetModifier(out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a from the player. + /// + /// The type. + /// The predicate to check the modifier by. + /// if the is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Type type, Func? predicate = null) + { + return TryGetModifier(type, out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a from the player. + /// + /// The object. + /// if the is not active on this player, else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(BaseModifier modifier) + { + if (!ActiveModifiers.Contains(modifier)) + { + return false; + } + + _toRemove.Add(modifier); + return true; + } + + /// + /// Tries to remove a from the player. + /// + /// The 's type ID. + /// The predicate to check the by. + /// if the is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(uint typeId, Func? predicate = null) + { + return TryGetModifier(typeId, out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a from the player. + /// + /// The 's unique ID. + /// if the is not active on this player, else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Guid uniqueId) + { + return TryGetModifier(uniqueId, out var modifier) && + TryRemoveModifier(modifier); + } + /// /// Adds a to the player. /// @@ -586,4 +695,30 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr)); bool MatchExpr(BaseModifier bm) => bm.UniqueId == id; } + + /// + /// Checks if a player has an active modifier by its type, if the type is an interface. + /// + /// The predicate to check the modifier. + /// The Type of the interface of the Modifier. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifierOfType(Func? predicate=null) where T : class + { + return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); + } + + /// + /// Checks if a player has an active modifier by its type, if the type is an interface. + /// + /// Whether to check inactive modifiers (those pending to be added). + /// The predicate to check the modifier. + /// The Type of the interface of the Modifier. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifierOfType(bool checkInactive, Func? predicate=null) where T : class + { + return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr)); + bool MatchExpr(BaseModifier bm) => bm is T modifier && (predicate == null || predicate(modifier)); + } } diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index e70a26a..4d69428 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -190,6 +190,18 @@ public static class ModifierExtensions return player.GetModifierComponent().HasModifier(uniqueId); } + /// + /// Checks if the has a specific , if the type is an interface. + /// + /// The type of the interface of the . + /// The instance. + /// Optional predicate to filter the s. + /// if the has the of type , otherwise. + public static bool HasModifierOfType(this PlayerControl player, Func? predicate = null) where T : class + { + return player.GetModifierComponent().HasModifierOfType(predicate); + } + /// /// Clears all s from the . /// @@ -265,6 +277,19 @@ public static class ModifierExtensions return player.GetModifierComponent().TryGetModifier(modifierGuid, out modifier); } + /// + /// Tries to get a by its type, if the type is an interface. + /// + /// The instance. + /// The of type or null. + /// The predicate to check the of type by. + /// The Type of the interface of the . + /// if the of type was found, otherwise. + public static bool TryGetModifierOfType(this PlayerControl player, [NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : class + { + return player.GetModifierComponent().TryGetModifierOfType(out modifier, predicate); + } + /// /// Gets a specific from the . /// @@ -320,6 +345,18 @@ public static class ModifierExtensions return player.GetModifierComponent().GetModifier(uniqueId); } + /// + /// Gets a specific of type from the , if the type is an interface. + /// + /// The type of the interface of the modifier. + /// The instance. + /// Optional predicate to filter the s. + /// The of type if found, otherwise. + public static T? GetModifierOfType(this PlayerControl player, Func? predicate = null) where T : class + { + return player.GetModifierComponent().GetModifierOfType(predicate); + } + /// /// Gets all s from the . /// @@ -363,6 +400,19 @@ public static class ModifierExtensions return player.GetModifierComponent().GetModifiers(typeId, predicate); } + /// + /// Gets all s of a specific type from the , if the type is an interface. + /// + /// The type of the interface of the s. + /// The PlayerControl instance. + /// Optional predicate to filter the s. + /// An of s of type . + public static IEnumerable GetModifiersOfType(this PlayerControl player, Func? predicate = null) + where T : class + { + return player.GetModifierComponent().GetModifiersOfType(predicate); + } + /// /// Removes a specific from the . /// @@ -422,6 +472,73 @@ public static class ModifierExtensions player.GetModifierComponent().RemoveModifier(uniqueId); } + /// + /// Tries to remove a specific from the . + /// + /// The instance. + /// The to remove. + /// if the is not active on this , else . + public static bool TryRemoveModifier(this PlayerControl player, BaseModifier modifier) + { + return player.GetModifierComponent().TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a specific from the . + /// + /// The type of the . + /// The instance. + /// Optional predicate to filter the s. + /// if the is not active on this , or there are multiple instances; + /// else . + public static bool TryRemoveModifier(this PlayerControl player, Func? predicate = null) + where T : BaseModifier + { + return player.GetModifierComponent().TryRemoveModifier(predicate); + } + + /// + /// Tries to remove a specific from the by type. + /// + /// The instance. + /// The type of the . + /// Optional predicate to filter the s. + /// if the is not active on this , or there are multiple instances; + /// else . + public static bool TryRemoveModifier(this PlayerControl player, Type type, Func? predicate = null) + { + return player.GetModifierComponent().TryRemoveModifier(type, predicate); + } + + /// + /// Tries to remove a specific from the by its type ID. + /// + /// The instance. + /// The type ID of the . + /// Optional predicate to filter the s. + /// if the is not active on this , or there are multiple instances; + /// else . + public static bool TryRemoveModifier( + this PlayerControl player, + uint typeId, + Func? predicate = null) + { + return player.GetModifierComponent().TryRemoveModifier(typeId, predicate); + } + + /// + /// Tries to remove a specific from the by its GUID. + /// + /// The instance. + /// The GUID of the . + /// if the is not active on this , else . + public static bool TryRemoveModifier( + this PlayerControl player, + Guid uniqueId) + { + return player.GetModifierComponent().TryRemoveModifier(uniqueId); + } + /// /// Adds a specific to the . /// diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 7c96f7d..5cc8a51 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -458,7 +458,7 @@ public static class LobbyViewPanePatches foreach (var grouping in sortedRoleGroups) { - if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings())) + if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings() || !x.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode))) { continue; } @@ -484,7 +484,7 @@ public static class LobbyViewPanePatches foreach (var customRole in grouping) { - if (customRole.Configuration.HideSettings || !customRole.VisibleInSettings()) + if (customRole.Configuration.HideSettings || !customRole.VisibleInSettings() || !customRole.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode)) { continue; } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 1a3fe4b..e2dbc91 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -330,15 +330,26 @@ public sealed class MiraPluginManager } var attribute = property.GetCustomAttribute(); - if (attribute == null) + if (attribute != null) { + ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo); continue; } - ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo); + if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList))) + { + ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo); + } + + var listAttr = property.GetCustomAttribute(); + if (listAttr != null) + { + ModdedOptionsManager.RegisterAttributeOptionList(type, listAttr, property, pluginInfo); + } } - foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption)))) + foreach (var field in type.GetFields() + .Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption)) || f.FieldType.IsAssignableTo(typeof(IModdedOptionList)))) { Error($"{field.Name} is a field, not a property. Use properties for options."); } diff --git a/MiraAPI/Roles/RoleId.cs b/MiraAPI/Roles/RoleId.cs index 189682b..96d2db8 100644 --- a/MiraAPI/Roles/RoleId.cs +++ b/MiraAPI/Roles/RoleId.cs @@ -1,4 +1,5 @@ -using System; +using AmongUs.GameOptions; +using System; namespace MiraAPI.Roles; @@ -38,4 +39,59 @@ public static class RoleId return roleId; } + + /// + /// Gets the ID of a role. + /// + /// The role. + /// The ID as a ushort. + /// Thrown when the role is not registered. + public static ushort Get(ICustomRole role) + { + return Get(role.GetType()); + } + + /// + /// Gets the ID of an . + /// + /// The type of the role. + /// The role ID as a . + /// Thrown when the role is not registered. + public static RoleTypes GetType() where T : ICustomRole + { + return (RoleTypes)Get(); + } + + /// + /// Gets the ID of a role. + /// + /// The type of the role. + /// The ID as a . + /// Thrown when the role is not registered. + public static RoleTypes GetType(Type type) + { + return (RoleTypes)Get(type); + } + + /// + /// Gets the role. + /// + /// The type of the role. + /// The object. + /// Thrown when the role is not registered. + public static RoleBehaviour GetRole() where T : ICustomRole + { + return RoleManager.Instance.GetRole(GetType()); + } + + /// + /// Gets the role from its type. + /// + /// The type of the role. + /// The object. + /// Thrown when the role is not registered. + public static RoleBehaviour GetRole(Type type) + { + return RoleManager.Instance.GetRole(GetType(type)); + } } diff --git a/MiraAPI/Utilities/Assets/LoadableAsset.cs b/MiraAPI/Utilities/Assets/LoadableAsset.cs index 7b0c032..f20bb05 100644 --- a/MiraAPI/Utilities/Assets/LoadableAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableAsset.cs @@ -21,6 +21,12 @@ public abstract class LoadableAsset where T : UnityEngine.Object /// The loaded asset. public abstract T LoadAsset(); + /// + /// Converts the into its by loading the asset. + /// + /// The to get the asset from. + public static implicit operator T(LoadableAsset loadable) => loadable.LoadAsset(); + /// /// Unloads an asset. /// diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index c31b9e5..d99c24b 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -43,7 +43,7 @@ public static class Extensions } return new NetData( - RoleId.Get(role.GetType()), + RoleId.Get(role), BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); }