From d64eaddb0ea802ebe9e1307be36730c4a755c2f8 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:43:28 -0600 Subject: [PATCH 01/51] Added remove self method for modifiers --- MiraAPI/Modifiers/BaseModifier.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 0e31779..bc65692 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -128,4 +128,12 @@ public abstract class BaseModifier : IOptionable /// /// 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); + } } From 388b6ec676bdc3c7d613c61ddba44de1deb2456e Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:17:31 -0600 Subject: [PATCH 02/51] Added TryRemoveModifier methods to simplify get and remove modifiers --- MiraAPI/Modifiers/ModifierComponent.cs | 71 ++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index d64f366..b3c5622 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -389,6 +389,77 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) 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. /// From 29779f37113f9150563ff10cdf03dd6fd70af2b3 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:24:28 -0600 Subject: [PATCH 03/51] Added similar extensions with new try remove methods --- MiraAPI/Modifiers/ModifierExtensions.cs | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index 7a0ef23..bc3433f 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -423,6 +423,73 @@ public static class ModifierExtensions 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. /// From 34c172b6f1b58b09eac149f96b9a25370c4bb38a Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:37:22 -0600 Subject: [PATCH 04/51] Added methods to get modifiers from an interface type --- MiraAPI/Modifiers/ModifierComponent.cs | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index b3c5622..a152d0b 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -203,6 +203,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) 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 class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) 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 class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) 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. /// @@ -656,4 +694,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)); + } } From 01fe88ede9ee1ed4de23f6a8ffe427390696f89a Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:47:07 -0600 Subject: [PATCH 05/51] Added equivalent extensions of Modifier OfType methods --- MiraAPI/Modifiers/ModifierExtensions.cs | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index bc3433f..cccba45 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -191,6 +191,18 @@ public static class ModifierExtensions 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 class ModifierExtensions 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 class ModifierExtensions 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 class ModifierExtensions 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. /// From 690b1b2daef2bc66343b5af633f0c2a723ec5cad Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:23:27 -0600 Subject: [PATCH 06/51] Added generic Enum Option attribute --- .../Attributes/ModdedEnumOptionAttribute.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index 54680f2..ac3582b 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -32,3 +32,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"); + } +} From 42eba6280da3778ccf496f5eb3fd4acbd1be1d32 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:10:09 -0600 Subject: [PATCH 07/51] Created interface for sets of equal/comparable modded options --- MiraAPI/GameOptions/IModdedOptionList.cs | 122 +++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 MiraAPI/GameOptions/IModdedOptionList.cs diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs new file mode 100644 index 0000000..da6da53 --- /dev/null +++ b/MiraAPI/GameOptions/IModdedOptionList.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using BepInEx.Configuration; +using MiraAPI.Networking; +using MiraAPI.PluginLoading; +using UnityEngine; + +namespace MiraAPI.GameOptions; + +/// +/// Interface for list of modded options. +/// +public interface IModdedOptionList +{ + /// + /// Gets the number of options in the list. + /// + int Count { get; } + + /// + /// Gets the unique identifiers for the options. + /// + IReadOnlyList Ids { get; } + + /// + /// Gets or sets the titles of the options. + /// The end title will equal this value with its index appended (e.g., Title1). + /// + string Title { get; set; } + + /// + /// Gets or sets a value indicating whether the first option's title has a 0, else starts with 1. + /// + bool ZeroIndexTitle { get; set; } + + /// + /// Gets the StringName for the options, used for localization. + /// + IReadOnlyList StringNames { get; } + + /// + /// Gets or sets the MiraPlugin that created these options. + /// + IMiraPlugin? ParentMod { get; set; } + + /// + /// Gets the game setting data for the options. + /// + IReadOnlyList Data { get; } + + /// + /// Gets the OptionBehaviour object of the options. + /// + IReadOnlyList OptionBehaviours { get; } + + /// + /// Gets or sets the visibility function for the options. + /// + Func Visible { get; set; } + + /// + /// Gets or sets a value indicating whether the options should be included with presets. + /// + bool IncludeInPreset { get; set; } + + /// + /// Gets the array of ConfigDefinition for the options, used for BepInEx configuration. + /// + ConfigDefinition?[] ConfigDefinitions { get; } + + /// + /// Creates the option behaviour for the modded option at index . + /// + /// The option's index. + /// The ToggleOption template. + /// The NumberOption template. + /// The StringOption template. + /// The PlayerOption template. + /// >The Transform container for the option. + /// The created OptionBehaviour object. + OptionBehaviour CreateOption(int idx, ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container); + + /// + /// Gets the value at index as a float. + /// + /// The option's index. + /// The value of the option as a float. + float GetFloatData(int idx); + + /// + /// Gets the NetData for the option at index , used for network synchronization. + /// + /// The option's index. + /// Returns the NetData object for the option. + NetData GetNetData(int idx); + + /// + /// Handles incoming network data for the option at index . + /// + /// The option's index. + /// The byte array representing the network data. + void HandleNetData(int idx, byte[] data); + + /// + /// Saves the options to a preset configuration file. + /// + /// The ConfigFile representing the preset configuration. + /// Indicates whether to save the default value instead of the current value. + void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false); + + /// + /// Binds the options to a configuration file. + /// + /// The ConfigFile to bind the option to. + void Bind(ConfigFile config); + + /// + /// Loads the options from a preset configuration file, applying the values to the options' configuration. + /// + /// The ConfigFile representing the preset configuration. + void LoadFromPreset(ConfigFile presetConfig); +} From 612597566b773676500d8acbbd09c9bb52eae2cd Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 21:35:03 -0600 Subject: [PATCH 08/51] Created abstract class for Option list --- .../OptionTypes/ModdedOptionList.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs new file mode 100644 index 0000000..be14e14 --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using BepInEx.Configuration; +using MiraAPI.Networking; +using MiraAPI.PluginLoading; +using Reactor.Localization.Utilities; +using Reactor.Networking.Rpc; +using Reactor.Utilities; +using UnityEngine; + +namespace MiraAPI.GameOptions.OptionTypes; + +/// +/// Represents a modded option list. +/// +public abstract class ModdedOptionList : IModdedOptionList +{ + protected IMiraPlugin? _parentMod; + + /// + public int Count { get; } + + /// + public IReadOnlyList Ids { get; } + + /// + public string Title { get; set; } + + /// + public bool ZeroIndexTitle { get; set; } + + /// + public IReadOnlyList StringNames { get; } + + /// + public IReadOnlyList Data { get; protected set; } = []; + + /// + public IMiraPlugin? ParentMod + { + get => _parentMod; + set + { + if (_parentMod != null || value == null) return; + _parentMod = value; + + OnParentModChange(); + } + } + + protected abstract void OnParentModChange(); + + /// + public Func Visible { get; set; } + + /// + public bool IncludeInPreset { get; set; } + + protected readonly OptionBehaviour?[] _optionBehaviours; + + /// + public IReadOnlyList OptionBehaviours => _optionBehaviours; + + /// + public ConfigDefinition?[] ConfigDefinitions { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The options' title. + /// The option list's length. + /// Whether to include the options in the preset. + /// Whether the first option's title index is 0, else 1. + protected ModdedOptionList(string title, int count, bool includeInPreset = true, bool zeroIndexTitle = false) + { + Count = count; + ZeroIndexTitle = zeroIndexTitle; + + var range = Enumerable.Range(0, Count); + var titleOffset = ZeroIndexTitle ? 0 : 1; + + Ids = range.Select(_ => ModdedOptionsManager.NextId).ToList(); + Title = title; + StringNames = range.Select(i => CustomStringName.CreateAndRegister($"{Title}{i + titleOffset}")).ToList(); + Visible = _ => true; + IncludeInPreset = includeInPreset; + + _optionBehaviours = range.Select(_ => null).ToArray(); + ConfigDefinitions = range.Select(_ => null).ToArray(); + } + + /// + public abstract void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false); + + /// + public abstract void Bind(ConfigFile config); + + /// + public abstract void LoadFromPreset(ConfigFile presetConfig); + + /// + public abstract float GetFloatData(int idx); + + /// + public abstract NetData GetNetData(int idx); + + /// + public abstract void HandleNetData(int idx, byte[] data); + + /// + public abstract OptionBehaviour CreateOption( + int idx, + ToggleOption toggleOpt, + NumberOption numberOpt, + StringOption stringOpt, + PlayerOption playerOpt, + Transform container); +} From 71a34614326a6cd0f5ebbf33d5e2317e3514194d Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 21:36:16 -0600 Subject: [PATCH 09/51] Created subclass with generic values --- .../OptionTypes/ModdedOptionList.cs | 147 +++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index be14e14..4c61485 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -34,8 +34,10 @@ public abstract class ModdedOptionList : IModdedOptionList /// public IReadOnlyList StringNames { get; } + protected readonly BaseGameSetting[] _data; + /// - public IReadOnlyList Data { get; protected set; } = []; + public IReadOnlyList Data => _data; /// public IMiraPlugin? ParentMod @@ -118,3 +120,146 @@ public abstract class ModdedOptionList : IModdedOptionList PlayerOption playerOpt, Transform container); } + +/// +/// Represents a modded option list. +/// +/// The value type. +public abstract class ModdedOptionList : ModdedOptionList +{ + protected override void OnParentModChange() + { + var configFile = _parentMod!.GetConfigFile(); + for (int i = 0; i < Count; i++) + { + var entry = configFile.Bind(ConfigDefinitions[i], DefaultValue(i)); + Values[i] = entry.Value; + } + } + + /// + /// Gets the list of values of the options. + /// + public T[] Values { get; } + + /// + /// Gets the list of default value of the options. + /// + public Func DefaultValue { get; } + + /// + /// Gets or sets the event that is invoked when the value of an option changes. + /// + public Action? ChangedEvent { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The options' title. + /// The option list's length. + /// The default values. + /// Whether to include the options in the preset. + /// Whether the first option's title index is 0, else 1. + protected ModdedOptionList(string title, int count, Func defaultValues, bool includeInPreset = true, bool zeroIndexTitle = false) + : base(title, count, includeInPreset, zeroIndexTitle) + { + DefaultValue = defaultValues; + Values = Enumerable.Range(0, Count).Select(defaultValues).ToArray(); + } + + internal void ValueChanged(int idx, OptionBehaviour optionBehaviour) + { + SetValue(idx, GetValueFromOptionBehaviour(idx, optionBehaviour)); + } + + /// + /// Sets the value of the option. + /// + /// The option's index. + /// The new value. + /// Whether to send the value to other players. + public void SetValue(int idx, T newValue, bool sendRpc = true) + { + var oldVal = Values[idx]; + Values[idx] = newValue; + + if (Values[idx]?.Equals(oldVal) == false) + { + ChangedEvent?.Invoke(idx, Values[idx]); + } + + if (sendRpc && AmongUsClient.Instance.AmHost) + { + if (ParentMod?.GetConfigFile().TryGetEntry(ConfigDefinitions[idx], out var entry) == true) + { + entry.Value = Values[idx]; + } + + Rpc.Instance.Send(PlayerControl.LocalPlayer, [GetNetData(idx)], true); // This might not work + } + + OnValueChanged(idx, newValue); + } + + /// + public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) + { + if (ConfigDefinitions.Any(d => d is null)) + { + Error($"Attempted to save {Title} to preset, but some ConfigDefinitions are null."); + return; + } + Bind(presetConfig); + for (int i = 0; i < Count; i++) + { + presetConfig[ConfigDefinitions[i]].BoxedValue = saveDefault ? DefaultValue(i) : Values[i]; + } + } + + /// + public override void Bind(ConfigFile config) + { + for (int i = 0; i < Count; i++) + { + config.Bind(ConfigDefinitions[i], DefaultValue(i)); + } + } + + /// + public override void LoadFromPreset(ConfigFile presetConfig) + { + for (int i = 0; i < Count; i++) + { + if (presetConfig.TryGetEntry(ConfigDefinitions[i], out ConfigEntry entry)) + { + SetValue(i, entry.Value, false); + } + else + { + Error($"Attempted to load {Title} from preset, but ConfigDefinition {i} is not found in preset."); + } + } + } + + /// + /// Handles the value changed event. + /// + /// The option's index. + /// The new value. + protected abstract void OnValueChanged(int idx, T newValue); + + /// + /// Gets the value from the option behaviour. + /// + /// The option's index. + /// The OptionBehaviour. + /// The value. + public abstract T GetValueFromOptionBehaviour(int idx, OptionBehaviour optionBehaviour); + + /// + /// Indexes the option for its value of type . + /// + /// The option's index. + /// value of type . + public T this[int idx] => Values[idx]; +} From f9a33ce10996b7c638ee3be2b3658fee620c4c3d Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:32:16 -0600 Subject: [PATCH 10/51] Created concrete class for nested IModdedOptions that aren't the concrete class --- .../OptionTypes/ModdedOptionList.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 4c61485..927bcb2 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -263,3 +263,101 @@ public abstract class ModdedOptionList : ModdedOptionList /// value of type . public T this[int idx] => Values[idx]; } + +/// +/// Represents a modded option list. +/// +/// The option's type. +public class ModdedOptionsList : ModdedOptionList where T : IModdedOption +{ + protected override void OnParentModChange() + { + foreach (var option in Options) + { + option.ParentMod = _parentMod!; + } + } + + /// + /// Gets the list of options. + /// + public T[] Options { get; } + + /// + /// Gets the default option from its index. + /// + public Func DefaultOption { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The options' title. + /// The option list's length. + /// The default option. + /// Whether to include the options in the preset. + /// Whether the first option's title index is 0, else 1. + protected ModdedOptionsList(string title, int count, Func defaultOption, bool includeInPreset = true, bool zeroIndexTitle = false) + : base(title, count, includeInPreset, zeroIndexTitle) + { + DefaultOption = defaultOption; + Options = Enumerable.Range(0, Count).Select(defaultOption).ToArray(); + } + + /// + public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) + { + foreach (var option in Options) + { + option.SaveToPreset(presetConfig, saveDefault); + } + } + + /// + public override void Bind(ConfigFile config) + { + foreach (var option in Options) + { + option.Bind(config); + } + } + + /// + public override void LoadFromPreset(ConfigFile presetConfig) + { + foreach (var option in Options) + { + option.LoadFromPreset(presetConfig); + } + } + + /// + public override float GetFloatData(int idx) + { + return Options[idx].GetFloatData(); + } + + /// + public override NetData GetNetData(int idx) + { + return Options[idx].GetNetData(); + } + + /// + public override void HandleNetData(int idx, byte[] data) + { + Options[idx].HandleNetData(data); + } + + /// + public override OptionBehaviour CreateOption(int idx, ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) + { + return _optionBehaviours[idx] = Options[idx].CreateOption(toggleOpt, numberOpt, stringOpt, playerOpt, container); + } + + /// + /// Indexes the option of type . + /// + /// The option's index. + /// option of type . + public T this[int idx] => Options[idx]; +} From b0be07192e024faa9a9071bc9c196f2a61ba9767 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:12:08 -0600 Subject: [PATCH 11/51] Removed middleman classes and fields that can be handled much easier --- MiraAPI/GameOptions/IModdedOptionList.cs | 49 +-- .../OptionTypes/ModdedOptionList.cs | 316 ++---------------- 2 files changed, 39 insertions(+), 326 deletions(-) diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs index da6da53..26f9bee 100644 --- a/MiraAPI/GameOptions/IModdedOptionList.cs +++ b/MiraAPI/GameOptions/IModdedOptionList.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using BepInEx.Configuration; -using MiraAPI.Networking; using MiraAPI.PluginLoading; -using UnityEngine; namespace MiraAPI.GameOptions; @@ -23,15 +21,9 @@ public interface IModdedOptionList IReadOnlyList Ids { get; } /// - /// Gets or sets the titles of the options. - /// The end title will equal this value with its index appended (e.g., Title1). + /// Gets the titles of the options. /// - string Title { get; set; } - - /// - /// Gets or sets a value indicating whether the first option's title has a 0, else starts with 1. - /// - bool ZeroIndexTitle { get; set; } + IReadOnlyList Titles { get; } /// /// Gets the StringName for the options, used for localization. @@ -64,42 +56,9 @@ public interface IModdedOptionList bool IncludeInPreset { get; set; } /// - /// Gets the array of ConfigDefinition for the options, used for BepInEx configuration. + /// Gets the set of ConfigDefinition for the options, used for BepInEx configuration. /// - ConfigDefinition?[] ConfigDefinitions { get; } - - /// - /// Creates the option behaviour for the modded option at index . - /// - /// The option's index. - /// The ToggleOption template. - /// The NumberOption template. - /// The StringOption template. - /// The PlayerOption template. - /// >The Transform container for the option. - /// The created OptionBehaviour object. - OptionBehaviour CreateOption(int idx, ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container); - - /// - /// Gets the value at index as a float. - /// - /// The option's index. - /// The value of the option as a float. - float GetFloatData(int idx); - - /// - /// Gets the NetData for the option at index , used for network synchronization. - /// - /// The option's index. - /// Returns the NetData object for the option. - NetData GetNetData(int idx); - - /// - /// Handles incoming network data for the option at index . - /// - /// The option's index. - /// The byte array representing the network data. - void HandleNetData(int idx, byte[] data); + IReadOnlyList ConfigDefinitions { get; } /// /// Saves the options to a preset configuration file. diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 927bcb2..aca5b28 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -1,43 +1,33 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using BepInEx.Configuration; -using MiraAPI.Networking; using MiraAPI.PluginLoading; -using Reactor.Localization.Utilities; -using Reactor.Networking.Rpc; -using Reactor.Utilities; -using UnityEngine; namespace MiraAPI.GameOptions.OptionTypes; /// /// Represents a modded option list. /// -public abstract class ModdedOptionList : IModdedOptionList +/// The type of options. +public class ModdedOptionList : IModdedOptionList where T : IModdedOption { - protected IMiraPlugin? _parentMod; + private IMiraPlugin? _parentMod; /// public int Count { get; } /// - public IReadOnlyList Ids { get; } + public IReadOnlyList Ids => FromOptions(o => o.Id); /// - public string Title { get; set; } - - /// - public bool ZeroIndexTitle { get; set; } + public IReadOnlyList Titles => FromOptions(o => o.Title); /// - public IReadOnlyList StringNames { get; } - - protected readonly BaseGameSetting[] _data; + public IReadOnlyList StringNames => FromOptions(o => o.StringName); /// - public IReadOnlyList Data => _data; + public IReadOnlyList Data => FromOptions(o => o.Data); /// public IMiraPlugin? ParentMod @@ -48,233 +38,10 @@ public abstract class ModdedOptionList : IModdedOptionList if (_parentMod != null || value == null) return; _parentMod = value; - OnParentModChange(); - } - } - - protected abstract void OnParentModChange(); - - /// - public Func Visible { get; set; } - - /// - public bool IncludeInPreset { get; set; } - - protected readonly OptionBehaviour?[] _optionBehaviours; - - /// - public IReadOnlyList OptionBehaviours => _optionBehaviours; - - /// - public ConfigDefinition?[] ConfigDefinitions { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The options' title. - /// The option list's length. - /// Whether to include the options in the preset. - /// Whether the first option's title index is 0, else 1. - protected ModdedOptionList(string title, int count, bool includeInPreset = true, bool zeroIndexTitle = false) - { - Count = count; - ZeroIndexTitle = zeroIndexTitle; - - var range = Enumerable.Range(0, Count); - var titleOffset = ZeroIndexTitle ? 0 : 1; - - Ids = range.Select(_ => ModdedOptionsManager.NextId).ToList(); - Title = title; - StringNames = range.Select(i => CustomStringName.CreateAndRegister($"{Title}{i + titleOffset}")).ToList(); - Visible = _ => true; - IncludeInPreset = includeInPreset; - - _optionBehaviours = range.Select(_ => null).ToArray(); - ConfigDefinitions = range.Select(_ => null).ToArray(); - } - - /// - public abstract void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false); - - /// - public abstract void Bind(ConfigFile config); - - /// - public abstract void LoadFromPreset(ConfigFile presetConfig); - - /// - public abstract float GetFloatData(int idx); - - /// - public abstract NetData GetNetData(int idx); - - /// - public abstract void HandleNetData(int idx, byte[] data); - - /// - public abstract OptionBehaviour CreateOption( - int idx, - ToggleOption toggleOpt, - NumberOption numberOpt, - StringOption stringOpt, - PlayerOption playerOpt, - Transform container); -} - -/// -/// Represents a modded option list. -/// -/// The value type. -public abstract class ModdedOptionList : ModdedOptionList -{ - protected override void OnParentModChange() - { - var configFile = _parentMod!.GetConfigFile(); - for (int i = 0; i < Count; i++) - { - var entry = configFile.Bind(ConfigDefinitions[i], DefaultValue(i)); - Values[i] = entry.Value; - } - } - - /// - /// Gets the list of values of the options. - /// - public T[] Values { get; } - - /// - /// Gets the list of default value of the options. - /// - public Func DefaultValue { get; } - - /// - /// Gets or sets the event that is invoked when the value of an option changes. - /// - public Action? ChangedEvent { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The options' title. - /// The option list's length. - /// The default values. - /// Whether to include the options in the preset. - /// Whether the first option's title index is 0, else 1. - protected ModdedOptionList(string title, int count, Func defaultValues, bool includeInPreset = true, bool zeroIndexTitle = false) - : base(title, count, includeInPreset, zeroIndexTitle) - { - DefaultValue = defaultValues; - Values = Enumerable.Range(0, Count).Select(defaultValues).ToArray(); - } - - internal void ValueChanged(int idx, OptionBehaviour optionBehaviour) - { - SetValue(idx, GetValueFromOptionBehaviour(idx, optionBehaviour)); - } - - /// - /// Sets the value of the option. - /// - /// The option's index. - /// The new value. - /// Whether to send the value to other players. - public void SetValue(int idx, T newValue, bool sendRpc = true) - { - var oldVal = Values[idx]; - Values[idx] = newValue; - - if (Values[idx]?.Equals(oldVal) == false) - { - ChangedEvent?.Invoke(idx, Values[idx]); - } - - if (sendRpc && AmongUsClient.Instance.AmHost) - { - if (ParentMod?.GetConfigFile().TryGetEntry(ConfigDefinitions[idx], out var entry) == true) + foreach (var option in Options) { - entry.Value = Values[idx]; + option.ParentMod = _parentMod!; } - - Rpc.Instance.Send(PlayerControl.LocalPlayer, [GetNetData(idx)], true); // This might not work - } - - OnValueChanged(idx, newValue); - } - - /// - public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) - { - if (ConfigDefinitions.Any(d => d is null)) - { - Error($"Attempted to save {Title} to preset, but some ConfigDefinitions are null."); - return; - } - Bind(presetConfig); - for (int i = 0; i < Count; i++) - { - presetConfig[ConfigDefinitions[i]].BoxedValue = saveDefault ? DefaultValue(i) : Values[i]; - } - } - - /// - public override void Bind(ConfigFile config) - { - for (int i = 0; i < Count; i++) - { - config.Bind(ConfigDefinitions[i], DefaultValue(i)); - } - } - - /// - public override void LoadFromPreset(ConfigFile presetConfig) - { - for (int i = 0; i < Count; i++) - { - if (presetConfig.TryGetEntry(ConfigDefinitions[i], out ConfigEntry entry)) - { - SetValue(i, entry.Value, false); - } - else - { - Error($"Attempted to load {Title} from preset, but ConfigDefinition {i} is not found in preset."); - } - } - } - - /// - /// Handles the value changed event. - /// - /// The option's index. - /// The new value. - protected abstract void OnValueChanged(int idx, T newValue); - - /// - /// Gets the value from the option behaviour. - /// - /// The option's index. - /// The OptionBehaviour. - /// The value. - public abstract T GetValueFromOptionBehaviour(int idx, OptionBehaviour optionBehaviour); - - /// - /// Indexes the option for its value of type . - /// - /// The option's index. - /// value of type . - public T this[int idx] => Values[idx]; -} - -/// -/// Represents a modded option list. -/// -/// The option's type. -public class ModdedOptionsList : ModdedOptionList where T : IModdedOption -{ - protected override void OnParentModChange() - { - foreach (var option in Options) - { - option.ParentMod = _parentMod!; } } @@ -283,28 +50,39 @@ public class ModdedOptionsList : ModdedOptionList where T : IModdedOption /// public T[] Options { get; } - /// - /// Gets the default option from its index. - /// - public Func DefaultOption { get; } + /// + public Func Visible { get; set; } + + /// + public bool IncludeInPreset { get; set; } + + /// + public IReadOnlyList OptionBehaviours => FromOptions(o => o.OptionBehaviour); + + /// + public IReadOnlyList ConfigDefinitions => FromOptions(o => o.ConfigDefinition); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The options' title. /// The option list's length. - /// The default option. + /// The option factory to instantiate the options from. /// Whether to include the options in the preset. - /// Whether the first option's title index is 0, else 1. - protected ModdedOptionsList(string title, int count, Func defaultOption, bool includeInPreset = true, bool zeroIndexTitle = false) - : base(title, count, includeInPreset, zeroIndexTitle) + public ModdedOptionList(int count, Func optionFactory, bool includeInPreset = true) { - DefaultOption = defaultOption; - Options = Enumerable.Range(0, Count).Select(defaultOption).ToArray(); + Count = count; + Visible = _ => true; + IncludeInPreset = includeInPreset; + Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray(); + } + + internal IReadOnlyList FromOptions(Func getter) + { + return Options.Select(getter).ToArray(); } /// - public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) + public void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) { foreach (var option in Options) { @@ -313,7 +91,7 @@ public class ModdedOptionsList : ModdedOptionList where T : IModdedOption } /// - public override void Bind(ConfigFile config) + public void Bind(ConfigFile config) { foreach (var option in Options) { @@ -322,7 +100,7 @@ public class ModdedOptionsList : ModdedOptionList where T : IModdedOption } /// - public override void LoadFromPreset(ConfigFile presetConfig) + public void LoadFromPreset(ConfigFile presetConfig) { foreach (var option in Options) { @@ -330,30 +108,6 @@ public class ModdedOptionsList : ModdedOptionList where T : IModdedOption } } - /// - public override float GetFloatData(int idx) - { - return Options[idx].GetFloatData(); - } - - /// - public override NetData GetNetData(int idx) - { - return Options[idx].GetNetData(); - } - - /// - public override void HandleNetData(int idx, byte[] data) - { - Options[idx].HandleNetData(data); - } - - /// - public override OptionBehaviour CreateOption(int idx, ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) - { - return _optionBehaviours[idx] = Options[idx].CreateOption(toggleOpt, numberOpt, stringOpt, playerOpt, container); - } - /// /// Indexes the option of type . /// From be1d77bc6caef69d217e5578450fc1246a85e56b Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:13:10 -0600 Subject: [PATCH 12/51] Removed readonly properties that can be more easily replicated with the indexer --- MiraAPI/GameOptions/IModdedOptionList.cs | 31 ------------------- .../OptionTypes/ModdedOptionList.cs | 23 -------------- 2 files changed, 54 deletions(-) diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs index 26f9bee..4fed3b3 100644 --- a/MiraAPI/GameOptions/IModdedOptionList.cs +++ b/MiraAPI/GameOptions/IModdedOptionList.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using BepInEx.Configuration; using MiraAPI.PluginLoading; @@ -15,36 +14,11 @@ public interface IModdedOptionList /// int Count { get; } - /// - /// Gets the unique identifiers for the options. - /// - IReadOnlyList Ids { get; } - - /// - /// Gets the titles of the options. - /// - IReadOnlyList Titles { get; } - - /// - /// Gets the StringName for the options, used for localization. - /// - IReadOnlyList StringNames { get; } - /// /// Gets or sets the MiraPlugin that created these options. /// IMiraPlugin? ParentMod { get; set; } - /// - /// Gets the game setting data for the options. - /// - IReadOnlyList Data { get; } - - /// - /// Gets the OptionBehaviour object of the options. - /// - IReadOnlyList OptionBehaviours { get; } - /// /// Gets or sets the visibility function for the options. /// @@ -55,11 +29,6 @@ public interface IModdedOptionList /// bool IncludeInPreset { get; set; } - /// - /// Gets the set of ConfigDefinition for the options, used for BepInEx configuration. - /// - IReadOnlyList ConfigDefinitions { get; } - /// /// Saves the options to a preset configuration file. /// diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index aca5b28..7ff88bf 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -17,18 +17,6 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption /// public int Count { get; } - /// - public IReadOnlyList Ids => FromOptions(o => o.Id); - - /// - public IReadOnlyList Titles => FromOptions(o => o.Title); - - /// - public IReadOnlyList StringNames => FromOptions(o => o.StringName); - - /// - public IReadOnlyList Data => FromOptions(o => o.Data); - /// public IMiraPlugin? ParentMod { @@ -56,12 +44,6 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption /// public bool IncludeInPreset { get; set; } - /// - public IReadOnlyList OptionBehaviours => FromOptions(o => o.OptionBehaviour); - - /// - public IReadOnlyList ConfigDefinitions => FromOptions(o => o.ConfigDefinition); - /// /// Initializes a new instance of the class. /// @@ -76,11 +58,6 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray(); } - internal IReadOnlyList FromOptions(Func getter) - { - return Options.Select(getter).ToArray(); - } - /// public void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) { From beb7443c9a974fdb11039b28bd26a1fa30d2eb64 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:16:38 -0600 Subject: [PATCH 13/51] Extended from IReadOnlyList for potential easier iteration --- .../GameOptions/OptionTypes/ModdedOptionList.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 7ff88bf..cc7bdfd 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using BepInEx.Configuration; @@ -10,7 +11,7 @@ namespace MiraAPI.GameOptions.OptionTypes; /// Represents a modded option list. /// /// The type of options. -public class ModdedOptionList : IModdedOptionList where T : IModdedOption +public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : IModdedOption { private IMiraPlugin? _parentMod; @@ -36,7 +37,7 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption /// /// Gets the list of options. /// - public T[] Options { get; } + public IReadOnlyList Options { get; } /// public Func Visible { get; set; } @@ -85,6 +86,17 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption } } + /// + public IEnumerator GetEnumerator() + { + return ((IEnumerable)Options).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Options.GetEnumerator(); + } + /// /// Indexes the option of type . /// From f9f7b7e667fc700462bdc4a0b794efb7adcf18e1 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:30:58 -0600 Subject: [PATCH 14/51] Expanded setters to set the inner options values as well --- .../OptionTypes/ModdedOptionList.cs | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index cc7bdfd..a32a727 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -13,23 +13,21 @@ namespace MiraAPI.GameOptions.OptionTypes; /// The type of options. public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : IModdedOption { - private IMiraPlugin? _parentMod; - /// public int Count { get; } /// public IMiraPlugin? ParentMod { - get => _parentMod; + get; set { - if (_parentMod != null || value == null) return; - _parentMod = value; + if (field != null || value == null) return; + field = value; foreach (var option in Options) { - option.ParentMod = _parentMod!; + option.ParentMod = value; } } } @@ -40,10 +38,32 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : public IReadOnlyList Options { get; } /// - public Func Visible { get; set; } + public Func Visible + { + get; + set + { + field = value; + foreach (var (option, idx) in Options.Select((o, i) => (o, i))) + { + option.Visible = () => value(idx); + } + } + } /// - public bool IncludeInPreset { get; set; } + public bool IncludeInPreset + { + get; + set + { + field = value; + foreach (var option in Options) + { + option.IncludeInPreset = value; + } + } + } /// /// Initializes a new instance of the class. From 007c2020587549b94b45901765610d9e89f43a2b Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 20 Jun 2026 00:52:07 -0600 Subject: [PATCH 15/51] Changed properties to init instead of set to allow using object initializer but not changing the values later --- MiraAPI/GameOptions/IModdedOptionList.cs | 10 ++++++---- .../GameOptions/OptionTypes/ModdedOptionList.cs | 17 +++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs index 4fed3b3..62dd7d5 100644 --- a/MiraAPI/GameOptions/IModdedOptionList.cs +++ b/MiraAPI/GameOptions/IModdedOptionList.cs @@ -20,14 +20,16 @@ public interface IModdedOptionList IMiraPlugin? ParentMod { get; set; } /// - /// Gets or sets the visibility function for the options. + /// Gets the visibility function for the options. + /// Leaving it null will leave the options unchanged. /// - Func Visible { get; set; } + Func? Visible { get; init; } /// - /// Gets or sets a value indicating whether the options should be included with presets. + /// Gets a value indicating whether the options should be included with presets. + /// Leaving it null will leave the options unchanged. /// - bool IncludeInPreset { get; set; } + bool? IncludeInPreset { get; init; } /// /// Saves the options to a preset configuration file. diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index a32a727..7e97a1a 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -38,12 +38,13 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : public IReadOnlyList Options { get; } /// - public Func Visible + public Func? Visible { get; - set + init { field = value; + if (value == null) return; foreach (var (option, idx) in Options.Select((o, i) => (o, i))) { option.Visible = () => value(idx); @@ -52,15 +53,16 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : } /// - public bool IncludeInPreset + public bool? IncludeInPreset { get; - set + init { field = value; + if (value == null) return; foreach (var option in Options) { - option.IncludeInPreset = value; + option.IncludeInPreset = value.Value; } } } @@ -71,12 +73,11 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : /// The option list's length. /// The option factory to instantiate the options from. /// Whether to include the options in the preset. - public ModdedOptionList(int count, Func optionFactory, bool includeInPreset = true) + public ModdedOptionList(int count, Func optionFactory, bool? includeInPreset = true) { Count = count; - Visible = _ => true; - IncludeInPreset = includeInPreset; Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray(); + IncludeInPreset = includeInPreset; } /// From 93d5ebf15a38a368fbcdb1db668587180bcf24b7 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 20 Jun 2026 01:20:08 -0600 Subject: [PATCH 16/51] Removed Preset fields from interface that won't be used --- MiraAPI/GameOptions/IModdedOption.cs | 28 ++++++++++ MiraAPI/GameOptions/IModdedOptionList.cs | 52 ------------------- .../OptionTypes/ModdedOptionList.cs | 28 ---------- 3 files changed, 28 insertions(+), 80 deletions(-) delete mode 100644 MiraAPI/GameOptions/IModdedOptionList.cs diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 5a3c3f8..824a832 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -104,3 +104,31 @@ public interface IModdedOption /// The ConfigFile representing the preset configuration. void LoadFromPreset(ConfigFile presetConfig); } + +/// +/// Interface for list of modded options. +/// +public interface IModdedOptionList +{ + /// + /// Gets or sets the MiraPlugin that created this list of options. + /// + IMiraPlugin? ParentMod { get; set; } + + /// + /// Gets the number of options in the list. + /// + int Count { get; } + + /// + /// Gets the visibility function for the options. + /// Leaving it null will leave the options unchanged. + /// + Func? Visible { get; init; } + + /// + /// Gets a value indicating whether the options should be included with presets. + /// Leaving it null will leave the options unchanged. + /// + bool? IncludeInPreset { get; init; } +} diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs deleted file mode 100644 index 62dd7d5..0000000 --- a/MiraAPI/GameOptions/IModdedOptionList.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using BepInEx.Configuration; -using MiraAPI.PluginLoading; - -namespace MiraAPI.GameOptions; - -/// -/// Interface for list of modded options. -/// -public interface IModdedOptionList -{ - /// - /// Gets the number of options in the list. - /// - int Count { get; } - - /// - /// Gets or sets the MiraPlugin that created these options. - /// - IMiraPlugin? ParentMod { get; set; } - - /// - /// Gets the visibility function for the options. - /// Leaving it null will leave the options unchanged. - /// - Func? Visible { get; init; } - - /// - /// Gets a value indicating whether the options should be included with presets. - /// Leaving it null will leave the options unchanged. - /// - bool? IncludeInPreset { get; init; } - - /// - /// Saves the options to a preset configuration file. - /// - /// The ConfigFile representing the preset configuration. - /// Indicates whether to save the default value instead of the current value. - void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false); - - /// - /// Binds the options to a configuration file. - /// - /// The ConfigFile to bind the option to. - void Bind(ConfigFile config); - - /// - /// Loads the options from a preset configuration file, applying the values to the options' configuration. - /// - /// The ConfigFile representing the preset configuration. - void LoadFromPreset(ConfigFile presetConfig); -} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 7e97a1a..12f725e 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using BepInEx.Configuration; using MiraAPI.PluginLoading; namespace MiraAPI.GameOptions.OptionTypes; @@ -80,33 +79,6 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : IncludeInPreset = includeInPreset; } - /// - public void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) - { - foreach (var option in Options) - { - option.SaveToPreset(presetConfig, saveDefault); - } - } - - /// - public void Bind(ConfigFile config) - { - foreach (var option in Options) - { - option.Bind(config); - } - } - - /// - public void LoadFromPreset(ConfigFile presetConfig) - { - foreach (var option in Options) - { - option.LoadFromPreset(presetConfig); - } - } - /// public IEnumerator GetEnumerator() { From ad6ca6fa3ee1006fcb73b643220587e0c116c315 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 20 Jun 2026 01:22:44 -0600 Subject: [PATCH 17/51] Simplified property setters and getter --- MiraAPI/GameOptions/OptionTypes/ModdedOption.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 206894d..644c87c 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 abstract class ModdedOption : IModdedOption 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; } From 6a899ba27d335f47bc386e05b4fb9d43bcd3cd97 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 20 Jun 2026 01:29:56 -0600 Subject: [PATCH 18/51] Removed properties that aren't required --- MiraAPI/GameOptions/IModdedOption.cs | 12 ------- .../OptionTypes/ModdedOptionList.cs | 34 +------------------ 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 824a832..8d3e751 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -119,16 +119,4 @@ public interface IModdedOptionList /// Gets the number of options in the list. /// int Count { get; } - - /// - /// Gets the visibility function for the options. - /// Leaving it null will leave the options unchanged. - /// - Func? Visible { get; init; } - - /// - /// Gets a value indicating whether the options should be included with presets. - /// Leaving it null will leave the options unchanged. - /// - bool? IncludeInPreset { get; init; } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 12f725e..85f650a 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -36,47 +36,15 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : /// public IReadOnlyList Options { get; } - /// - public Func? Visible - { - get; - init - { - field = value; - if (value == null) return; - foreach (var (option, idx) in Options.Select((o, i) => (o, i))) - { - option.Visible = () => value(idx); - } - } - } - - /// - public bool? IncludeInPreset - { - get; - init - { - field = value; - if (value == null) return; - foreach (var option in Options) - { - option.IncludeInPreset = value.Value; - } - } - } - /// /// Initializes a new instance of the class. /// /// The option list's length. /// The option factory to instantiate the options from. - /// Whether to include the options in the preset. - public ModdedOptionList(int count, Func optionFactory, bool? includeInPreset = true) + public ModdedOptionList(int count, Func optionFactory) { Count = count; Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray(); - IncludeInPreset = includeInPreset; } /// From e115317ad4cbb49b5e4bdbb8985ec6151e1cf575 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 20 Jun 2026 01:53:23 -0600 Subject: [PATCH 19/51] Removed generic from interface for easier manager registering alongside property --- MiraAPI/GameOptions/IModdedOption.cs | 12 ++------- .../OptionTypes/ModdedOptionList.cs | 25 ++++--------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 8d3e751..e21c3b2 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; @@ -108,15 +109,6 @@ public interface IModdedOption /// /// Interface for list of modded options. /// -public interface IModdedOptionList +public interface IModdedOptionList : IReadOnlyList { - /// - /// Gets or sets the MiraPlugin that created this list of options. - /// - IMiraPlugin? ParentMod { get; set; } - - /// - /// Gets the number of options in the list. - /// - int Count { get; } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 85f650a..13609d9 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using MiraAPI.PluginLoading; namespace MiraAPI.GameOptions.OptionTypes; @@ -10,27 +9,11 @@ namespace MiraAPI.GameOptions.OptionTypes; /// Represents a modded option list. /// /// The type of options. -public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : IModdedOption +public class ModdedOptionList : IModdedOptionList where T : IModdedOption { /// public int Count { get; } - /// - public IMiraPlugin? ParentMod - { - get; - set - { - if (field != null || value == null) return; - field = value; - - foreach (var option in Options) - { - option.ParentMod = value; - } - } - } - /// /// Gets the list of options. /// @@ -48,9 +31,9 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : } /// - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { - return ((IEnumerable)Options).GetEnumerator(); + return ((IEnumerable)Options).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() @@ -64,4 +47,6 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : /// The option's index. /// option of type . public T this[int idx] => Options[idx]; + + IModdedOption IReadOnlyList.this[int index] => this[index]; } From bea37d815a26dc53e62279850dc38d75111d3bb5 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 20 Jun 2026 02:06:25 -0600 Subject: [PATCH 20/51] Applied Modded option list into options manager --- MiraAPI/GameOptions/ModdedOptionsManager.cs | 20 ++++++++++++++++++++ MiraAPI/PluginLoading/MiraPluginManager.cs | 8 ++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 3b85a73..8fc1b1a 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -113,6 +113,26 @@ public static class ModdedOptionsManager 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 RegisterOption( IModdedOption option, AbstractOptionGroup group, diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 26a881f..607b905 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -201,12 +201,16 @@ 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); + } } foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption)))) From b5e35e2533f52504152c59ae4825905a6a4929f2 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:38:38 -0600 Subject: [PATCH 21/51] Added constructor overload --- MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 13609d9..5a764b2 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -30,6 +30,16 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption 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() { From 39a43f5395e44a8f146d68a5e06095909ef8eb55 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:57:16 -0600 Subject: [PATCH 22/51] Removed unnecessary private get --- MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index 702d75e..e2014fb 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -16,12 +16,12 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null) /// /// 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; + protected Type? RoleType => roleType; /// /// Sets the value of the option. From 604d2f375d2ee1511c2538b7f5e2e89d5e172252 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 13:54:08 -0600 Subject: [PATCH 23/51] Added interface to error logger check --- MiraAPI/PluginLoading/MiraPluginManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 607b905..443d206 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -213,7 +213,8 @@ public sealed class MiraPluginManager } } - 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."); } From 11fa9581fac0d00844c5598bc3324c6fc29d9ab9 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 13:55:29 -0600 Subject: [PATCH 24/51] Added abstract option list attribute --- .../Attributes/ModdedOptionListAttribute.cs | 47 +++++++++++ MiraAPI/GameOptions/ModdedOptionsManager.cs | 80 ++++++++++++++++++- MiraAPI/PluginLoading/MiraPluginManager.cs | 6 ++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs new file mode 100644 index 0000000..116dd58 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -0,0 +1,47 @@ +using System; +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) : Attribute +{ + internal IModdedOptionList? HolderOptionList { 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 abstract void SetValue(object value); + + /// + /// Sets the value of the specific option. + /// + /// The option to set. + /// The new value as an object. + public abstract void SetValue(IModdedOption modOpt, object value); + + /// + /// Gets the value of all the options. + /// + /// The value of the options as an object. + public abstract object GetValue(); + + /// + /// Gets the value of the specific option. + /// + /// The option to set. + /// The value of the option as an object. + public abstract object GetValue(IModdedOption modOpt); + + internal abstract IModdedOptionList? CreateOptionList(object? value, PropertyInfo property); +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 8fc1b1a..3b6708f 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -21,6 +21,7 @@ namespace MiraAPI.GameOptions; public static class ModdedOptionsManager { private static readonly Dictionary OptionAttributes = []; + private static readonly Dictionary OptionListAttributes = []; private static readonly Dictionary TypeToGroup = []; internal static readonly Dictionary CreatedPlayerOptions = []; @@ -100,11 +101,11 @@ public static class ModdedOptionsManager } var setterOriginal = property.GetSetMethod(); - var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); + var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch)); PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); var getterOriginal = property.GetGetMethod(); - var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); + var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch)); PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); OptionAttributes.Add(property, attribute); @@ -133,6 +134,51 @@ public static class ModdedOptionsManager } } + internal static void RegisterAttributeOptionList( + Type type, + ModdedOptionListAttribute attribute, + PropertyInfo property, + MiraPluginInfo pluginInfo) + { + if (OptionListAttributes.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 optionList = attribute.CreateOptionList(property.GetValue(group), property); + + if (optionList == null) + { + Error($"Failed to get option for {property.Name}"); + 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)); + + // Add indexer patches + + OptionListAttributes.Add(property, attribute); + attribute.HolderOptionList = optionList; + + for (int i = 0; i < optionList.Count; i++) + { + RegisterOption(optionList[i], group, property.Name + i, pluginInfo); + } + } + internal static void RegisterOption( IModdedOption option, AbstractOptionGroup group, @@ -222,4 +268,34 @@ public static class ModdedOptionsManager __result = attribute.GetValue(); return false; } + + /// + /// Patches the setter of a list property to update the value of the options. + /// + /// The original setter method. + /// The new object value. +#pragma warning disable CA1707 + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] + public static void PropertyListSetterPatch(MethodBase __originalMethod, object value) +#pragma warning restore CA1707 + { + var attribute = OptionListAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value; + attribute.SetValue(value); + } + + /// + /// Patches the getter of a list property to return the value of the options. + /// + /// The original getter method. + /// 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(MethodBase __originalMethod, ref object __result) +#pragma warning restore CA1707 + { + var attribute = OptionListAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value; + __result = attribute.GetValue(); + return false; + } } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 443d206..bb4e6cc 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -211,6 +211,12 @@ public sealed class MiraPluginManager { ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo); } + + var listAttr = property.GetCustomAttribute(); + if (listAttr != null) + { + ModdedOptionsManager.RegisterAttributeOptionList(type, listAttr, property, pluginInfo); + } } foreach (var field in type.GetFields() From 887be871f0a55dd88e3d4a31e5cf0ce6096b4510 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:28:17 -0600 Subject: [PATCH 25/51] Changed abstract methods to handle casting and calling abstract overload --- .../Attributes/ModdedOptionListAttribute.cs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs index 116dd58..de927ab 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Reflection; namespace MiraAPI.GameOptions.Attributes; @@ -12,6 +13,10 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr { internal IModdedOptionList? HolderOptionList { get; set; } + internal PropertyInfo? BaseProperty { get; set; } + + internal AbstractOptionGroup? Group { get; set; } + /// /// Gets the function to title of the options. /// @@ -21,7 +26,20 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr /// Sets the value of all the options. /// /// The new values as an object. - public abstract void SetValue(object value); + public void SetValue(object value) + { + var list = (IList)value; + if (list.Count != ((IList)BaseProperty!.GetValue(Group)!).Count || + list.Count != HolderOptionList!.Count) + { + throw new InvalidOperationException($"Value set to {BaseProperty!.Name} cannot change the list's length."); + } + + for (int i = 0; i < list!.Count; i++) + { + SetValue(HolderOptionList![i], list[i]!); + } + } /// /// Sets the value of the specific option. @@ -34,7 +52,15 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr /// Gets the value of all the options. /// /// The value of the options as an object. - public abstract object GetValue(); + public object GetValue() + { + var list = (IList)BaseProperty!.GetValue(Group)!; + for (int i = 0; i < list!.Count; i++) + { + list[i] = GetValue(HolderOptionList![i]); + } + return list; + } /// /// Gets the value of the specific option. From e5658df8c551557c67a2c53455d76f3d6d453483 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:35:14 -0600 Subject: [PATCH 26/51] Created parent attribute class to share methods --- .../Attributes/ModdedOptionAttribute.cs | 14 +------ .../Attributes/ModdedOptionListAttribute.cs | 6 +-- .../Attributes/PropertyOptionAttribute.cs | 22 ++++++++++ MiraAPI/GameOptions/ModdedOptionsManager.cs | 41 +++---------------- 4 files changed, 31 insertions(+), 52 deletions(-) create mode 100644 MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index e2014fb..62ce43b 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -9,7 +9,7 @@ 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; } @@ -23,17 +23,5 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null) /// protected Type? RoleType => 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(); - internal abstract IModdedOption? CreateOption(object? value, PropertyInfo property); } diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs index de927ab..bce0190 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.Attributes; /// /// A function to title of the options. [AttributeUsage(AttributeTargets.Property)] -public abstract class ModdedOptionListAttribute(Func titler) : Attribute +public abstract class ModdedOptionListAttribute(Func titler) : PropertyOptionAttribute { internal IModdedOptionList? HolderOptionList { get; set; } @@ -26,7 +26,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr /// Sets the value of all the options. /// /// The new values as an object. - public void SetValue(object value) + public override void SetValue(object value) { var list = (IList)value; if (list.Count != ((IList)BaseProperty!.GetValue(Group)!).Count || @@ -52,7 +52,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr /// Gets the value of all the options. /// /// The value of the options as an object. - public object GetValue() + public override object GetValue() { var list = (IList)BaseProperty!.GetValue(Group)!; for (int i = 0; i < list!.Count; i++) 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/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 3b6708f..00b726d 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -20,8 +20,7 @@ namespace MiraAPI.GameOptions; /// public static class ModdedOptionsManager { - private static readonly Dictionary OptionAttributes = []; - private static readonly Dictionary OptionListAttributes = []; + private static readonly Dictionary OptionAttributes = []; private static readonly Dictionary TypeToGroup = []; internal static readonly Dictionary CreatedPlayerOptions = []; @@ -101,11 +100,11 @@ public static class ModdedOptionsManager } var setterOriginal = property.GetSetMethod(); - var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch)); + 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(PropertyListGetterPatch)); + var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); OptionAttributes.Add(property, attribute); @@ -140,7 +139,7 @@ public static class ModdedOptionsManager PropertyInfo property, MiraPluginInfo pluginInfo) { - if (OptionListAttributes.ContainsKey(property)) + if (OptionAttributes.ContainsKey(property)) { Error($"Property {property.Name} already has an attribute registered."); return; @@ -170,7 +169,7 @@ public static class ModdedOptionsManager // Add indexer patches - OptionListAttributes.Add(property, attribute); + OptionAttributes.Add(property, attribute); attribute.HolderOptionList = optionList; for (int i = 0; i < optionList.Count; i++) @@ -268,34 +267,4 @@ public static class ModdedOptionsManager __result = attribute.GetValue(); return false; } - - /// - /// Patches the setter of a list property to update the value of the options. - /// - /// The original setter method. - /// The new object value. -#pragma warning disable CA1707 - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] - public static void PropertyListSetterPatch(MethodBase __originalMethod, object value) -#pragma warning restore CA1707 - { - var attribute = OptionListAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value; - attribute.SetValue(value); - } - - /// - /// Patches the getter of a list property to return the value of the options. - /// - /// The original getter method. - /// 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(MethodBase __originalMethod, ref object __result) -#pragma warning restore CA1707 - { - var attribute = OptionListAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value; - __result = attribute.GetValue(); - return false; - } } From 80b8af85cb0c542d03570b03a236470cda8d382c Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:59:49 -0600 Subject: [PATCH 27/51] Added IList cast and null check --- MiraAPI/GameOptions/ModdedOptionsManager.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 00b726d..1706719 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; @@ -151,7 +152,16 @@ public static class ModdedOptionsManager return; } - var optionList = attribute.CreateOptionList(property.GetValue(group), property); + 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) { From 68fb1d2b58177cb5a676e4293eb719098482a186 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:16:11 -0600 Subject: [PATCH 28/51] Added patches to the list's getter and setter --- .../Attributes/ModdedOptionListAttribute.cs | 10 +---- MiraAPI/GameOptions/ModdedOptionsManager.cs | 45 ++++++++++++++++++- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs index bce0190..3e26432 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -29,8 +29,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop public override void SetValue(object value) { var list = (IList)value; - if (list.Count != ((IList)BaseProperty!.GetValue(Group)!).Count || - list.Count != HolderOptionList!.Count) + if (list.Count != HolderOptionList!.Count) { throw new InvalidOperationException($"Value set to {BaseProperty!.Name} cannot change the list's length."); } @@ -54,12 +53,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop /// The value of the options as an object. public override object GetValue() { - var list = (IList)BaseProperty!.GetValue(Group)!; - for (int i = 0; i < list!.Count; i++) - { - list[i] = GetValue(HolderOptionList![i]); - } - return list; + return BaseProperty!.GetValue(Group)!; } /// diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 1706719..3dbab02 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -168,6 +168,11 @@ public static class ModdedOptionsManager 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)); @@ -177,7 +182,15 @@ public static class ModdedOptionsManager var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); - // Add indexer patches + 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; @@ -277,4 +290,34 @@ public static class ModdedOptionsManager __result = attribute.GetValue(); return false; } + + /// + /// Patches the setter of a list property to update the value of the option. + /// + /// The original setter method. + /// 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(MethodBase __originalMethod, int index, object value) +#pragma warning restore CA1707 + { + // Actually implement + } + + /// + /// Patches the getter of a list property to return the value of the option. + /// + /// The original getter method. + /// 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(MethodBase __originalMethod, int index, ref object __result) +#pragma warning restore CA1707 + { + // Actually implement + return true; + } } From 67727945da9c7b028c6f012ab02eaa5ea0070c29 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:35:47 -0600 Subject: [PATCH 29/51] Changed to store a reference to the property's value instance --- MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs | 5 +++-- MiraAPI/GameOptions/ModdedOptionsManager.cs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs index 3e26432..4a89375 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -15,7 +15,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop internal PropertyInfo? BaseProperty { get; set; } - internal AbstractOptionGroup? Group { get; set; } + internal object? Value { get; set; } /// /// Gets the function to title of the options. @@ -28,6 +28,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop /// The new values as an object. public override void SetValue(object value) { + Value = value; var list = (IList)value; if (list.Count != HolderOptionList!.Count) { @@ -53,7 +54,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop /// The value of the options as an object. public override object GetValue() { - return BaseProperty!.GetValue(Group)!; + return Value!; } /// diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 3dbab02..d9c42bc 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -194,6 +194,7 @@ public static class ModdedOptionsManager OptionAttributes.Add(property, attribute); attribute.HolderOptionList = optionList; + attribute.Value = propertyVal; for (int i = 0; i < optionList.Count; i++) { From edeec4ca4d191ad6e6e7388844baf0b34a753f44 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:57:24 -0600 Subject: [PATCH 30/51] Implemented list value patches --- MiraAPI/GameOptions/ModdedOptionsManager.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index d9c42bc..785c95d 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -295,30 +295,34 @@ public static class ModdedOptionsManager /// /// Patches the setter of a list property to update the value of the option. /// - /// The original setter method. + /// 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(MethodBase __originalMethod, int index, object value) + public static void PropertyListSetterPatch(object __instance, int index, object value) #pragma warning restore CA1707 { - // Actually implement + var attribute = (ModdedOptionListAttribute)OptionAttributes.First( + pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value; + attribute.SetValue(attribute.HolderOptionList![index], value); } /// /// Patches the getter of a list property to return the value of the option. /// - /// The original getter method. + /// 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(MethodBase __originalMethod, int index, ref object __result) + public static bool PropertyListGetterPatch(object __instance, int index, ref object __result) #pragma warning restore CA1707 { - // Actually implement - return true; + var attribute = (ModdedOptionListAttribute)OptionAttributes.First( + pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value; + __result = attribute.GetValue(attribute.HolderOptionList![index]); + return false; } } From 3fd3c68aaf643dfb999895cba2501015166dc85c Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:06:14 -0600 Subject: [PATCH 31/51] Removed propertyinfo field --- MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs index 4a89375..a945b1d 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -13,8 +13,6 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop { internal IModdedOptionList? HolderOptionList { get; set; } - internal PropertyInfo? BaseProperty { get; set; } - internal object? Value { get; set; } /// @@ -32,7 +30,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop var list = (IList)value; if (list.Count != HolderOptionList!.Count) { - throw new InvalidOperationException($"Value set to {BaseProperty!.Name} 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++) From c3384ef8506d393c89ac7626c09c5fbf2e237046 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:43:26 -0600 Subject: [PATCH 32/51] Changed parent attribute method signatures --- .../Attributes/ModdedOptionListAttribute.cs | 12 ++++++------ MiraAPI/GameOptions/ModdedOptionsManager.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs index a945b1d..74615dd 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -35,16 +35,16 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop for (int i = 0; i < list!.Count; i++) { - SetValue(HolderOptionList![i], list[i]!); + SetValue(i, list[i]!); } } /// /// Sets the value of the specific option. /// - /// The option to set. + /// The option to set. /// The new value as an object. - public abstract void SetValue(IModdedOption modOpt, object value); + public abstract void SetValue(int idx, object value); /// /// Gets the value of all the options. @@ -58,9 +58,9 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop /// /// Gets the value of the specific option. /// - /// The option to set. + /// The option to set. /// The value of the option as an object. - public abstract object GetValue(IModdedOption modOpt); + public abstract object GetValue(int idx); - internal abstract IModdedOptionList? CreateOptionList(object? value, PropertyInfo property); + internal abstract IModdedOptionList? CreateOptionList(IList value, PropertyInfo property); } diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 785c95d..ffb85e3 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -305,7 +305,7 @@ public static class ModdedOptionsManager { var attribute = (ModdedOptionListAttribute)OptionAttributes.First( pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value; - attribute.SetValue(attribute.HolderOptionList![index], value); + attribute.SetValue(index, value); } /// @@ -322,7 +322,7 @@ public static class ModdedOptionsManager { var attribute = (ModdedOptionListAttribute)OptionAttributes.First( pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value; - __result = attribute.GetValue(attribute.HolderOptionList![index]); + __result = attribute.GetValue(index); return false; } } From 25794b149bc027903140450ea8f3dabd6520b6cc Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:05:14 -0600 Subject: [PATCH 33/51] Created attribute versions for option list --- .../ModdedEnumOptionListAttribute.cs | 67 +++++++++++++++++++ .../ModdedNumberOptionListAttribute.cs | 55 +++++++++++++++ .../ModdedToggleOptionListAttribute.cs | 38 +++++++++++ 3 files changed, 160 insertions(+) create mode 100644 MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs create mode 100644 MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs create mode 100644 MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs new file mode 100644 index 0000000..c8ec447 --- /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 0000000..18d7f37 --- /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/ModdedToggleOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs new file mode 100644 index 0000000..7e34080 --- /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."); + } +} From a4daa87bfcdfddaf9f2929e41e3b777b0216bf62 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:41:44 -0600 Subject: [PATCH 34/51] Added implicit operator to loadable asset which will call LoadAsset --- MiraAPI/Utilities/Assets/LoadableAsset.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MiraAPI/Utilities/Assets/LoadableAsset.cs b/MiraAPI/Utilities/Assets/LoadableAsset.cs index 798dfc9..58c326c 100644 --- a/MiraAPI/Utilities/Assets/LoadableAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableAsset.cs @@ -18,4 +18,10 @@ 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(); } From 706a836e4abb336e94e45a633a35717f63d7eacd Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:17:29 -0600 Subject: [PATCH 35/51] Invert order of operation for ResetTarget so SetOutline(false) still has previous target --- MiraAPI/Hud/CustomActionButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index f5cf521..8a58e39 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; } } From 6598cb69331a8b641c7be4cfe39c4b9aead7332e Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:21:28 -0600 Subject: [PATCH 36/51] Undid chaging property to init setter --- MiraAPI/GameOptions/OptionTypes/ModdedOption.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 644c87c..b3197b5 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -52,9 +52,9 @@ public abstract class ModdedOption : IModdedOption public T DefaultValue { get; } /// - /// Gets the event that is invoked when the value of the option changes. + /// Gets or sets the event that is invoked when the value of the option changes. /// - public Action? ChangedEvent { get; init; } + public Action? ChangedEvent { get; set; } /// public Func Visible { get; set; } From e7184c9592784c6cc15a050a48f129adeac88005 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:44:46 -0600 Subject: [PATCH 37/51] Added RoleID further util methods --- MiraAPI/Roles/RoleId.cs | 58 ++++++++++++++++++++++++++++++++- MiraAPI/Utilities/Extensions.cs | 2 +- 2 files changed, 58 insertions(+), 2 deletions(-) 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/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 6c8b74b..9c1cb5f 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))); } From 908ff4fa67f0cde3a94893846e7a94db8b5e8a50 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:04:48 -0600 Subject: [PATCH 38/51] Created modded option visibility attribute --- .../ModdedOptionVisibilityAttribute.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs new file mode 100644 index 0000000..bd39a50 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs @@ -0,0 +1,38 @@ +using System; +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) + { + } + +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property) + { + return null; + } + + internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) + { + return null; + } +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +} From 22d45c7c647ed2473f9d6fcc4200cc54489aeaf4 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:28:50 -0600 Subject: [PATCH 39/51] Applied attribute function in options manager --- MiraAPI/GameOptions/ModdedOptionsManager.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 239166c..60b2502 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -111,6 +111,13 @@ 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); } @@ -196,9 +203,16 @@ public static class ModdedOptionsManager attribute.HolderOptionList = optionList; attribute.Value = propertyVal; + var visibilityAttr = property.GetCustomAttribute(); + var visibilityFunc = visibilityAttr?.GetListVisibility(group, property); for (int i = 0; i < optionList.Count; i++) { - RegisterOption(optionList[i], group, property.Name + i, pluginInfo); + var option = optionList[i]; + if (visibilityFunc != null) + { + option.Visible = () => visibilityFunc(i); + } + RegisterOption(option, group, property.Name + i, pluginInfo); } } From 0895c58524e9fad653d0e4451fba42c49a24768b Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:37:55 -0600 Subject: [PATCH 40/51] Added logic to determine method member validity and obtain visibility function --- .../ModdedOptionVisibilityAttribute.cs | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs index bd39a50..322efc4 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs @@ -24,15 +24,78 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri { } -#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property) { + MemberInfo? member = GetVisibilityMember(group, property, out Type type); + + 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(group, null)!; + } + // TODO: Properties and/or fields + + Error($"Valid member {memberName} does not exist in group {type.FullName}."); return null; } internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) { + MemberInfo? member = GetVisibilityMember(group, property, out Type type); + + 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(group, null)!; + } + + if (paramList[0].ParameterType != typeof(int)) + { + Error($"Method {memberName}'s parameter is not an int."); + return null; + } + return i => (bool)vMethod.Invoke(group, [i])!; + } + // TODO: Properties and/or fields + + Error($"Valid member {memberName} does not exist in group {type.FullName}."); 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) + { + type = holderType ?? group.GetType(); + memberName ??= $"{property.Name}Visible"; + BindingFlags flags = BindingFlags.Static | BindingFlags.Public; + if (holderType == null) + { + flags |= BindingFlags.Instance | BindingFlags.NonPublic; + } + return type.GetMember(memberName, flags).FirstOrDefault(); + } #pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields } From 7b5e38b10e0fd1922a08c3fd1aea0e6fa61bdba2 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:09:34 -0600 Subject: [PATCH 41/51] Added null member case --- .../ModdedOptionVisibilityAttribute.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs index 322efc4..2e5df8f 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs @@ -26,7 +26,11 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property) { - MemberInfo? member = GetVisibilityMember(group, property, out Type type); + if (GetVisibilityMember(group, property, out Type type) is not { } member) + { + Error($"Member {memberName} does not exist in {type.FullName}"); + return null; + } if (member is MethodInfo vMethod) { @@ -44,15 +48,18 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri return () => (bool)vMethod.Invoke(group, null)!; } - // TODO: Properties and/or fields - Error($"Valid member {memberName} does not exist in group {type.FullName}."); + Error($"Member {memberName} of {type.FullName} is not valid for Visible function."); return null; } internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) { - MemberInfo? member = GetVisibilityMember(group, property, out Type type); + if (GetVisibilityMember(group, property, out Type type) is not { } member) + { + Error($"Member {memberName} does not exist in {type.FullName}"); + return null; + } if (member is MethodInfo vMethod) { @@ -79,9 +86,8 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri } return i => (bool)vMethod.Invoke(group, [i])!; } - // TODO: Properties and/or fields - Error($"Valid member {memberName} does not exist in group {type.FullName}."); + Error($"Member {memberName} of {type.FullName} is not valid for Visible function."); return null; } From bae3a245f1acddda97119e7e2c920b1bc6cddd77 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:15:36 -0600 Subject: [PATCH 42/51] Added case for properties, including list values and indexers --- .../ModdedOptionVisibilityAttribute.cs | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs index 2e5df8f..b941da8 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -32,6 +33,23 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri return null; } + if (member is PropertyInfo vProperty) + { + if (vProperty.PropertyType != typeof(bool)) + { + Error($"Property {memberName} is not a bool."); + return null; + } + + var indexParams = vProperty.GetIndexParameters(); + if (indexParams.Length > 0) + { + Error($"Property {memberName} cannot be an indexer."); + return null; + } + + return () => (bool)vProperty.GetValue(group)!; + } if (member is MethodInfo vMethod) { if (vMethod.ReturnType != typeof(bool)) @@ -39,6 +57,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri Error($"Method {memberName} does not return a bool."); return null; } + var paramList = vMethod.GetParameters(); if (paramList.Length > 0) { @@ -61,6 +80,44 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri return null; } + if (member is PropertyInfo vProperty) + { + if (vProperty.PropertyType != typeof(bool) && !vProperty.PropertyType.IsAssignableTo(typeof(IList))) + { + Error($"Property {memberName} is not a bool value or indexer, or list of bools."); + 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(group)!)[i]; + } + + if (indexParams.Length == 0) + { + return _ => (bool)vProperty.GetValue(group)!; + } + if (indexParams[0].ParameterType != typeof(int)) + { + Error($"Indexer {memberName}'s parameter is not an int."); + return null; + } + + return i => (bool)vProperty.GetValue(group, [i])!; + } if (member is MethodInfo vMethod) { if (vMethod.ReturnType != typeof(bool)) @@ -68,22 +125,24 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri 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(group, null)!; } - if (paramList[0].ParameterType != typeof(int)) { Error($"Method {memberName}'s parameter is not an int."); return null; } + return i => (bool)vMethod.Invoke(group, [i])!; } From 121744e9ef5028f5ab9b491bee8960c80aa9d152 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:07:07 -0600 Subject: [PATCH 43/51] Changed what flags are set for member finding --- .../Attributes/ModdedOptionVisibilityAttribute.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs index b941da8..3a3d489 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs @@ -153,12 +153,19 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri #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) { - type = holderType ?? group.GetType(); - memberName ??= $"{property.Name}Visible"; + Type groupType = group.GetType(); BindingFlags flags = BindingFlags.Static | BindingFlags.Public; - if (holderType == null) + + type = holderType ?? groupType; + memberName ??= $"{property.Name}Visible"; + if (type.IsAssignableTo(typeof(AbstractOptionGroup))) { - flags |= BindingFlags.Instance | BindingFlags.NonPublic; + flags |= BindingFlags.Instance; + + if (type == groupType) + { + flags |= BindingFlags.NonPublic; + } } return type.GetMember(memberName, flags).FirstOrDefault(); } From a2f363c8da4a2ba6d7e9d41ed1a2c2039cc72fe7 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:48:50 -0600 Subject: [PATCH 44/51] Added instanceGet out parameter to provide the group instance when needed, null when static, or the Singleton Instance if another OptionGroup --- .../ModdedOptionVisibilityAttribute.cs | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs index 3a3d489..acf929a 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs @@ -27,7 +27,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property) { - if (GetVisibilityMember(group, property, out Type type) is not { } member) + if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member) { Error($"Member {memberName} does not exist in {type.FullName}"); return null; @@ -48,7 +48,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri return null; } - return () => (bool)vProperty.GetValue(group)!; + return () => (bool)vProperty.GetValue(instanceGet?.Invoke())!; } if (member is MethodInfo vMethod) { @@ -65,7 +65,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri return null; } - return () => (bool)vMethod.Invoke(group, null)!; + return () => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!; } Error($"Member {memberName} of {type.FullName} is not valid for Visible function."); @@ -74,7 +74,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property) { - if (GetVisibilityMember(group, property, out Type type) is not { } member) + if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member) { Error($"Member {memberName} does not exist in {type.FullName}"); return null; @@ -103,12 +103,12 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri return null; } - return i => ((IList)vProperty.GetValue(group)!)[i]; + return i => ((IList)vProperty.GetValue(instanceGet?.Invoke())!)[i]; } if (indexParams.Length == 0) { - return _ => (bool)vProperty.GetValue(group)!; + return _ => (bool)vProperty.GetValue(instanceGet?.Invoke())!; } if (indexParams[0].ParameterType != typeof(int)) { @@ -116,7 +116,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri return null; } - return i => (bool)vProperty.GetValue(group, [i])!; + return i => (bool)vProperty.GetValue(instanceGet?.Invoke(), [i])!; } if (member is MethodInfo vMethod) { @@ -135,7 +135,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri if (paramList.Length == 0) { - return _ => (bool)vMethod.Invoke(group, null)!; + return _ => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!; } if (paramList[0].ParameterType != typeof(int)) { @@ -143,7 +143,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri return null; } - return i => (bool)vMethod.Invoke(group, [i])!; + return i => (bool)vMethod.Invoke(instanceGet?.Invoke(), [i])!; } Error($"Member {memberName} of {type.FullName} is not valid for Visible function."); @@ -151,11 +151,12 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri } #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) + 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))) @@ -165,9 +166,26 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri if (type == groupType) { flags |= BindingFlags.NonPublic; + instanceGet = () => group; + } + else + { + var instanceProp = typeof(OptionGroupSingleton<>).MakeGenericType(type) + .GetProperty(nameof(OptionGroupSingleton<>.Instance))!; + instanceGet = () => instanceProp.GetValue(null)!; } } - return type.GetMember(memberName, flags).FirstOrDefault(); + 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 } From 2ebb9f183c129a25ed9d1068deac98c3310eb171 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:55:33 -0600 Subject: [PATCH 45/51] Allowed properties to be of type ModdedOption --- .../Attributes/ModdedOptionVisibilityAttribute.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs index acf929a..3e4475d 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs @@ -1,4 +1,5 @@ -using System; +using MiraAPI.GameOptions.OptionTypes; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -35,9 +36,10 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri if (member is PropertyInfo vProperty) { - if (vProperty.PropertyType != typeof(bool)) + if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)) && + !vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption))) { - Error($"Property {memberName} is not a bool."); + Error($"Property {memberName} is not a bool or a bool ModdedOption."); return null; } @@ -82,9 +84,11 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri if (member is PropertyInfo vProperty) { - if (vProperty.PropertyType != typeof(bool) && !vProperty.PropertyType.IsAssignableTo(typeof(IList))) + 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, or list of bools."); + Error($"Property {memberName} is not a bool value or indexer, a list of bools, or a bool ModdedOption."); return null; } From f34697a5bfb70b361467a89a17224e343f4006ec Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:02:06 -0600 Subject: [PATCH 46/51] Moved option list attributes to subfolder --- .../Attributes/{ => OptionList}/ModdedEnumOptionListAttribute.cs | 0 .../{ => OptionList}/ModdedNumberOptionListAttribute.cs | 0 .../Attributes/{ => OptionList}/ModdedOptionListAttribute.cs | 0 .../{ => OptionList}/ModdedToggleOptionListAttribute.cs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename MiraAPI/GameOptions/Attributes/{ => OptionList}/ModdedEnumOptionListAttribute.cs (100%) rename MiraAPI/GameOptions/Attributes/{ => OptionList}/ModdedNumberOptionListAttribute.cs (100%) rename MiraAPI/GameOptions/Attributes/{ => OptionList}/ModdedOptionListAttribute.cs (100%) rename MiraAPI/GameOptions/Attributes/{ => OptionList}/ModdedToggleOptionListAttribute.cs (100%) diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs similarity index 100% rename from MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs rename to MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs similarity index 100% rename from MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs rename to MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs similarity index 100% rename from MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs rename to MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs similarity index 100% rename from MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs rename to MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs From ff6c6114cfecab5e5f4326d380899665331ce2f0 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:17:23 -0600 Subject: [PATCH 47/51] Changed option list attributes to only accept strings with zero or one format parameters --- .../ModdedEnumOptionListAttribute.cs | 16 ++++----- .../ModdedNumberOptionListAttribute.cs | 8 ++--- .../OptionList/ModdedOptionListAttribute.cs | 33 ++++++++++++++++--- .../ModdedToggleOptionListAttribute.cs | 8 ++--- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs index c8ec447..dde2cfb 100644 --- a/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedEnumOptionListAttribute.cs @@ -9,13 +9,13 @@ 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) +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(Titler(idx), (int)(value[idx] ?? 0), enumType, values)); + value.Count, idx => new(GetFormattedTitle(idx), (int)(value[idx] ?? 0), enumType, values)); return optList; } @@ -31,7 +31,7 @@ public class ModdedEnumOptionListAttribute(Func titler, Type enumTy { 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"); + : throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); } } @@ -40,13 +40,13 @@ public class ModdedEnumOptionListAttribute(Func titler, Type enumTy /// /// The enum type. [AttributeUsage(AttributeTargets.Property)] -public class ModdedEnumOptionListAttribute(Func titler, string[]? values = null) - : ModdedOptionListAttribute(titler) where T : Enum +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(Titler(idx), (T)(value[idx] ?? 0), values)); + value.Count, idx => new(GetFormattedTitle(idx), (T)(value[idx] ?? 0), values)); return optList; } @@ -62,6 +62,6 @@ public class ModdedEnumOptionListAttribute(Func titler, string[] { 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"); + : 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 index 18d7f37..96e9729 100644 --- a/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs @@ -11,21 +11,21 @@ namespace MiraAPI.GameOptions.Attributes; /// [AttributeUsage(AttributeTargets.Property)] public class ModdedNumberOptionListAttribute( - Func titler, + string title, float min, float max, float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, string? formatString = null, bool zeroInfinity = false) - : ModdedOptionListAttribute(titler) + : ModdedOptionListAttribute(title) { internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) { return new ModdedOptionList(value.Count, idx => { return new( - Titler(idx), + GetFormattedTitle(idx), (float)(value[idx] ?? min + increment), min, max, @@ -50,6 +50,6 @@ public class ModdedNumberOptionListAttribute( { return opt.Value; } - throw new InvalidOperationException($"HolderOption for option \"{Titler(idx)}\" is not a ModdedNumberOption"); + 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 index 74615dd..f109ffc 100644 --- a/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedOptionListAttribute.cs @@ -1,24 +1,28 @@ 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. /// -/// A function to title of the options. +/// The title of the options. [AttributeUsage(AttributeTargets.Property)] -public abstract class ModdedOptionListAttribute(Func titler) : PropertyOptionAttribute +public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAttribute { internal IModdedOptionList? HolderOptionList { get; set; } internal object? Value { get; set; } /// - /// Gets the function to title of the options. + /// 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 Func Titler => titler; + public string Title => title; /// /// Sets the value of all the options. @@ -63,4 +67,25 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop 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 index 7e34080..5209bcc 100644 --- a/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedToggleOptionListAttribute.cs @@ -8,14 +8,14 @@ namespace MiraAPI.GameOptions.Attributes; /// /// Attribute for a list of toggle options. /// -/// The function to get each option's title. +/// The option's title. [AttributeUsage(AttributeTargets.Property)] -public class ModdedToggleOptionListAttribute(Func titler) : ModdedOptionListAttribute(titler) +public class ModdedToggleOptionListAttribute(string title) : ModdedOptionListAttribute(title) { internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) { var optList = new ModdedOptionList( - value.Count, idx => new(Titler(idx), (bool)(value[idx] ?? false))); + value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false))); return optList; } @@ -33,6 +33,6 @@ public class ModdedToggleOptionListAttribute(Func titler) : ModdedO { return opt.Value; } - throw new InvalidOperationException($"Holder option for {Titler(idx)} is not a ModdedToggleOption."); + throw new InvalidOperationException($"Holder option for {GetFormattedTitle(idx)} is not a ModdedToggleOption."); } } From e205a683306da7e3bab0e576ed0fbdc9ae606ca2 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:37:41 -0600 Subject: [PATCH 48/51] Created examples of utilizing option lists --- MiraAPI.Example/Options/ExampleOptions.cs | 12 ++++++++++++ MiraAPI.Example/Options/ExampleOptions2.cs | 3 +++ 2 files changed, 15 insertions(+) diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index 53bc9eb..3663cad 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; @@ -21,6 +22,10 @@ public class ExampleOptions : AbstractOptionGroup [ModdedEnumOption("Best API", 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 +34,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 bc3c910..59de0c6 100644 --- a/MiraAPI.Example/Options/ExampleOptions2.cs +++ b/MiraAPI.Example/Options/ExampleOptions2.cs @@ -14,6 +14,9 @@ public class ExampleOptions2 : AbstractOptionGroup public ModdedPlayerOption PlayerOption { get; } = new("Eh whatever"); public ModdedToggleOption ToggleOpt1 { get; } = new("Toggle Option 1", false); + public ModdedOptionList PlayerNums { get; } = new(5, i => + new($"PlayerNum{i}", 2, 0, 5, 1, Utilities.MiraNumberSuffixes.None)); + public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false) { Visible = () => OptionGroupSingleton.Instance.ToggleOpt1, // implicit cast from ModdedToggleOption to bool From 8db3e10ce75b5ba89ac1c1a044bbd8aea58bd30a Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:41:23 -0600 Subject: [PATCH 49/51] Added example for Visibility attribute --- MiraAPI.Example/Options/ExampleOptions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index 3663cad..0f17f27 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -17,6 +17,10 @@ public class ExampleOptions : AbstractOptionGroup [ModdedToggleOption("Toggle Opt 2")] 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("Number Opt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)] public float NumberOpt { get; set; } = 4f; From 6540f8b7bbf0c5a998d8e98e29caba2b0490f507 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:40:21 -0600 Subject: [PATCH 50/51] Made EventHandler InvokeEvent extension methods --- MiraAPI/Events/MiraEventManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From 7f79685696cd71708ebe62700faa5ed1f9275f98 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:06:28 -0500 Subject: [PATCH 51/51] fix other gamemode roles appearing in view pane --- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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; }