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/33] 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 0e31779c..bc65692e 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -128,4 +128,12 @@ public virtual void OnMeetingStart() /// /// True if the player can vent, false otherwise. Null for no effect. public virtual bool? CanVent() => null; + + /// + /// Removes this modifier instance from the player. + /// + public void RemoveSelf() + { + ModifierComponent?.RemoveModifier(this); + } } 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/33] 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 d64f3662..b3c56229 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -389,6 +389,77 @@ public void RemoveModifier(Guid uniqueId) RemoveModifier(modifier); } + /// + /// Tries to remove a modifier from the player. + /// + /// The modifier type. + /// The predicate to check the modifier by. + /// if the modifier is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Func? predicate = null) where T : BaseModifier + { + return TryGetModifier(out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a modifier from the player. + /// + /// The modifier type. + /// The predicate to check the modifier by. + /// if the modifier is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Type type, Func? predicate = null) + { + return TryGetModifier(type, out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a modifier from the player. + /// + /// The modifier object. + /// if the modifier is not active on this player, else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(BaseModifier modifier) + { + if (!ActiveModifiers.Contains(modifier)) + { + return false; + } + + _toRemove.Add(modifier); + return true; + } + + /// + /// Tries to remove a modifier from the player. + /// + /// The modifier's type ID. + /// The predicate to check the modifier by. + /// if the modifier is not active on this player, or there are multiple instances; + /// else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(uint typeId, Func? predicate = null) + { + return TryGetModifier(typeId, out var modifier, predicate) && + TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a modifier from the player. + /// + /// The modifier's unique ID. + /// if the modifier is not active on this player, else . + [HideFromIl2Cpp] + public bool TryRemoveModifier(Guid uniqueId) + { + return TryGetModifier(uniqueId, out var modifier) && + TryRemoveModifier(modifier); + } + /// /// Adds a modifier to the player. /// 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/33] 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 7a0ef23f..bc3433f0 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -423,6 +423,73 @@ public static void RemoveModifier( player.GetModifierComponent().RemoveModifier(uniqueId); } + /// + /// Tries to remove a specific modifier from the player. + /// + /// The PlayerControl instance. + /// The modifier to remove. + /// if the modifier is not active on this player, else . + public static bool TryRemoveModifier(this PlayerControl player, BaseModifier modifier) + { + return player.GetModifierComponent().TryRemoveModifier(modifier); + } + + /// + /// Tries to remove a specific modifier from the player. + /// + /// The type of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// if the modifier is not active on this player, or there are multiple instances; + /// else . + public static bool TryRemoveModifier(this PlayerControl player, Func? predicate = null) + where T : BaseModifier + { + return player.GetModifierComponent().TryRemoveModifier(predicate); + } + + /// + /// Tries to remove a specific modifier from the player by type. + /// + /// The PlayerControl instance. + /// The type of the modifier. + /// Optional predicate to filter the modifiers. + /// if the modifier is not active on this player, or there are multiple instances; + /// else . + public static bool TryRemoveModifier(this PlayerControl player, Type type, Func? predicate = null) + { + return player.GetModifierComponent().TryRemoveModifier(type, predicate); + } + + /// + /// Tries to remove a specific modifier from the player by its type ID. + /// + /// The PlayerControl instance. + /// The type ID of the modifier. + /// Optional predicate to filter the modifiers. + /// if the modifier is not active on this player, or there are multiple instances; + /// else . + public static bool TryRemoveModifier( + this PlayerControl player, + uint typeId, + Func? predicate = null) + { + return player.GetModifierComponent().TryRemoveModifier(typeId, predicate); + } + + /// + /// Tries to remove a specific modifier from the player by its GUID. + /// + /// The PlayerControl instance. + /// The GUID of the modifier. + /// if the modifier is not active on this player, else . + public static bool TryRemoveModifier( + this PlayerControl player, + Guid uniqueId) + { + return player.GetModifierComponent().TryRemoveModifier(uniqueId); + } + /// /// Adds a specific modifier to the player. /// 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/33] 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 b3c56229..a152d0b7 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -203,6 +203,18 @@ public IEnumerable GetModifiers(uint id, Func? return GetModifiers(type, predicate); } + /// + /// Gets a collection of modifiers by type, if the type is an interface. + /// + /// The predicate to check the modifier by. + /// The Type of the interface of the Modifier. + /// A collection of modifiers. + [HideFromIl2Cpp] + public IEnumerable GetModifiersOfType(Func? predicate = null) where T : class + { + return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x)); + } + /// /// Tries to get a modifier by its type. /// @@ -258,6 +270,20 @@ public bool TryGetModifier(Guid modifierGuid, [NotNullWhen(true)] out BaseModifi return modifier != null; } + /// + /// Tries to get a modifier by its type, if the type is an interface. + /// + /// The modifier or null. + /// The predicate to check the modifier by. + /// The Type of the interface of the Modifier. + /// True if the modifier was found, false otherwise. + [HideFromIl2Cpp] + public bool TryGetModifierOfType([NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : class + { + modifier = GetModifierOfType(predicate); + return modifier != null; + } + /// /// Gets a modifier by its type. /// @@ -308,6 +334,18 @@ public bool TryGetModifier(Guid modifierGuid, [NotNullWhen(true)] out BaseModifi return ActiveModifiers.Find(x => x.UniqueId == modifierGuid); } + /// + /// Gets a modifier by its type, if the type is an interface. + /// + /// The predicate to check the modifier by. + /// The Type of the interface of the Modifier. + /// The Modifier if it is found, null otherwise. + [HideFromIl2Cpp] + public T? GetModifierOfType(Func? predicate = null) where T : class + { + return GetModifiersOfType(predicate).FirstOrDefault(); + } + /// /// Removes a modifier from the player. /// @@ -656,4 +694,30 @@ public bool HasModifier(Guid id, bool checkInactive) return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr)); bool MatchExpr(BaseModifier bm) => bm.UniqueId == id; } + + /// + /// Checks if a player has an active modifier by its type, if the type is an interface. + /// + /// The predicate to check the modifier. + /// The Type of the interface of the Modifier. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifierOfType(Func? predicate=null) where T : class + { + return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); + } + + /// + /// Checks if a player has an active modifier by its type, if the type is an interface. + /// + /// Whether to check inactive modifiers (those pending to be added). + /// The predicate to check the modifier. + /// The Type of the interface of the Modifier. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifierOfType(bool checkInactive, Func? predicate=null) where T : class + { + return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr)); + bool MatchExpr(BaseModifier bm) => bm is T modifier && (predicate == null || predicate(modifier)); + } } 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/33] 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 bc3433f0..cccba45a 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -191,6 +191,18 @@ public static bool HasModifier(this PlayerControl player, Guid uniqueId) return player.GetModifierComponent().HasModifier(uniqueId); } + /// + /// Checks if the player has a specific modifier, if the type is an interface. + /// + /// The type of the interface of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// True if the player has the modifier, false otherwise. + public static bool HasModifierOfType(this PlayerControl player, Func? predicate = null) where T : class + { + return player.GetModifierComponent().HasModifierOfType(predicate); + } + /// /// Clears all modifiers from a player. /// @@ -266,6 +278,19 @@ public static bool TryGetModifier(this PlayerControl player, Guid modifierGuid, return player.GetModifierComponent().TryGetModifier(modifierGuid, out modifier); } + /// + /// Tries to get a modifier by its type, if the type is an interface. + /// + /// The PlayerControl instance. + /// The modifier or null. + /// The predicate to check the modifier by. + /// The Type of the interface of the Modifier. + /// True if the modifier was found, false otherwise. + public static bool TryGetModifierOfType(this PlayerControl player, [NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : class + { + return player.GetModifierComponent().TryGetModifierOfType(out modifier, predicate); + } + /// /// Gets a specific modifier from the player. /// @@ -321,6 +346,18 @@ public static bool TryGetModifier(this PlayerControl player, Guid modifierGuid, return player.GetModifierComponent().GetModifier(uniqueId); } + /// + /// Gets a specific modifier from the player, if the type is an interface. + /// + /// The type of the interface of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// The modifier if found, null otherwise. + public static T? GetModifierOfType(this PlayerControl player, Func? predicate = null) where T : class + { + return player.GetModifierComponent().GetModifierOfType(predicate); + } + /// /// Gets all modifiers of a specific type from the player. /// @@ -364,6 +401,19 @@ public static IEnumerable GetModifiers( return player.GetModifierComponent().GetModifiers(typeId, predicate); } + /// + /// Gets all modifiers of a specific type from the player, if the type is an interface. + /// + /// The type of the interface of the modifiers. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// A collection of modifiers. + public static IEnumerable GetModifiersOfType(this PlayerControl player, Func? predicate = null) + where T : class + { + return player.GetModifierComponent().GetModifiersOfType(predicate); + } + /// /// Removes a specific modifier from the player. /// 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/33] 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 54680f2c..ac3582b2 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -32,3 +32,33 @@ public override object GetValue() : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); } } + +/// +/// Attribute for creating an enum option. +/// +/// The enum type. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionAttribute(string title, string[]? values = null) + : ModdedOptionAttribute(title) where T : Enum +{ + internal override IModdedOption CreateOption(object? value, PropertyInfo property) + { + var opt = new ModdedEnumOption(Title, (T)(value ?? 0), values); + return opt; + } + + /// + public override void SetValue(object value) + { + var opt = HolderOption as ModdedEnumOption; + opt?.SetValue((T)value); + } + + /// + public override object GetValue() + { + return HolderOption is ModdedEnumOption opt + ? opt.Value + : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption"); + } +} 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/33] 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 00000000..da6da53b --- /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/33] 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 00000000..be14e142 --- /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/33] 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 be14e142..4c614856 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 OptionBehaviour CreateOption( 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/33] 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 4c614856..927bcb22 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -263,3 +263,101 @@ public override void LoadFromPreset(ConfigFile presetConfig) /// 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/33] Removed middleman classes and fields that can be handled much easier --- MiraAPI/GameOptions/IModdedOptionList.cs | 49 +-- .../OptionTypes/ModdedOptionList.cs | 298 ++---------------- 2 files changed, 30 insertions(+), 317 deletions(-) diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs index da6da53b..26f9bee9 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. - /// - 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 . + /// Gets the set of ConfigDefinition for the options, used for BepInEx configuration. /// - /// 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 927bcb22..aca5b284 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,11 +38,17 @@ public IMiraPlugin? ParentMod if (_parentMod != null || value == null) return; _parentMod = value; - OnParentModChange(); + foreach (var option in Options) + { + option.ParentMod = _parentMod!; + } } } - protected abstract void OnParentModChange(); + /// + /// Gets the list of options. + /// + public T[] Options { get; } /// public Func Visible { get; set; } @@ -60,251 +56,33 @@ public IMiraPlugin? ParentMod /// public bool IncludeInPreset { get; set; } - protected readonly OptionBehaviour?[] _optionBehaviours; - /// - public IReadOnlyList OptionBehaviours => _optionBehaviours; + public IReadOnlyList OptionBehaviours => FromOptions(o => o.OptionBehaviour); /// - public ConfigDefinition?[] ConfigDefinitions { get; set; } + 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 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 ModdedOptionList(string title, int count, bool includeInPreset = true, bool zeroIndexTitle = false) + public ModdedOptionList(int count, Func optionFactory, bool includeInPreset = true) { 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(); + Options = Enumerable.Range(0, Count).Select(optionFactory).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() + internal IReadOnlyList FromOptions(Func getter) { - 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); + return Options.Select(getter).ToArray(); } /// - 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!; - } - } - - /// - /// 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) + public void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false) { foreach (var option in Options) { @@ -313,7 +91,7 @@ public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = fa } /// - public override void Bind(ConfigFile config) + public void Bind(ConfigFile config) { foreach (var option in Options) { @@ -322,7 +100,7 @@ public override void Bind(ConfigFile config) } /// - public override void LoadFromPreset(ConfigFile presetConfig) + public void LoadFromPreset(ConfigFile presetConfig) { foreach (var option in Options) { @@ -330,30 +108,6 @@ public override void LoadFromPreset(ConfigFile 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 . /// 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/33] 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 26f9bee9..4fed3b31 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 aca5b284..7ff88bfc 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 IMiraPlugin? ParentMod /// 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 ModdedOptionList(int count, Func optionFactory, bool includeInPre 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/33] 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 7ff88bfc..cc7bdfdd 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 IMiraPlugin? ParentMod /// /// Gets the list of options. /// - public T[] Options { get; } + public IReadOnlyList Options { get; } /// public Func Visible { get; set; } @@ -85,6 +86,17 @@ public void LoadFromPreset(ConfigFile presetConfig) } } + /// + 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/33] 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 cc7bdfdd..a32a7278 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 IMiraPlugin? ParentMod 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/33] 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 4fed3b31..62dd7d5c 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 a32a7278..7e97a1a2 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -38,12 +38,13 @@ public IMiraPlugin? ParentMod 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 Func Visible } /// - 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 bool IncludeInPreset /// 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/33] 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 5a3c3f8f..824a8329 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 62dd7d5c..00000000 --- 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 7e97a1a2..12f725e2 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 ModdedOptionList(int count, Func optionFactory, bool? includeInPr 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/33] 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 206894dc..644c87c8 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -15,8 +15,6 @@ namespace MiraAPI.GameOptions.OptionTypes; /// The value type. public abstract class ModdedOption : IModdedOption { - private IMiraPlugin? _parentMod; - /// public uint Id { get; } @@ -32,13 +30,13 @@ public abstract class ModdedOption : IModdedOption /// public IMiraPlugin? ParentMod { - get => _parentMod; + get; set { - if (_parentMod != null || value == null) return; - _parentMod = value; + if (field != null || value == null) return; + field = value; - var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue); + var entry = field.GetConfigFile().Bind(ConfigDefinition, DefaultValue); Value = entry.Value; } } @@ -54,9 +52,9 @@ public IMiraPlugin? ParentMod public T DefaultValue { get; } /// - /// Gets or sets the event that is invoked when the value of the option changes. + /// Gets the event that is invoked when the value of the option changes. /// - public Action? ChangedEvent { get; set; } + public Action? ChangedEvent { get; init; } /// public Func Visible { get; set; } 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/33] 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 824a8329..8d3e751a 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 12f725e2..85f650a8 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -36,47 +36,15 @@ public IMiraPlugin? ParentMod /// 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/33] 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 8d3e751a..e21c3b29 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; @@ -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 85f650a8..13609d95 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 ModdedOptionList(int count, Func optionFactory) } /// - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { - return ((IEnumerable)Options).GetEnumerator(); + return ((IEnumerable)Options).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() @@ -64,4 +47,6 @@ IEnumerator IEnumerable.GetEnumerator() /// 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/33] 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 3b85a73c..8fc1b1a8 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -113,6 +113,26 @@ internal static void RegisterAttributeOption( RegisterOption(option, group, property.Name, pluginInfo); } + internal static void RegisterPropertyOptionList(Type type, PropertyInfo property, MiraPluginInfo pluginInfo) + { + if (!TypeToGroup.TryGetValue(type, out var group)) + { + Error($"Failed to get group for {type.Name}"); + return; + } + + if (property.GetValue(group) is not IModdedOptionList optionList) + { + Error($"Failed to get option list for {property.Name}"); + return; + } + + for (int i = 0; i < optionList.Count; i++) + { + RegisterOption(optionList[i], group, property.Name + i, pluginInfo); + } + } + internal static void RegisterOption( IModdedOption option, AbstractOptionGroup group, diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 26a881f4..607b9050 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -201,12 +201,16 @@ private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo) } var attribute = property.GetCustomAttribute(); - if (attribute == null) + if (attribute != null) { + ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo); continue; } - ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo); + if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList))) + { + ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo); + } } 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/33] 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 13609d95..5a764b21 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -30,6 +30,16 @@ public ModdedOptionList(int count, Func optionFactory) 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/33] 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 702d75e3..e2014fb5 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/33] 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 607b9050..443d206b 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -213,7 +213,8 @@ private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo) } } - foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption)))) + foreach (var field in type.GetFields() + .Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption)) || f.FieldType.IsAssignableTo(typeof(IModdedOptionList)))) { Error($"{field.Name} is a field, not a property. Use properties for options."); } 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/33] 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 00000000..116dd58e --- /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 8fc1b1a8..3b6708ff 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 @@ internal static void RegisterAttributeOption( } 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 @@ internal static void RegisterPropertyOptionList(Type type, PropertyInfo property } } + 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 bool PropertyGetterPatch(MethodBase __originalMethod, ref object _ __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 443d206b..bb4e6cc7 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -211,6 +211,12 @@ private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo) { 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/33] 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 116dd58e..de927ab0 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/33] 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 e2014fb5..62ce43b6 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 de927ab0..bce0190b 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 void SetValue(object value) /// 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 00000000..643d7b7e --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Represents an attribute that is used to intercept a property's getter and setter. +/// +[AttributeUsage(AttributeTargets.Property)] +public abstract class PropertyOptionAttribute : Attribute +{ + /// + /// Sets the value of the option. + /// + /// The new value as an object. + public abstract void SetValue(object value); + + /// + /// Gets the value of the option. + /// + /// The value of the option as an object. + public abstract object GetValue(); +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 3b6708ff..00b726d7 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 @@ internal static void RegisterAttributeOption( } 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 @@ internal static void RegisterAttributeOptionList( 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 @@ internal static void RegisterAttributeOptionList( // 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 bool PropertyGetterPatch(MethodBase __originalMethod, ref object _ __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/33] 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 00b726d7..1706719c 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 @@ internal static void RegisterAttributeOptionList( 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/33] 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 bce0190b..3e26432d 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 override void SetValue(object value) /// 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 1706719c..3dbab02a 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -168,6 +168,11 @@ internal static void RegisterAttributeOptionList( 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 @@ internal static void RegisterAttributeOptionList( 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 bool PropertyGetterPatch(MethodBase __originalMethod, ref object _ __result = attribute.GetValue(); return false; } + + /// + /// Patches the setter of a list property to update the value of the option. + /// + /// The 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/33] 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 3e26432d..4a89375d 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 override void SetValue(object value) /// 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 3dbab02a..d9c42bc8 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -194,6 +194,7 @@ internal static void RegisterAttributeOptionList( 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/33] 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 d9c42bc8..785c95de 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -295,30 +295,34 @@ public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object _ /// /// 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/33] 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 4a89375d..a945b1d7 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 override void SetValue(object value) 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/33] 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 a945b1d7..74615ddd 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs @@ -35,16 +35,16 @@ public override void SetValue(object value) 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 override object GetValue() /// /// 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 785c95de..ffb85e30 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -305,7 +305,7 @@ public static void PropertyListSetterPatch(object __instance, int index, object { 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 bool PropertyListGetterPatch(object __instance, int index, ref obj { 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/33] 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 00000000..c8ec4472 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs @@ -0,0 +1,67 @@ +using MiraAPI.GameOptions.OptionTypes; +using System; +using System.Collections; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for creating a list of enum options. +/// +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionListAttribute(Func titler, Type enumType, string[]? values = null) + : ModdedOptionListAttribute(titler) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList( + value.Count, idx => new(Titler(idx), (int)(value[idx] ?? 0), enumType, values)); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedEnumOption; + opt?.SetValue((int)value); + } + + /// + public override object GetValue(int idx) + { + return HolderOptionList?[idx] is ModdedEnumOption opt + ? Enum.ToObject(enumType, opt.Value) + : throw new InvalidOperationException($"HolderOption for option \"{Titler(idx)}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); + } +} + +/// +/// Attribute for creating a list of enum options. +/// +/// The enum type. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionListAttribute(Func titler, string[]? values = null) + : ModdedOptionListAttribute(titler) where T : Enum +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList>( + value.Count, idx => new(Titler(idx), (T)(value[idx] ?? 0), values)); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedEnumOption; + opt?.SetValue((T)value); + } + + /// + public override object GetValue(int idx) + { + return HolderOptionList?[idx] is ModdedEnumOption opt + ? opt.Value + : throw new InvalidOperationException($"HolderOption for option \"{Titler(idx)}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs new file mode 100644 index 00000000..18d7f37d --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs @@ -0,0 +1,55 @@ +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; +using System; +using System.Collections; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// A number option attribute for the modded options system. +/// +[AttributeUsage(AttributeTargets.Property)] +public class ModdedNumberOptionListAttribute( + Func titler, + float min, + float max, + float increment = 1, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null, + bool zeroInfinity = false) + : ModdedOptionListAttribute(titler) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + return new ModdedOptionList(value.Count, idx => + { + return new( + Titler(idx), + (float)(value[idx] ?? min + increment), + min, + max, + increment, + suffixType, + formatString, + zeroInfinity); + }); + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedNumberOption; + opt?.SetValue((float)value); + } + + /// + public override object GetValue(int idx) + { + if (HolderOptionList?[idx] is ModdedNumberOption opt) + { + return opt.Value; + } + throw new InvalidOperationException($"HolderOption for option \"{Titler(idx)}\" is not a ModdedNumberOption"); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs new file mode 100644 index 00000000..7e34080a --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs @@ -0,0 +1,38 @@ +using MiraAPI.GameOptions.OptionTypes; +using System; +using System.Collections; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes; + +/// +/// Attribute for a list of toggle options. +/// +/// The function to get each option's title. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedToggleOptionListAttribute(Func titler) : ModdedOptionListAttribute(titler) +{ + internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property) + { + var optList = new ModdedOptionList( + value.Count, idx => new(Titler(idx), (bool)(value[idx] ?? false))); + return optList; + } + + /// + public override void SetValue(int idx, object value) + { + var opt = HolderOptionList?[idx] as ModdedToggleOption; + opt?.SetValue((bool)value); + } + + /// + public override object GetValue(int idx) + { + if (HolderOptionList?[idx] is ModdedToggleOption opt) + { + return opt.Value; + } + throw new InvalidOperationException($"Holder option for {Titler(idx)} is not a ModdedToggleOption."); + } +}