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.");
+ }
+}