diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index 54680f2c..ac3582b2 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -32,3 +32,33 @@ public override object GetValue() : 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/ModdedEnumOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs new file mode 100644 index 00000000..c8ec4472 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/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(Func titler, Type enumType, string[]? values = null) + : ModdedOptionListAttribute(titler) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList( + value.Count, idx => new(Titler(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 \"{Titler(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(Func titler, string[]? values = null) + : ModdedOptionListAttribute(titler) where T : Enum +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList>( + value.Count, idx => new(Titler(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 \"{Titler(idx)}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs new file mode 100644 index 00000000..18d7f37d --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/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( + Func titler, + float min, + float max, + float increment = 1, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null, + bool zeroInfinity = false) + : ModdedOptionListAttribute(titler) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + return new ModdedOptionList(value.Count, idx => + { + return new( + Titler(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 \"{Titler(idx)}\" is not a ModdedNumberOption"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index 702d75e3..62ce43b6 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -9,31 +9,19 @@ namespace MiraAPI.GameOptions.Attributes; /// The option title. /// Optional parameter to specify a role Type. [AttributeUsage(AttributeTargets.Property)] -public abstract class ModdedOptionAttribute(string title, Type? roleType = null) : Attribute +public abstract class ModdedOptionAttribute(string title, Type? roleType = 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. /// - protected Type? RoleType { get; private set; } = roleType; - - /// - /// 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(); + protected Type? RoleType => roleType; internal abstract IModdedOption? CreateOption(object? value, PropertyInfo property); } diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs new file mode 100644 index 00000000..74615ddd --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Represents an attribute that is used to define a list of modded options. +/// +/// A function to title of the options. +[AttributeUsage(AttributeTargets.Property)] +public abstract class ModdedOptionListAttribute(Func titler) : PropertyOptionAttribute +{ + internal IModdedOptionList? HolderOptionList { get; set; } + + internal object? Value { get; set; } + + /// + /// Gets the function to title of the options. + /// + public Func Titler => titler; + + /// + /// 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); +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs new file mode 100644 index 00000000..7e34080a --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/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 function to get each option's title. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedToggleOptionListAttribute(Func titler) : ModdedOptionListAttribute(titler) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList( + value.Count, idx => new(Titler(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 {Titler(idx)} is not a ModdedToggleOption."); + } +} diff --git a/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs new file mode 100644 index 00000000..643d7b7e --- /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 5a3c3f8f..e21c3b29 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; @@ -104,3 +105,10 @@ public interface IModdedOption /// The ConfigFile 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 3b85a73c..ffb85e30 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; @@ -20,7 +21,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 = []; @@ -113,6 +114,94 @@ internal static void RegisterAttributeOption( 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; + + for (int i = 0; i < optionList.Count; i++) + { + RegisterOption(optionList[i], group, property.Name + i, pluginInfo); + } + } + internal static void RegisterOption( IModdedOption option, AbstractOptionGroup group, @@ -202,4 +291,38 @@ public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object _ __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 206894dc..644c87c8 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; } } @@ -54,9 +52,9 @@ public IMiraPlugin? ParentMod public T DefaultValue { get; } /// - /// Gets or sets the event that is invoked when the value of the option changes. + /// Gets the event that is invoked when the value of the option changes. /// - public Action? ChangedEvent { get; set; } + public Action? ChangedEvent { get; init; } /// public Func Visible { get; set; } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs new file mode 100644 index 00000000..5a764b21 --- /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/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 0e31779c..bc65692e 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -128,4 +128,12 @@ public virtual void OnMeetingStart() /// /// True if the player can vent, false otherwise. Null 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 d64f3662..a152d0b7 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -203,6 +203,18 @@ public IEnumerable GetModifiers(uint id, Func? return GetModifiers(type, predicate); } + /// + /// Gets a collection of modifiers by type, if the type is an interface. + /// + /// The predicate to check the modifier by. + /// The Type of the interface of the Modifier. + /// A collection of modifiers. + [HideFromIl2Cpp] + public IEnumerable GetModifiersOfType(Func? predicate = null) where T : class + { + return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x)); + } + /// /// Tries to get a modifier by its type. /// @@ -258,6 +270,20 @@ public bool TryGetModifier(Guid modifierGuid, [NotNullWhen(true)] out BaseModifi return modifier != null; } + /// + /// Tries to get a modifier by its type, if the type is an interface. + /// + /// The modifier or null. + /// The predicate to check the modifier by. + /// The Type of the interface of the Modifier. + /// True if the modifier was found, false otherwise. + [HideFromIl2Cpp] + public bool TryGetModifierOfType([NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : class + { + modifier = GetModifierOfType(predicate); + return modifier != null; + } + /// /// Gets a modifier by its type. /// @@ -308,6 +334,18 @@ public bool TryGetModifier(Guid modifierGuid, [NotNullWhen(true)] out BaseModifi return ActiveModifiers.Find(x => x.UniqueId == modifierGuid); } + /// + /// Gets a modifier by its type, if the type is an interface. + /// + /// The predicate to check the modifier by. + /// The Type of the interface of the Modifier. + /// The Modifier if it is found, null otherwise. + [HideFromIl2Cpp] + public T? GetModifierOfType(Func? predicate = null) where T : class + { + return GetModifiersOfType(predicate).FirstOrDefault(); + } + /// /// Removes a modifier from the player. /// @@ -389,6 +427,77 @@ public void RemoveModifier(Guid uniqueId) RemoveModifier(modifier); } + /// + /// Tries to remove a modifier from the player. + /// + /// The modifier type. + /// The predicate to check the modifier by. + /// if the modifier 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 modifier from the player. + /// + /// The modifier type. + /// The predicate to check the modifier by. + /// if the modifier 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 modifier from the player. + /// + /// The modifier object. + /// if the modifier 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 modifier from the player. + /// + /// The modifier's type ID. + /// The predicate to check the modifier by. + /// if the modifier 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 modifier from the player. + /// + /// The modifier's unique ID. + /// if the modifier is not active on this player, else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Guid uniqueId) + { + return TryGetModifier(uniqueId, out var modifier) && + TryRemoveModifier(modifier); + } + /// /// Adds a modifier to the player. /// @@ -585,4 +694,30 @@ public bool HasModifier(Guid id, bool checkInactive) 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 7a0ef23f..cccba45a 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -191,6 +191,18 @@ public static bool HasModifier(this PlayerControl player, Guid uniqueId) return player.GetModifierComponent().HasModifier(uniqueId); } + /// + /// Checks if the player has a specific modifier, if the type is an interface. + /// + /// The type of the interface of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// True if the player has the modifier, false otherwise. + public static bool HasModifierOfType(this PlayerControl player, Func? predicate = null) where T : class + { + return player.GetModifierComponent().HasModifierOfType(predicate); + } + /// /// Clears all modifiers from a player. /// @@ -266,6 +278,19 @@ public static bool TryGetModifier(this PlayerControl player, Guid modifierGuid, return player.GetModifierComponent().TryGetModifier(modifierGuid, out modifier); } + /// + /// Tries to get a modifier by its type, if the type is an interface. + /// + /// The PlayerControl instance. + /// The modifier or null. + /// The predicate to check the modifier by. + /// The Type of the interface of the Modifier. + /// True if the modifier was found, false 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 modifier from the player. /// @@ -321,6 +346,18 @@ public static bool TryGetModifier(this PlayerControl player, Guid modifierGuid, return player.GetModifierComponent().GetModifier(uniqueId); } + /// + /// Gets a specific modifier from the player, if the type is an interface. + /// + /// The type of the interface of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// The modifier if found, null otherwise. + public static T? GetModifierOfType(this PlayerControl player, Func? predicate = null) where T : class + { + return player.GetModifierComponent().GetModifierOfType(predicate); + } + /// /// Gets all modifiers of a specific type from the player. /// @@ -364,6 +401,19 @@ public static IEnumerable GetModifiers( return player.GetModifierComponent().GetModifiers(typeId, predicate); } + /// + /// Gets all modifiers of a specific type from the player, if the type is an interface. + /// + /// The type of the interface of the modifiers. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// A collection of modifiers. + public static IEnumerable GetModifiersOfType(this PlayerControl player, Func? predicate = null) + where T : class + { + return player.GetModifierComponent().GetModifiersOfType(predicate); + } + /// /// Removes a specific modifier from the player. /// @@ -423,6 +473,73 @@ public static void RemoveModifier( player.GetModifierComponent().RemoveModifier(uniqueId); } + /// + /// Tries to remove a specific modifier from the player. + /// + /// The PlayerControl instance. + /// The modifier to remove. + /// if the modifier is not active on this player, else . + public static bool TryRemoveModifier(this PlayerControl player, BaseModifier modifier) + { + return player.GetModifierComponent().TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a specific modifier from the player. + /// + /// The type of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// if the modifier is not active on this player, 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 modifier from the player by type. + /// + /// The PlayerControl instance. + /// The type of the modifier. + /// Optional predicate to filter the modifiers. + /// if the modifier is not active on this player, 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 modifier from the player by its type ID. + /// + /// The PlayerControl instance. + /// The type ID of the modifier. + /// Optional predicate to filter the modifiers. + /// if the modifier is not active on this player, 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 modifier from the player by its GUID. + /// + /// The PlayerControl instance. + /// The GUID of the modifier. + /// if the modifier is not active on this player, else . + public static bool TryRemoveModifier( + this PlayerControl player, + Guid uniqueId) + { + return player.GetModifierComponent().TryRemoveModifier(uniqueId); + } + /// /// Adds a specific modifier to the player. /// diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 26a881f4..bb4e6cc7 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -201,15 +201,26 @@ private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo) } 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."); }