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 001/276] Added remove self method for modifiers
---
MiraAPI/Modifiers/BaseModifier.cs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs
index 0e31779..bc65692 100644
--- a/MiraAPI/Modifiers/BaseModifier.cs
+++ b/MiraAPI/Modifiers/BaseModifier.cs
@@ -128,4 +128,12 @@ public abstract class BaseModifier : IOptionable
///
/// True if the player can vent, false otherwise. Null for no effect.
public virtual bool? CanVent() => null;
+
+ ///
+ /// Removes this modifier instance from the player.
+ ///
+ public void RemoveSelf()
+ {
+ ModifierComponent?.RemoveModifier(this);
+ }
}
From 388b6ec676bdc3c7d613c61ddba44de1deb2456e Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 13:17:31 -0600
Subject: [PATCH 002/276] Added TryRemoveModifier methods to simplify get and
remove modifiers
---
MiraAPI/Modifiers/ModifierComponent.cs | 71 ++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs
index d64f366..b3c5622 100644
--- a/MiraAPI/Modifiers/ModifierComponent.cs
+++ b/MiraAPI/Modifiers/ModifierComponent.cs
@@ -389,6 +389,77 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
RemoveModifier(modifier);
}
+ ///
+ /// Tries to remove a modifier from the player.
+ ///
+ /// The modifier type.
+ /// The predicate to check the modifier by.
+ /// if the modifier is not active on this player, or there are multiple instances;
+ /// else .
+ [HideFromIl2Cpp]
+ public bool TryRemoveModifier(Func? predicate = null) where T : BaseModifier
+ {
+ return TryGetModifier(out var modifier, predicate) &&
+ TryRemoveModifier(modifier);
+ }
+
+ ///
+ /// Tries to remove a modifier from the player.
+ ///
+ /// The modifier type.
+ /// The predicate to check the modifier by.
+ /// if the modifier is not active on this player, or there are multiple instances;
+ /// else .
+ [HideFromIl2Cpp]
+ public bool TryRemoveModifier(Type type, Func? predicate = null)
+ {
+ return TryGetModifier(type, out var modifier, predicate) &&
+ TryRemoveModifier(modifier);
+ }
+
+ ///
+ /// Tries to remove a modifier from the player.
+ ///
+ /// The modifier object.
+ /// if the modifier is not active on this player, else .
+ [HideFromIl2Cpp]
+ public bool TryRemoveModifier(BaseModifier modifier)
+ {
+ if (!ActiveModifiers.Contains(modifier))
+ {
+ return false;
+ }
+
+ _toRemove.Add(modifier);
+ return true;
+ }
+
+ ///
+ /// Tries to remove a modifier from the player.
+ ///
+ /// The modifier's type ID.
+ /// The predicate to check the modifier by.
+ /// if the modifier is not active on this player, or there are multiple instances;
+ /// else .
+ [HideFromIl2Cpp]
+ public bool TryRemoveModifier(uint typeId, Func? predicate = null)
+ {
+ return TryGetModifier(typeId, out var modifier, predicate) &&
+ TryRemoveModifier(modifier);
+ }
+
+ ///
+ /// Tries to remove a modifier from the player.
+ ///
+ /// The modifier's unique ID.
+ /// if the modifier is not active on this player, else .
+ [HideFromIl2Cpp]
+ public bool TryRemoveModifier(Guid uniqueId)
+ {
+ return TryGetModifier(uniqueId, out var modifier) &&
+ TryRemoveModifier(modifier);
+ }
+
///
/// Adds a modifier to the player.
///
From 29779f37113f9150563ff10cdf03dd6fd70af2b3 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 13:24:28 -0600
Subject: [PATCH 003/276] Added similar extensions with new try remove methods
---
MiraAPI/Modifiers/ModifierExtensions.cs | 67 +++++++++++++++++++++++++
1 file changed, 67 insertions(+)
diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs
index 7a0ef23..bc3433f 100644
--- a/MiraAPI/Modifiers/ModifierExtensions.cs
+++ b/MiraAPI/Modifiers/ModifierExtensions.cs
@@ -423,6 +423,73 @@ public static class ModifierExtensions
player.GetModifierComponent().RemoveModifier(uniqueId);
}
+ ///
+ /// Tries to remove a specific modifier from the player.
+ ///
+ /// The PlayerControl instance.
+ /// The modifier to remove.
+ /// if the modifier is not active on this player, else .
+ public static bool TryRemoveModifier(this PlayerControl player, BaseModifier modifier)
+ {
+ return player.GetModifierComponent().TryRemoveModifier(modifier);
+ }
+
+ ///
+ /// Tries to remove a specific modifier from the player.
+ ///
+ /// The type of the modifier.
+ /// The PlayerControl instance.
+ /// Optional predicate to filter the modifiers.
+ /// if the modifier is not active on this player, or there are multiple instances;
+ /// else .
+ public static bool TryRemoveModifier(this PlayerControl player, Func? predicate = null)
+ where T : BaseModifier
+ {
+ return player.GetModifierComponent().TryRemoveModifier(predicate);
+ }
+
+ ///
+ /// Tries to remove a specific modifier from the player by type.
+ ///
+ /// The PlayerControl instance.
+ /// The type of the modifier.
+ /// Optional predicate to filter the modifiers.
+ /// if the modifier is not active on this player, or there are multiple instances;
+ /// else .
+ public static bool TryRemoveModifier(this PlayerControl player, Type type, Func? predicate = null)
+ {
+ return player.GetModifierComponent().TryRemoveModifier(type, predicate);
+ }
+
+ ///
+ /// Tries to remove a specific modifier from the player by its type ID.
+ ///
+ /// The PlayerControl instance.
+ /// The type ID of the modifier.
+ /// Optional predicate to filter the modifiers.
+ /// if the modifier is not active on this player, or there are multiple instances;
+ /// else .
+ public static bool TryRemoveModifier(
+ this PlayerControl player,
+ uint typeId,
+ Func? predicate = null)
+ {
+ return player.GetModifierComponent().TryRemoveModifier(typeId, predicate);
+ }
+
+ ///
+ /// Tries to remove a specific modifier from the player by its GUID.
+ ///
+ /// The PlayerControl instance.
+ /// The GUID of the modifier.
+ /// if the modifier is not active on this player, else .
+ public static bool TryRemoveModifier(
+ this PlayerControl player,
+ Guid uniqueId)
+ {
+ return player.GetModifierComponent().TryRemoveModifier(uniqueId);
+ }
+
///
/// Adds a specific modifier to the player.
///
From 34c172b6f1b58b09eac149f96b9a25370c4bb38a Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 13:37:22 -0600
Subject: [PATCH 004/276] Added methods to get modifiers from an interface type
---
MiraAPI/Modifiers/ModifierComponent.cs | 64 ++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs
index b3c5622..a152d0b 100644
--- a/MiraAPI/Modifiers/ModifierComponent.cs
+++ b/MiraAPI/Modifiers/ModifierComponent.cs
@@ -203,6 +203,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
return GetModifiers(type, predicate);
}
+ ///
+ /// Gets a collection of modifiers by type, if the type is an interface.
+ ///
+ /// The predicate to check the modifier by.
+ /// The Type of the interface of the Modifier.
+ /// A collection of modifiers.
+ [HideFromIl2Cpp]
+ public IEnumerable GetModifiersOfType(Func? predicate = null) where T : class
+ {
+ return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x));
+ }
+
///
/// Tries to get a modifier by its type.
///
@@ -258,6 +270,20 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
return modifier != null;
}
+ ///
+ /// Tries to get a modifier by its type, if the type is an interface.
+ ///
+ /// The modifier or null.
+ /// The predicate to check the modifier by.
+ /// The Type of the interface of the Modifier.
+ /// True if the modifier was found, false otherwise.
+ [HideFromIl2Cpp]
+ public bool TryGetModifierOfType([NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : class
+ {
+ modifier = GetModifierOfType(predicate);
+ return modifier != null;
+ }
+
///
/// Gets a modifier by its type.
///
@@ -308,6 +334,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
return ActiveModifiers.Find(x => x.UniqueId == modifierGuid);
}
+ ///
+ /// Gets a modifier by its type, if the type is an interface.
+ ///
+ /// The predicate to check the modifier by.
+ /// The Type of the interface of the Modifier.
+ /// The Modifier if it is found, null otherwise.
+ [HideFromIl2Cpp]
+ public T? GetModifierOfType(Func? predicate = null) where T : class
+ {
+ return GetModifiersOfType(predicate).FirstOrDefault();
+ }
+
///
/// Removes a modifier from the player.
///
@@ -656,4 +694,30 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
bool MatchExpr(BaseModifier bm) => bm.UniqueId == id;
}
+
+ ///
+ /// Checks if a player has an active modifier by its type, if the type is an interface.
+ ///
+ /// The predicate to check the modifier.
+ /// The Type of the interface of the Modifier.
+ /// True if the Modifier is present, false otherwise.
+ [HideFromIl2Cpp]
+ public bool HasModifierOfType(Func? predicate=null) where T : class
+ {
+ return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier)));
+ }
+
+ ///
+ /// Checks if a player has an active modifier by its type, if the type is an interface.
+ ///
+ /// Whether to check inactive modifiers (those pending to be added).
+ /// The predicate to check the modifier.
+ /// The Type of the interface of the Modifier.
+ /// True if the Modifier is present, false otherwise.
+ [HideFromIl2Cpp]
+ public bool HasModifierOfType(bool checkInactive, Func? predicate=null) where T : class
+ {
+ return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
+ bool MatchExpr(BaseModifier bm) => bm is T modifier && (predicate == null || predicate(modifier));
+ }
}
From 01fe88ede9ee1ed4de23f6a8ffe427390696f89a Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 13:47:07 -0600
Subject: [PATCH 005/276] Added equivalent extensions of Modifier OfType
methods
---
MiraAPI/Modifiers/ModifierExtensions.cs | 50 +++++++++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs
index bc3433f..cccba45 100644
--- a/MiraAPI/Modifiers/ModifierExtensions.cs
+++ b/MiraAPI/Modifiers/ModifierExtensions.cs
@@ -191,6 +191,18 @@ public static class ModifierExtensions
return player.GetModifierComponent().HasModifier(uniqueId);
}
+ ///
+ /// Checks if the player has a specific modifier, if the type is an interface.
+ ///
+ /// The type of the interface of the modifier.
+ /// The PlayerControl instance.
+ /// Optional predicate to filter the modifiers.
+ /// True if the player has the modifier, false otherwise.
+ public static bool HasModifierOfType(this PlayerControl player, Func? predicate = null) where T : class
+ {
+ return player.GetModifierComponent().HasModifierOfType(predicate);
+ }
+
///
/// Clears all modifiers from a player.
///
@@ -266,6 +278,19 @@ public static class ModifierExtensions
return player.GetModifierComponent().TryGetModifier(modifierGuid, out modifier);
}
+ ///
+ /// Tries to get a modifier by its type, if the type is an interface.
+ ///
+ /// The PlayerControl instance.
+ /// The modifier or null.
+ /// The predicate to check the modifier by.
+ /// The Type of the interface of the Modifier.
+ /// True if the modifier was found, false otherwise.
+ public static bool TryGetModifierOfType(this PlayerControl player, [NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : class
+ {
+ return player.GetModifierComponent().TryGetModifierOfType(out modifier, predicate);
+ }
+
///
/// Gets a specific modifier from the player.
///
@@ -321,6 +346,18 @@ public static class ModifierExtensions
return player.GetModifierComponent().GetModifier(uniqueId);
}
+ ///
+ /// Gets a specific modifier from the player, if the type is an interface.
+ ///
+ /// The type of the interface of the modifier.
+ /// The PlayerControl instance.
+ /// Optional predicate to filter the modifiers.
+ /// The modifier if found, null otherwise.
+ public static T? GetModifierOfType(this PlayerControl player, Func? predicate = null) where T : class
+ {
+ return player.GetModifierComponent().GetModifierOfType(predicate);
+ }
+
///
/// Gets all modifiers of a specific type from the player.
///
@@ -364,6 +401,19 @@ public static class ModifierExtensions
return player.GetModifierComponent().GetModifiers(typeId, predicate);
}
+ ///
+ /// Gets all modifiers of a specific type from the player, if the type is an interface.
+ ///
+ /// The type of the interface of the modifiers.
+ /// The PlayerControl instance.
+ /// Optional predicate to filter the modifiers.
+ /// A collection of modifiers.
+ public static IEnumerable GetModifiersOfType(this PlayerControl player, Func? predicate = null)
+ where T : class
+ {
+ return player.GetModifierComponent().GetModifiersOfType(predicate);
+ }
+
///
/// Removes a specific modifier from the player.
///
From 690b1b2daef2bc66343b5af633f0c2a723ec5cad Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 14:23:27 -0600
Subject: [PATCH 006/276] Added generic Enum Option attribute
---
.../Attributes/ModdedEnumOptionAttribute.cs | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs
index 54680f2..ac3582b 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs
@@ -32,3 +32,33 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption");
}
}
+
+///
+/// Attribute for creating an enum option.
+///
+/// The enum type.
+[AttributeUsage(AttributeTargets.Property)]
+public class ModdedEnumOptionAttribute(string title, string[]? values = null)
+ : ModdedOptionAttribute(title) where T : Enum
+{
+ internal override IModdedOption CreateOption(object? value, PropertyInfo property)
+ {
+ var opt = new ModdedEnumOption(Title, (T)(value ?? 0), values);
+ return opt;
+ }
+
+ ///
+ public override void SetValue(object value)
+ {
+ var opt = HolderOption as ModdedEnumOption;
+ opt?.SetValue((T)value);
+ }
+
+ ///
+ public override object GetValue()
+ {
+ return HolderOption is ModdedEnumOption opt
+ ? opt.Value
+ : throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption");
+ }
+}
From 42eba6280da3778ccf496f5eb3fd4acbd1be1d32 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 19:10:09 -0600
Subject: [PATCH 007/276] Created interface for sets of equal/comparable modded
options
---
MiraAPI/GameOptions/IModdedOptionList.cs | 122 +++++++++++++++++++++++
1 file changed, 122 insertions(+)
create mode 100644 MiraAPI/GameOptions/IModdedOptionList.cs
diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs
new file mode 100644
index 0000000..da6da53
--- /dev/null
+++ b/MiraAPI/GameOptions/IModdedOptionList.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using BepInEx.Configuration;
+using MiraAPI.Networking;
+using MiraAPI.PluginLoading;
+using UnityEngine;
+
+namespace MiraAPI.GameOptions;
+
+///
+/// Interface for list of modded options.
+///
+public interface IModdedOptionList
+{
+ ///
+ /// Gets the number of options in the list.
+ ///
+ int Count { get; }
+
+ ///
+ /// Gets the unique identifiers for the options.
+ ///
+ IReadOnlyList Ids { get; }
+
+ ///
+ /// Gets or sets the titles of the options.
+ /// The end title will equal this value with its index appended (e.g., Title1).
+ ///
+ string Title { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the first option's title has a 0, else starts with 1.
+ ///
+ bool ZeroIndexTitle { get; set; }
+
+ ///
+ /// Gets the StringName for the options, used for localization.
+ ///
+ IReadOnlyList StringNames { get; }
+
+ ///
+ /// Gets or sets the MiraPlugin that created these options.
+ ///
+ IMiraPlugin? ParentMod { get; set; }
+
+ ///
+ /// Gets the game setting data for the options.
+ ///
+ IReadOnlyList Data { get; }
+
+ ///
+ /// Gets the OptionBehaviour object of the options.
+ ///
+ IReadOnlyList OptionBehaviours { get; }
+
+ ///
+ /// Gets or sets the visibility function for the options.
+ ///
+ Func Visible { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the options should be included with presets.
+ ///
+ bool IncludeInPreset { get; set; }
+
+ ///
+ /// Gets the array of ConfigDefinition for the options, used for BepInEx configuration.
+ ///
+ ConfigDefinition?[] ConfigDefinitions { get; }
+
+ ///
+ /// Creates the option behaviour for the modded option at index .
+ ///
+ /// The option's index.
+ /// The ToggleOption template.
+ /// The NumberOption template.
+ /// The StringOption template.
+ /// The PlayerOption template.
+ /// >The Transform container for the option.
+ /// The created OptionBehaviour object.
+ OptionBehaviour CreateOption(int idx, ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container);
+
+ ///
+ /// Gets the value at index as a float.
+ ///
+ /// The option's index.
+ /// The value of the option as a float.
+ float GetFloatData(int idx);
+
+ ///
+ /// Gets the NetData for the option at index , used for network synchronization.
+ ///
+ /// The option's index.
+ /// Returns the NetData object for the option.
+ NetData GetNetData(int idx);
+
+ ///
+ /// Handles incoming network data for the option at index .
+ ///
+ /// The option's index.
+ /// The byte array representing the network data.
+ void HandleNetData(int idx, byte[] data);
+
+ ///
+ /// Saves the options to a preset configuration file.
+ ///
+ /// The ConfigFile representing the preset configuration.
+ /// Indicates whether to save the default value instead of the current value.
+ void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
+
+ ///
+ /// Binds the options to a configuration file.
+ ///
+ /// The ConfigFile to bind the option to.
+ void Bind(ConfigFile config);
+
+ ///
+ /// Loads the options from a preset configuration file, applying the values to the options' configuration.
+ ///
+ /// The ConfigFile representing the preset configuration.
+ void LoadFromPreset(ConfigFile presetConfig);
+}
From 612597566b773676500d8acbbd09c9bb52eae2cd Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 21:35:03 -0600
Subject: [PATCH 008/276] Created abstract class for Option list
---
.../OptionTypes/ModdedOptionList.cs | 120 ++++++++++++++++++
1 file changed, 120 insertions(+)
create mode 100644 MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
new file mode 100644
index 0000000..be14e14
--- /dev/null
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using BepInEx.Configuration;
+using MiraAPI.Networking;
+using MiraAPI.PluginLoading;
+using Reactor.Localization.Utilities;
+using Reactor.Networking.Rpc;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace MiraAPI.GameOptions.OptionTypes;
+
+///
+/// Represents a modded option list.
+///
+public abstract class ModdedOptionList : IModdedOptionList
+{
+ protected IMiraPlugin? _parentMod;
+
+ ///
+ public int Count { get; }
+
+ ///
+ public IReadOnlyList Ids { get; }
+
+ ///
+ public string Title { get; set; }
+
+ ///
+ public bool ZeroIndexTitle { get; set; }
+
+ ///
+ public IReadOnlyList StringNames { get; }
+
+ ///
+ public IReadOnlyList Data { get; protected set; } = [];
+
+ ///
+ public IMiraPlugin? ParentMod
+ {
+ get => _parentMod;
+ set
+ {
+ if (_parentMod != null || value == null) return;
+ _parentMod = value;
+
+ OnParentModChange();
+ }
+ }
+
+ protected abstract void OnParentModChange();
+
+ ///
+ public Func Visible { get; set; }
+
+ ///
+ public bool IncludeInPreset { get; set; }
+
+ protected readonly OptionBehaviour?[] _optionBehaviours;
+
+ ///
+ public IReadOnlyList OptionBehaviours => _optionBehaviours;
+
+ ///
+ public ConfigDefinition?[] ConfigDefinitions { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The options' title.
+ /// The option list's length.
+ /// Whether to include the options in the preset.
+ /// Whether the first option's title index is 0, else 1.
+ protected ModdedOptionList(string title, int count, bool includeInPreset = true, bool zeroIndexTitle = false)
+ {
+ Count = count;
+ ZeroIndexTitle = zeroIndexTitle;
+
+ var range = Enumerable.Range(0, Count);
+ var titleOffset = ZeroIndexTitle ? 0 : 1;
+
+ Ids = range.Select(_ => ModdedOptionsManager.NextId).ToList();
+ Title = title;
+ StringNames = range.Select(i => CustomStringName.CreateAndRegister($"{Title}{i + titleOffset}")).ToList();
+ Visible = _ => true;
+ IncludeInPreset = includeInPreset;
+
+ _optionBehaviours = range.Select(_ => null).ToArray();
+ ConfigDefinitions = range.Select(_ => null).ToArray();
+ }
+
+ ///
+ public abstract void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
+
+ ///
+ public abstract void Bind(ConfigFile config);
+
+ ///
+ public abstract void LoadFromPreset(ConfigFile presetConfig);
+
+ ///
+ public abstract float GetFloatData(int idx);
+
+ ///
+ public abstract NetData GetNetData(int idx);
+
+ ///
+ public abstract void HandleNetData(int idx, byte[] data);
+
+ ///
+ public abstract OptionBehaviour CreateOption(
+ int idx,
+ ToggleOption toggleOpt,
+ NumberOption numberOpt,
+ StringOption stringOpt,
+ PlayerOption playerOpt,
+ Transform container);
+}
From 71a34614326a6cd0f5ebbf33d5e2317e3514194d Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 21:36:16 -0600
Subject: [PATCH 009/276] Created subclass with generic values
---
.../OptionTypes/ModdedOptionList.cs | 147 +++++++++++++++++-
1 file changed, 146 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index be14e14..4c61485 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -34,8 +34,10 @@ public abstract class ModdedOptionList : IModdedOptionList
///
public IReadOnlyList StringNames { get; }
+ protected readonly BaseGameSetting[] _data;
+
///
- public IReadOnlyList Data { get; protected set; } = [];
+ public IReadOnlyList Data => _data;
///
public IMiraPlugin? ParentMod
@@ -118,3 +120,146 @@ public abstract class ModdedOptionList : IModdedOptionList
PlayerOption playerOpt,
Transform container);
}
+
+///
+/// Represents a modded option list.
+///
+/// The value type.
+public abstract class ModdedOptionList : ModdedOptionList
+{
+ protected override void OnParentModChange()
+ {
+ var configFile = _parentMod!.GetConfigFile();
+ for (int i = 0; i < Count; i++)
+ {
+ var entry = configFile.Bind(ConfigDefinitions[i], DefaultValue(i));
+ Values[i] = entry.Value;
+ }
+ }
+
+ ///
+ /// Gets the list of values of the options.
+ ///
+ public T[] Values { get; }
+
+ ///
+ /// Gets the list of default value of the options.
+ ///
+ public Func DefaultValue { get; }
+
+ ///
+ /// Gets or sets the event that is invoked when the value of an option changes.
+ ///
+ public Action? ChangedEvent { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The options' title.
+ /// The option list's length.
+ /// The default values.
+ /// Whether to include the options in the preset.
+ /// Whether the first option's title index is 0, else 1.
+ protected ModdedOptionList(string title, int count, Func defaultValues, bool includeInPreset = true, bool zeroIndexTitle = false)
+ : base(title, count, includeInPreset, zeroIndexTitle)
+ {
+ DefaultValue = defaultValues;
+ Values = Enumerable.Range(0, Count).Select(defaultValues).ToArray();
+ }
+
+ internal void ValueChanged(int idx, OptionBehaviour optionBehaviour)
+ {
+ SetValue(idx, GetValueFromOptionBehaviour(idx, optionBehaviour));
+ }
+
+ ///
+ /// Sets the value of the option.
+ ///
+ /// The option's index.
+ /// The new value.
+ /// Whether to send the value to other players.
+ public void SetValue(int idx, T newValue, bool sendRpc = true)
+ {
+ var oldVal = Values[idx];
+ Values[idx] = newValue;
+
+ if (Values[idx]?.Equals(oldVal) == false)
+ {
+ ChangedEvent?.Invoke(idx, Values[idx]);
+ }
+
+ if (sendRpc && AmongUsClient.Instance.AmHost)
+ {
+ if (ParentMod?.GetConfigFile().TryGetEntry(ConfigDefinitions[idx], out var entry) == true)
+ {
+ entry.Value = Values[idx];
+ }
+
+ Rpc.Instance.Send(PlayerControl.LocalPlayer, [GetNetData(idx)], true); // This might not work
+ }
+
+ OnValueChanged(idx, newValue);
+ }
+
+ ///
+ public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false)
+ {
+ if (ConfigDefinitions.Any(d => d is null))
+ {
+ Error($"Attempted to save {Title} to preset, but some ConfigDefinitions are null.");
+ return;
+ }
+ Bind(presetConfig);
+ for (int i = 0; i < Count; i++)
+ {
+ presetConfig[ConfigDefinitions[i]].BoxedValue = saveDefault ? DefaultValue(i) : Values[i];
+ }
+ }
+
+ ///
+ public override void Bind(ConfigFile config)
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ config.Bind(ConfigDefinitions[i], DefaultValue(i));
+ }
+ }
+
+ ///
+ public override void LoadFromPreset(ConfigFile presetConfig)
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ if (presetConfig.TryGetEntry(ConfigDefinitions[i], out ConfigEntry entry))
+ {
+ SetValue(i, entry.Value, false);
+ }
+ else
+ {
+ Error($"Attempted to load {Title} from preset, but ConfigDefinition {i} is not found in preset.");
+ }
+ }
+ }
+
+ ///
+ /// Handles the value changed event.
+ ///
+ /// The option's index.
+ /// The new value.
+ protected abstract void OnValueChanged(int idx, T newValue);
+
+ ///
+ /// Gets the value from the option behaviour.
+ ///
+ /// The option's index.
+ /// The OptionBehaviour.
+ /// The value.
+ public abstract T GetValueFromOptionBehaviour(int idx, OptionBehaviour optionBehaviour);
+
+ ///
+ /// Indexes the option for its value of type .
+ ///
+ /// The option's index.
+ /// value of type .
+ public T this[int idx] => Values[idx];
+}
From f9a33ce10996b7c638ee3be2b3658fee620c4c3d Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 22:32:16 -0600
Subject: [PATCH 010/276] Created concrete class for nested IModdedOptions that
aren't the concrete class
---
.../OptionTypes/ModdedOptionList.cs | 98 +++++++++++++++++++
1 file changed, 98 insertions(+)
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index 4c61485..927bcb2 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -263,3 +263,101 @@ public abstract class ModdedOptionList : ModdedOptionList
/// value of type .
public T this[int idx] => Values[idx];
}
+
+///
+/// Represents a modded option list.
+///
+/// The option's type.
+public class ModdedOptionsList : ModdedOptionList where T : IModdedOption
+{
+ protected override void OnParentModChange()
+ {
+ foreach (var option in Options)
+ {
+ option.ParentMod = _parentMod!;
+ }
+ }
+
+ ///
+ /// Gets the list of options.
+ ///
+ public T[] Options { get; }
+
+ ///
+ /// Gets the default option from its index.
+ ///
+ public Func DefaultOption { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The options' title.
+ /// The option list's length.
+ /// The default option.
+ /// Whether to include the options in the preset.
+ /// Whether the first option's title index is 0, else 1.
+ protected ModdedOptionsList(string title, int count, Func defaultOption, bool includeInPreset = true, bool zeroIndexTitle = false)
+ : base(title, count, includeInPreset, zeroIndexTitle)
+ {
+ DefaultOption = defaultOption;
+ Options = Enumerable.Range(0, Count).Select(defaultOption).ToArray();
+ }
+
+ ///
+ public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false)
+ {
+ foreach (var option in Options)
+ {
+ option.SaveToPreset(presetConfig, saveDefault);
+ }
+ }
+
+ ///
+ public override void Bind(ConfigFile config)
+ {
+ foreach (var option in Options)
+ {
+ option.Bind(config);
+ }
+ }
+
+ ///
+ public override void LoadFromPreset(ConfigFile presetConfig)
+ {
+ foreach (var option in Options)
+ {
+ option.LoadFromPreset(presetConfig);
+ }
+ }
+
+ ///
+ public override float GetFloatData(int idx)
+ {
+ return Options[idx].GetFloatData();
+ }
+
+ ///
+ public override NetData GetNetData(int idx)
+ {
+ return Options[idx].GetNetData();
+ }
+
+ ///
+ public override void HandleNetData(int idx, byte[] data)
+ {
+ Options[idx].HandleNetData(data);
+ }
+
+ ///
+ public override OptionBehaviour CreateOption(int idx, ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
+ {
+ return _optionBehaviours[idx] = Options[idx].CreateOption(toggleOpt, numberOpt, stringOpt, playerOpt, container);
+ }
+
+ ///
+ /// Indexes the option of type .
+ ///
+ /// The option's index.
+ /// option of type .
+ public T this[int idx] => Options[idx];
+}
From b0be07192e024faa9a9071bc9c196f2a61ba9767 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 23:12:08 -0600
Subject: [PATCH 011/276] Removed middleman classes and fields that can be
handled much easier
---
MiraAPI/GameOptions/IModdedOptionList.cs | 49 +--
.../OptionTypes/ModdedOptionList.cs | 316 ++----------------
2 files changed, 39 insertions(+), 326 deletions(-)
diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs
index da6da53..26f9bee 100644
--- a/MiraAPI/GameOptions/IModdedOptionList.cs
+++ b/MiraAPI/GameOptions/IModdedOptionList.cs
@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using BepInEx.Configuration;
-using MiraAPI.Networking;
using MiraAPI.PluginLoading;
-using UnityEngine;
namespace MiraAPI.GameOptions;
@@ -23,15 +21,9 @@ public interface IModdedOptionList
IReadOnlyList Ids { get; }
///
- /// Gets or sets the titles of the options.
- /// The end title will equal this value with its index appended (e.g., Title1).
+ /// Gets the titles of the options.
///
- string Title { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the first option's title has a 0, else starts with 1.
- ///
- bool ZeroIndexTitle { get; set; }
+ IReadOnlyList Titles { get; }
///
/// Gets the StringName for the options, used for localization.
@@ -64,42 +56,9 @@ public interface IModdedOptionList
bool IncludeInPreset { get; set; }
///
- /// Gets the array of ConfigDefinition for the options, used for BepInEx configuration.
+ /// Gets the set of ConfigDefinition for the options, used for BepInEx configuration.
///
- ConfigDefinition?[] ConfigDefinitions { get; }
-
- ///
- /// Creates the option behaviour for the modded option at index .
- ///
- /// The option's index.
- /// The ToggleOption template.
- /// The NumberOption template.
- /// The StringOption template.
- /// The PlayerOption template.
- /// >The Transform container for the option.
- /// The created OptionBehaviour object.
- OptionBehaviour CreateOption(int idx, ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container);
-
- ///
- /// Gets the value at index as a float.
- ///
- /// The option's index.
- /// The value of the option as a float.
- float GetFloatData(int idx);
-
- ///
- /// Gets the NetData for the option at index , used for network synchronization.
- ///
- /// The option's index.
- /// Returns the NetData object for the option.
- NetData GetNetData(int idx);
-
- ///
- /// Handles incoming network data for the option at index .
- ///
- /// The option's index.
- /// The byte array representing the network data.
- void HandleNetData(int idx, byte[] data);
+ IReadOnlyList ConfigDefinitions { get; }
///
/// Saves the options to a preset configuration file.
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index 927bcb2..aca5b28 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -1,43 +1,33 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BepInEx.Configuration;
-using MiraAPI.Networking;
using MiraAPI.PluginLoading;
-using Reactor.Localization.Utilities;
-using Reactor.Networking.Rpc;
-using Reactor.Utilities;
-using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes;
///
/// Represents a modded option list.
///
-public abstract class ModdedOptionList : IModdedOptionList
+/// The type of options.
+public class ModdedOptionList : IModdedOptionList where T : IModdedOption
{
- protected IMiraPlugin? _parentMod;
+ private IMiraPlugin? _parentMod;
///
public int Count { get; }
///
- public IReadOnlyList Ids { get; }
+ public IReadOnlyList Ids => FromOptions(o => o.Id);
///
- public string Title { get; set; }
-
- ///
- public bool ZeroIndexTitle { get; set; }
+ public IReadOnlyList Titles => FromOptions(o => o.Title);
///
- public IReadOnlyList StringNames { get; }
-
- protected readonly BaseGameSetting[] _data;
+ public IReadOnlyList StringNames => FromOptions(o => o.StringName);
///
- public IReadOnlyList Data => _data;
+ public IReadOnlyList Data => FromOptions(o => o.Data);
///
public IMiraPlugin? ParentMod
@@ -48,233 +38,10 @@ public abstract class ModdedOptionList : IModdedOptionList
if (_parentMod != null || value == null) return;
_parentMod = value;
- OnParentModChange();
- }
- }
-
- protected abstract void OnParentModChange();
-
- ///
- public Func Visible { get; set; }
-
- ///
- public bool IncludeInPreset { get; set; }
-
- protected readonly OptionBehaviour?[] _optionBehaviours;
-
- ///
- public IReadOnlyList OptionBehaviours => _optionBehaviours;
-
- ///
- public ConfigDefinition?[] ConfigDefinitions { get; set; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The options' title.
- /// The option list's length.
- /// Whether to include the options in the preset.
- /// Whether the first option's title index is 0, else 1.
- protected ModdedOptionList(string title, int count, bool includeInPreset = true, bool zeroIndexTitle = false)
- {
- Count = count;
- ZeroIndexTitle = zeroIndexTitle;
-
- var range = Enumerable.Range(0, Count);
- var titleOffset = ZeroIndexTitle ? 0 : 1;
-
- Ids = range.Select(_ => ModdedOptionsManager.NextId).ToList();
- Title = title;
- StringNames = range.Select(i => CustomStringName.CreateAndRegister($"{Title}{i + titleOffset}")).ToList();
- Visible = _ => true;
- IncludeInPreset = includeInPreset;
-
- _optionBehaviours = range.Select(_ => null).ToArray();
- ConfigDefinitions = range.Select(_ => null).ToArray();
- }
-
- ///
- public abstract void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
-
- ///
- public abstract void Bind(ConfigFile config);
-
- ///
- public abstract void LoadFromPreset(ConfigFile presetConfig);
-
- ///
- public abstract float GetFloatData(int idx);
-
- ///
- public abstract NetData GetNetData(int idx);
-
- ///
- public abstract void HandleNetData(int idx, byte[] data);
-
- ///
- public abstract OptionBehaviour CreateOption(
- int idx,
- ToggleOption toggleOpt,
- NumberOption numberOpt,
- StringOption stringOpt,
- PlayerOption playerOpt,
- Transform container);
-}
-
-///
-/// Represents a modded option list.
-///
-/// The value type.
-public abstract class ModdedOptionList : ModdedOptionList
-{
- protected override void OnParentModChange()
- {
- var configFile = _parentMod!.GetConfigFile();
- for (int i = 0; i < Count; i++)
- {
- var entry = configFile.Bind(ConfigDefinitions[i], DefaultValue(i));
- Values[i] = entry.Value;
- }
- }
-
- ///
- /// Gets the list of values of the options.
- ///
- public T[] Values { get; }
-
- ///
- /// Gets the list of default value of the options.
- ///
- public Func DefaultValue { get; }
-
- ///
- /// Gets or sets the event that is invoked when the value of an option changes.
- ///
- public Action? ChangedEvent { get; set; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The options' title.
- /// The option list's length.
- /// The default values.
- /// Whether to include the options in the preset.
- /// Whether the first option's title index is 0, else 1.
- protected ModdedOptionList(string title, int count, Func defaultValues, bool includeInPreset = true, bool zeroIndexTitle = false)
- : base(title, count, includeInPreset, zeroIndexTitle)
- {
- DefaultValue = defaultValues;
- Values = Enumerable.Range(0, Count).Select(defaultValues).ToArray();
- }
-
- internal void ValueChanged(int idx, OptionBehaviour optionBehaviour)
- {
- SetValue(idx, GetValueFromOptionBehaviour(idx, optionBehaviour));
- }
-
- ///
- /// Sets the value of the option.
- ///
- /// The option's index.
- /// The new value.
- /// Whether to send the value to other players.
- public void SetValue(int idx, T newValue, bool sendRpc = true)
- {
- var oldVal = Values[idx];
- Values[idx] = newValue;
-
- if (Values[idx]?.Equals(oldVal) == false)
- {
- ChangedEvent?.Invoke(idx, Values[idx]);
- }
-
- if (sendRpc && AmongUsClient.Instance.AmHost)
- {
- if (ParentMod?.GetConfigFile().TryGetEntry(ConfigDefinitions[idx], out var entry) == true)
+ foreach (var option in Options)
{
- entry.Value = Values[idx];
+ option.ParentMod = _parentMod!;
}
-
- Rpc.Instance.Send(PlayerControl.LocalPlayer, [GetNetData(idx)], true); // This might not work
- }
-
- OnValueChanged(idx, newValue);
- }
-
- ///
- public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false)
- {
- if (ConfigDefinitions.Any(d => d is null))
- {
- Error($"Attempted to save {Title} to preset, but some ConfigDefinitions are null.");
- return;
- }
- Bind(presetConfig);
- for (int i = 0; i < Count; i++)
- {
- presetConfig[ConfigDefinitions[i]].BoxedValue = saveDefault ? DefaultValue(i) : Values[i];
- }
- }
-
- ///
- public override void Bind(ConfigFile config)
- {
- for (int i = 0; i < Count; i++)
- {
- config.Bind(ConfigDefinitions[i], DefaultValue(i));
- }
- }
-
- ///
- public override void LoadFromPreset(ConfigFile presetConfig)
- {
- for (int i = 0; i < Count; i++)
- {
- if (presetConfig.TryGetEntry(ConfigDefinitions[i], out ConfigEntry entry))
- {
- SetValue(i, entry.Value, false);
- }
- else
- {
- Error($"Attempted to load {Title} from preset, but ConfigDefinition {i} is not found in preset.");
- }
- }
- }
-
- ///
- /// Handles the value changed event.
- ///
- /// The option's index.
- /// The new value.
- protected abstract void OnValueChanged(int idx, T newValue);
-
- ///
- /// Gets the value from the option behaviour.
- ///
- /// The option's index.
- /// The OptionBehaviour.
- /// The value.
- public abstract T GetValueFromOptionBehaviour(int idx, OptionBehaviour optionBehaviour);
-
- ///
- /// Indexes the option for its value of type .
- ///
- /// The option's index.
- /// value of type .
- public T this[int idx] => Values[idx];
-}
-
-///
-/// Represents a modded option list.
-///
-/// The option's type.
-public class ModdedOptionsList : ModdedOptionList where T : IModdedOption
-{
- protected override void OnParentModChange()
- {
- foreach (var option in Options)
- {
- option.ParentMod = _parentMod!;
}
}
@@ -283,28 +50,39 @@ public class ModdedOptionsList : ModdedOptionList where T : IModdedOption
///
public T[] Options { get; }
- ///
- /// Gets the default option from its index.
- ///
- public Func DefaultOption { get; }
+ ///
+ public Func Visible { get; set; }
+
+ ///
+ public bool IncludeInPreset { get; set; }
+
+ ///
+ public IReadOnlyList OptionBehaviours => FromOptions(o => o.OptionBehaviour);
+
+ ///
+ public IReadOnlyList ConfigDefinitions => FromOptions(o => o.ConfigDefinition);
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// The options' title.
/// The option list's length.
- /// The default option.
+ /// The option factory to instantiate the options from.
/// Whether to include the options in the preset.
- /// Whether the first option's title index is 0, else 1.
- protected ModdedOptionsList(string title, int count, Func defaultOption, bool includeInPreset = true, bool zeroIndexTitle = false)
- : base(title, count, includeInPreset, zeroIndexTitle)
+ public ModdedOptionList(int count, Func optionFactory, bool includeInPreset = true)
{
- DefaultOption = defaultOption;
- Options = Enumerable.Range(0, Count).Select(defaultOption).ToArray();
+ Count = count;
+ Visible = _ => true;
+ IncludeInPreset = includeInPreset;
+ Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray();
+ }
+
+ internal IReadOnlyList FromOptions(Func getter)
+ {
+ return Options.Select(getter).ToArray();
}
///
- public override void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false)
+ public void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false)
{
foreach (var option in Options)
{
@@ -313,7 +91,7 @@ public class ModdedOptionsList : ModdedOptionList where T : IModdedOption
}
///
- public override void Bind(ConfigFile config)
+ public void Bind(ConfigFile config)
{
foreach (var option in Options)
{
@@ -322,7 +100,7 @@ public class ModdedOptionsList : ModdedOptionList where T : IModdedOption
}
///
- public override void LoadFromPreset(ConfigFile presetConfig)
+ public void LoadFromPreset(ConfigFile presetConfig)
{
foreach (var option in Options)
{
@@ -330,30 +108,6 @@ public class ModdedOptionsList : ModdedOptionList where T : IModdedOption
}
}
- ///
- public override float GetFloatData(int idx)
- {
- return Options[idx].GetFloatData();
- }
-
- ///
- public override NetData GetNetData(int idx)
- {
- return Options[idx].GetNetData();
- }
-
- ///
- public override void HandleNetData(int idx, byte[] data)
- {
- Options[idx].HandleNetData(data);
- }
-
- ///
- public override OptionBehaviour CreateOption(int idx, ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
- {
- return _optionBehaviours[idx] = Options[idx].CreateOption(toggleOpt, numberOpt, stringOpt, playerOpt, container);
- }
-
///
/// Indexes the option of type .
///
From be1d77bc6caef69d217e5578450fc1246a85e56b Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 23:13:10 -0600
Subject: [PATCH 012/276] Removed readonly properties that can be more easily
replicated with the indexer
---
MiraAPI/GameOptions/IModdedOptionList.cs | 31 -------------------
.../OptionTypes/ModdedOptionList.cs | 23 --------------
2 files changed, 54 deletions(-)
diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs
index 26f9bee..4fed3b3 100644
--- a/MiraAPI/GameOptions/IModdedOptionList.cs
+++ b/MiraAPI/GameOptions/IModdedOptionList.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using BepInEx.Configuration;
using MiraAPI.PluginLoading;
@@ -15,36 +14,11 @@ public interface IModdedOptionList
///
int Count { get; }
- ///
- /// Gets the unique identifiers for the options.
- ///
- IReadOnlyList Ids { get; }
-
- ///
- /// Gets the titles of the options.
- ///
- IReadOnlyList Titles { get; }
-
- ///
- /// Gets the StringName for the options, used for localization.
- ///
- IReadOnlyList StringNames { get; }
-
///
/// Gets or sets the MiraPlugin that created these options.
///
IMiraPlugin? ParentMod { get; set; }
- ///
- /// Gets the game setting data for the options.
- ///
- IReadOnlyList Data { get; }
-
- ///
- /// Gets the OptionBehaviour object of the options.
- ///
- IReadOnlyList OptionBehaviours { get; }
-
///
/// Gets or sets the visibility function for the options.
///
@@ -55,11 +29,6 @@ public interface IModdedOptionList
///
bool IncludeInPreset { get; set; }
- ///
- /// Gets the set of ConfigDefinition for the options, used for BepInEx configuration.
- ///
- IReadOnlyList ConfigDefinitions { get; }
-
///
/// Saves the options to a preset configuration file.
///
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index aca5b28..7ff88bf 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -17,18 +17,6 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption
///
public int Count { get; }
- ///
- public IReadOnlyList Ids => FromOptions(o => o.Id);
-
- ///
- public IReadOnlyList Titles => FromOptions(o => o.Title);
-
- ///
- public IReadOnlyList StringNames => FromOptions(o => o.StringName);
-
- ///
- public IReadOnlyList Data => FromOptions(o => o.Data);
-
///
public IMiraPlugin? ParentMod
{
@@ -56,12 +44,6 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption
///
public bool IncludeInPreset { get; set; }
- ///
- public IReadOnlyList OptionBehaviours => FromOptions(o => o.OptionBehaviour);
-
- ///
- public IReadOnlyList ConfigDefinitions => FromOptions(o => o.ConfigDefinition);
-
///
/// Initializes a new instance of the class.
///
@@ -76,11 +58,6 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption
Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray();
}
- internal IReadOnlyList FromOptions(Func getter)
- {
- return Options.Select(getter).ToArray();
- }
-
///
public void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false)
{
From beb7443c9a974fdb11039b28bd26a1fa30d2eb64 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 23:16:38 -0600
Subject: [PATCH 013/276] Extended from IReadOnlyList for potential easier
iteration
---
.../GameOptions/OptionTypes/ModdedOptionList.cs | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index 7ff88bf..cc7bdfd 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BepInEx.Configuration;
@@ -10,7 +11,7 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// Represents a modded option list.
///
/// The type of options.
-public class ModdedOptionList : IModdedOptionList where T : IModdedOption
+public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : IModdedOption
{
private IMiraPlugin? _parentMod;
@@ -36,7 +37,7 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption
///
/// Gets the list of options.
///
- public T[] Options { get; }
+ public IReadOnlyList Options { get; }
///
public Func Visible { get; set; }
@@ -85,6 +86,17 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption
}
}
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ return ((IEnumerable)Options).GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return Options.GetEnumerator();
+ }
+
///
/// Indexes the option of type .
///
From f9f7b7e667fc700462bdc4a0b794efb7adcf18e1 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 19 Jun 2026 23:30:58 -0600
Subject: [PATCH 014/276] Expanded setters to set the inner options values as
well
---
.../OptionTypes/ModdedOptionList.cs | 36 ++++++++++++++-----
1 file changed, 28 insertions(+), 8 deletions(-)
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index cc7bdfd..a32a727 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -13,23 +13,21 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// The type of options.
public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : IModdedOption
{
- private IMiraPlugin? _parentMod;
-
///
public int Count { get; }
///
public IMiraPlugin? ParentMod
{
- get => _parentMod;
+ get;
set
{
- if (_parentMod != null || value == null) return;
- _parentMod = value;
+ if (field != null || value == null) return;
+ field = value;
foreach (var option in Options)
{
- option.ParentMod = _parentMod!;
+ option.ParentMod = value;
}
}
}
@@ -40,10 +38,32 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T :
public IReadOnlyList Options { get; }
///
- public Func Visible { get; set; }
+ public Func Visible
+ {
+ get;
+ set
+ {
+ field = value;
+ foreach (var (option, idx) in Options.Select((o, i) => (o, i)))
+ {
+ option.Visible = () => value(idx);
+ }
+ }
+ }
///
- public bool IncludeInPreset { get; set; }
+ public bool IncludeInPreset
+ {
+ get;
+ set
+ {
+ field = value;
+ foreach (var option in Options)
+ {
+ option.IncludeInPreset = value;
+ }
+ }
+ }
///
/// Initializes a new instance of the class.
From 007c2020587549b94b45901765610d9e89f43a2b Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sat, 20 Jun 2026 00:52:07 -0600
Subject: [PATCH 015/276] Changed properties to init instead of set to allow
using object initializer but not changing the values later
---
MiraAPI/GameOptions/IModdedOptionList.cs | 10 ++++++----
.../GameOptions/OptionTypes/ModdedOptionList.cs | 17 +++++++++--------
2 files changed, 15 insertions(+), 12 deletions(-)
diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs
index 4fed3b3..62dd7d5 100644
--- a/MiraAPI/GameOptions/IModdedOptionList.cs
+++ b/MiraAPI/GameOptions/IModdedOptionList.cs
@@ -20,14 +20,16 @@ public interface IModdedOptionList
IMiraPlugin? ParentMod { get; set; }
///
- /// Gets or sets the visibility function for the options.
+ /// Gets the visibility function for the options.
+ /// Leaving it null will leave the options unchanged.
///
- Func Visible { get; set; }
+ Func? Visible { get; init; }
///
- /// Gets or sets a value indicating whether the options should be included with presets.
+ /// Gets a value indicating whether the options should be included with presets.
+ /// Leaving it null will leave the options unchanged.
///
- bool IncludeInPreset { get; set; }
+ bool? IncludeInPreset { get; init; }
///
/// Saves the options to a preset configuration file.
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index a32a727..7e97a1a 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -38,12 +38,13 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T :
public IReadOnlyList Options { get; }
///
- public Func Visible
+ public Func? Visible
{
get;
- set
+ init
{
field = value;
+ if (value == null) return;
foreach (var (option, idx) in Options.Select((o, i) => (o, i)))
{
option.Visible = () => value(idx);
@@ -52,15 +53,16 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T :
}
///
- public bool IncludeInPreset
+ public bool? IncludeInPreset
{
get;
- set
+ init
{
field = value;
+ if (value == null) return;
foreach (var option in Options)
{
- option.IncludeInPreset = value;
+ option.IncludeInPreset = value.Value;
}
}
}
@@ -71,12 +73,11 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T :
/// The option list's length.
/// The option factory to instantiate the options from.
/// Whether to include the options in the preset.
- public ModdedOptionList(int count, Func optionFactory, bool includeInPreset = true)
+ public ModdedOptionList(int count, Func optionFactory, bool? includeInPreset = true)
{
Count = count;
- Visible = _ => true;
- IncludeInPreset = includeInPreset;
Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray();
+ IncludeInPreset = includeInPreset;
}
///
From 93d5ebf15a38a368fbcdb1db668587180bcf24b7 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sat, 20 Jun 2026 01:20:08 -0600
Subject: [PATCH 016/276] Removed Preset fields from interface that won't be
used
---
MiraAPI/GameOptions/IModdedOption.cs | 28 ++++++++++
MiraAPI/GameOptions/IModdedOptionList.cs | 52 -------------------
.../OptionTypes/ModdedOptionList.cs | 28 ----------
3 files changed, 28 insertions(+), 80 deletions(-)
delete mode 100644 MiraAPI/GameOptions/IModdedOptionList.cs
diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs
index 5a3c3f8..824a832 100644
--- a/MiraAPI/GameOptions/IModdedOption.cs
+++ b/MiraAPI/GameOptions/IModdedOption.cs
@@ -104,3 +104,31 @@ public interface IModdedOption
/// The ConfigFile representing the preset configuration.
void LoadFromPreset(ConfigFile presetConfig);
}
+
+///
+/// Interface for list of modded options.
+///
+public interface IModdedOptionList
+{
+ ///
+ /// Gets or sets the MiraPlugin that created this list of options.
+ ///
+ IMiraPlugin? ParentMod { get; set; }
+
+ ///
+ /// Gets the number of options in the list.
+ ///
+ int Count { get; }
+
+ ///
+ /// Gets the visibility function for the options.
+ /// Leaving it null will leave the options unchanged.
+ ///
+ Func? Visible { get; init; }
+
+ ///
+ /// Gets a value indicating whether the options should be included with presets.
+ /// Leaving it null will leave the options unchanged.
+ ///
+ bool? IncludeInPreset { get; init; }
+}
diff --git a/MiraAPI/GameOptions/IModdedOptionList.cs b/MiraAPI/GameOptions/IModdedOptionList.cs
deleted file mode 100644
index 62dd7d5..0000000
--- a/MiraAPI/GameOptions/IModdedOptionList.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using BepInEx.Configuration;
-using MiraAPI.PluginLoading;
-
-namespace MiraAPI.GameOptions;
-
-///
-/// Interface for list of modded options.
-///
-public interface IModdedOptionList
-{
- ///
- /// Gets the number of options in the list.
- ///
- int Count { get; }
-
- ///
- /// Gets or sets the MiraPlugin that created these options.
- ///
- IMiraPlugin? ParentMod { get; set; }
-
- ///
- /// Gets the visibility function for the options.
- /// Leaving it null will leave the options unchanged.
- ///
- Func? Visible { get; init; }
-
- ///
- /// Gets a value indicating whether the options should be included with presets.
- /// Leaving it null will leave the options unchanged.
- ///
- bool? IncludeInPreset { get; init; }
-
- ///
- /// Saves the options to a preset configuration file.
- ///
- /// The ConfigFile representing the preset configuration.
- /// Indicates whether to save the default value instead of the current value.
- void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
-
- ///
- /// Binds the options to a configuration file.
- ///
- /// The ConfigFile to bind the option to.
- void Bind(ConfigFile config);
-
- ///
- /// Loads the options from a preset configuration file, applying the values to the options' configuration.
- ///
- /// The ConfigFile representing the preset configuration.
- void LoadFromPreset(ConfigFile presetConfig);
-}
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index 7e97a1a..12f725e 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -2,7 +2,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
-using BepInEx.Configuration;
using MiraAPI.PluginLoading;
namespace MiraAPI.GameOptions.OptionTypes;
@@ -80,33 +79,6 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T :
IncludeInPreset = includeInPreset;
}
- ///
- public void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false)
- {
- foreach (var option in Options)
- {
- option.SaveToPreset(presetConfig, saveDefault);
- }
- }
-
- ///
- public void Bind(ConfigFile config)
- {
- foreach (var option in Options)
- {
- option.Bind(config);
- }
- }
-
- ///
- public void LoadFromPreset(ConfigFile presetConfig)
- {
- foreach (var option in Options)
- {
- option.LoadFromPreset(presetConfig);
- }
- }
-
///
public IEnumerator GetEnumerator()
{
From ad6ca6fa3ee1006fcb73b643220587e0c116c315 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sat, 20 Jun 2026 01:22:44 -0600
Subject: [PATCH 017/276] Simplified property setters and getter
---
MiraAPI/GameOptions/OptionTypes/ModdedOption.cs | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
index 206894d..644c87c 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
@@ -15,8 +15,6 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// The value type.
public abstract class ModdedOption : IModdedOption
{
- private IMiraPlugin? _parentMod;
-
///
public uint Id { get; }
@@ -32,13 +30,13 @@ public abstract class ModdedOption : IModdedOption
///
public IMiraPlugin? ParentMod
{
- get => _parentMod;
+ get;
set
{
- if (_parentMod != null || value == null) return;
- _parentMod = value;
+ if (field != null || value == null) return;
+ field = value;
- var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
+ var entry = field.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
Value = entry.Value;
}
}
@@ -54,9 +52,9 @@ public abstract class ModdedOption : IModdedOption
public T DefaultValue { get; }
///
- /// Gets or sets the event that is invoked when the value of the option changes.
+ /// Gets the event that is invoked when the value of the option changes.
///
- public Action? ChangedEvent { get; set; }
+ public Action? ChangedEvent { get; init; }
///
public Func Visible { get; set; }
From 6a899ba27d335f47bc386e05b4fb9d43bcd3cd97 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sat, 20 Jun 2026 01:29:56 -0600
Subject: [PATCH 018/276] Removed properties that aren't required
---
MiraAPI/GameOptions/IModdedOption.cs | 12 -------
.../OptionTypes/ModdedOptionList.cs | 34 +------------------
2 files changed, 1 insertion(+), 45 deletions(-)
diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs
index 824a832..8d3e751 100644
--- a/MiraAPI/GameOptions/IModdedOption.cs
+++ b/MiraAPI/GameOptions/IModdedOption.cs
@@ -119,16 +119,4 @@ public interface IModdedOptionList
/// Gets the number of options in the list.
///
int Count { get; }
-
- ///
- /// Gets the visibility function for the options.
- /// Leaving it null will leave the options unchanged.
- ///
- Func? Visible { get; init; }
-
- ///
- /// Gets a value indicating whether the options should be included with presets.
- /// Leaving it null will leave the options unchanged.
- ///
- bool? IncludeInPreset { get; init; }
}
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index 12f725e..85f650a 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -36,47 +36,15 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T :
///
public IReadOnlyList Options { get; }
- ///
- public Func? Visible
- {
- get;
- init
- {
- field = value;
- if (value == null) return;
- foreach (var (option, idx) in Options.Select((o, i) => (o, i)))
- {
- option.Visible = () => value(idx);
- }
- }
- }
-
- ///
- public bool? IncludeInPreset
- {
- get;
- init
- {
- field = value;
- if (value == null) return;
- foreach (var option in Options)
- {
- option.IncludeInPreset = value.Value;
- }
- }
- }
-
///
/// Initializes a new instance of the class.
///
/// The option list's length.
/// The option factory to instantiate the options from.
- /// Whether to include the options in the preset.
- public ModdedOptionList(int count, Func optionFactory, bool? includeInPreset = true)
+ public ModdedOptionList(int count, Func optionFactory)
{
Count = count;
Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray();
- IncludeInPreset = includeInPreset;
}
///
From e115317ad4cbb49b5e4bdbb8985ec6151e1cf575 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sat, 20 Jun 2026 01:53:23 -0600
Subject: [PATCH 019/276] Removed generic from interface for easier manager
registering alongside property
---
MiraAPI/GameOptions/IModdedOption.cs | 12 ++-------
.../OptionTypes/ModdedOptionList.cs | 25 ++++---------------
2 files changed, 7 insertions(+), 30 deletions(-)
diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs
index 8d3e751..e21c3b2 100644
--- a/MiraAPI/GameOptions/IModdedOption.cs
+++ b/MiraAPI/GameOptions/IModdedOption.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using BepInEx.Configuration;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
@@ -108,15 +109,6 @@ public interface IModdedOption
///
/// Interface for list of modded options.
///
-public interface IModdedOptionList
+public interface IModdedOptionList : IReadOnlyList
{
- ///
- /// Gets or sets the MiraPlugin that created this list of options.
- ///
- IMiraPlugin? ParentMod { get; set; }
-
- ///
- /// Gets the number of options in the list.
- ///
- int Count { get; }
}
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index 85f650a..13609d9 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -2,7 +2,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
-using MiraAPI.PluginLoading;
namespace MiraAPI.GameOptions.OptionTypes;
@@ -10,27 +9,11 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// Represents a modded option list.
///
/// The type of options.
-public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T : IModdedOption
+public class ModdedOptionList : IModdedOptionList where T : IModdedOption
{
///
public int Count { get; }
- ///
- public IMiraPlugin? ParentMod
- {
- get;
- set
- {
- if (field != null || value == null) return;
- field = value;
-
- foreach (var option in Options)
- {
- option.ParentMod = value;
- }
- }
- }
-
///
/// Gets the list of options.
///
@@ -48,9 +31,9 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T :
}
///
- public IEnumerator GetEnumerator()
+ public IEnumerator GetEnumerator()
{
- return ((IEnumerable)Options).GetEnumerator();
+ return ((IEnumerable)Options).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
@@ -64,4 +47,6 @@ public class ModdedOptionList : IModdedOptionList, IReadOnlyList where T :
/// The option's index.
/// option of type .
public T this[int idx] => Options[idx];
+
+ IModdedOption IReadOnlyList.this[int index] => this[index];
}
From bea37d815a26dc53e62279850dc38d75111d3bb5 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sat, 20 Jun 2026 02:06:25 -0600
Subject: [PATCH 020/276] Applied Modded option list into options manager
---
MiraAPI/GameOptions/ModdedOptionsManager.cs | 20 ++++++++++++++++++++
MiraAPI/PluginLoading/MiraPluginManager.cs | 8 ++++++--
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 3b85a73..8fc1b1a 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -113,6 +113,26 @@ public static class ModdedOptionsManager
RegisterOption(option, group, property.Name, pluginInfo);
}
+ internal static void RegisterPropertyOptionList(Type type, PropertyInfo property, MiraPluginInfo pluginInfo)
+ {
+ if (!TypeToGroup.TryGetValue(type, out var group))
+ {
+ Error($"Failed to get group for {type.Name}");
+ return;
+ }
+
+ if (property.GetValue(group) is not IModdedOptionList optionList)
+ {
+ Error($"Failed to get option list for {property.Name}");
+ return;
+ }
+
+ for (int i = 0; i < optionList.Count; i++)
+ {
+ RegisterOption(optionList[i], group, property.Name + i, pluginInfo);
+ }
+ }
+
internal static void RegisterOption(
IModdedOption option,
AbstractOptionGroup group,
diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs
index 26a881f..607b905 100644
--- a/MiraAPI/PluginLoading/MiraPluginManager.cs
+++ b/MiraAPI/PluginLoading/MiraPluginManager.cs
@@ -201,12 +201,16 @@ public sealed class MiraPluginManager
}
var attribute = property.GetCustomAttribute();
- if (attribute == null)
+ if (attribute != null)
{
+ ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo);
continue;
}
- ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo);
+ if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList)))
+ {
+ ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo);
+ }
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption))))
From b5e35e2533f52504152c59ae4825905a6a4929f2 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 12:38:38 -0600
Subject: [PATCH 021/276] Added constructor overload
---
MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
index 13609d9..5a764b2 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs
@@ -30,6 +30,16 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption
Options = Enumerable.Range(0, Count).Select(optionFactory).ToArray();
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The options list.
+ public ModdedOptionList(IEnumerable options)
+ {
+ Count = options.Count();
+ Options = options.ToArray();
+ }
+
///
public IEnumerator GetEnumerator()
{
From 39a43f5395e44a8f146d68a5e06095909ef8eb55 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 12:57:16 -0600
Subject: [PATCH 022/276] Removed unnecessary private get
---
MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs
index 702d75e..e2014fb 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs
@@ -16,12 +16,12 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null)
///
/// Gets the title of the option.
///
- public string Title { get; private set; } = title;
+ public string Title => title;
///
/// Gets the role type of the option.
///
- protected Type? RoleType { get; private set; } = roleType;
+ protected Type? RoleType => roleType;
///
/// Sets the value of the option.
From 604d2f375d2ee1511c2538b7f5e2e89d5e172252 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 13:54:08 -0600
Subject: [PATCH 023/276] Added interface to error logger check
---
MiraAPI/PluginLoading/MiraPluginManager.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs
index 607b905..443d206 100644
--- a/MiraAPI/PluginLoading/MiraPluginManager.cs
+++ b/MiraAPI/PluginLoading/MiraPluginManager.cs
@@ -213,7 +213,8 @@ public sealed class MiraPluginManager
}
}
- foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption))))
+ foreach (var field in type.GetFields()
+ .Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption)) || f.FieldType.IsAssignableTo(typeof(IModdedOptionList))))
{
Error($"{field.Name} is a field, not a property. Use properties for options.");
}
From 11fa9581fac0d00844c5598bc3324c6fc29d9ab9 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 13:55:29 -0600
Subject: [PATCH 024/276] Added abstract option list attribute
---
.../Attributes/ModdedOptionListAttribute.cs | 47 +++++++++++
MiraAPI/GameOptions/ModdedOptionsManager.cs | 80 ++++++++++++++++++-
MiraAPI/PluginLoading/MiraPluginManager.cs | 6 ++
3 files changed, 131 insertions(+), 2 deletions(-)
create mode 100644 MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
new file mode 100644
index 0000000..116dd58
--- /dev/null
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Reflection;
+
+namespace MiraAPI.GameOptions.Attributes;
+
+///
+/// Represents an attribute that is used to define a list of modded options.
+///
+/// A function to title of the options.
+[AttributeUsage(AttributeTargets.Property)]
+public abstract class ModdedOptionListAttribute(Func titler) : Attribute
+{
+ internal IModdedOptionList? HolderOptionList { get; set; }
+
+ ///
+ /// Gets the function to title of the options.
+ ///
+ public Func Titler => titler;
+
+ ///
+ /// Sets the value of all the options.
+ ///
+ /// The new values as an object.
+ public abstract void SetValue(object value);
+
+ ///
+ /// Sets the value of the specific option.
+ ///
+ /// The option to set.
+ /// The new value as an object.
+ public abstract void SetValue(IModdedOption modOpt, object value);
+
+ ///
+ /// Gets the value of all the options.
+ ///
+ /// The value of the options as an object.
+ public abstract object GetValue();
+
+ ///
+ /// Gets the value of the specific option.
+ ///
+ /// The option to set.
+ /// The value of the option as an object.
+ public abstract object GetValue(IModdedOption modOpt);
+
+ internal abstract IModdedOptionList? CreateOptionList(object? value, PropertyInfo property);
+}
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 8fc1b1a..3b6708f 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -21,6 +21,7 @@ namespace MiraAPI.GameOptions;
public static class ModdedOptionsManager
{
private static readonly Dictionary OptionAttributes = [];
+ private static readonly Dictionary OptionListAttributes = [];
private static readonly Dictionary TypeToGroup = [];
internal static readonly Dictionary CreatedPlayerOptions = [];
@@ -100,11 +101,11 @@ public static class ModdedOptionsManager
}
var setterOriginal = property.GetSetMethod();
- var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
+ var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
var getterOriginal = property.GetGetMethod();
- var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
+ var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch));
PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
OptionAttributes.Add(property, attribute);
@@ -133,6 +134,51 @@ public static class ModdedOptionsManager
}
}
+ internal static void RegisterAttributeOptionList(
+ Type type,
+ ModdedOptionListAttribute attribute,
+ PropertyInfo property,
+ MiraPluginInfo pluginInfo)
+ {
+ if (OptionListAttributes.ContainsKey(property))
+ {
+ Error($"Property {property.Name} already has an attribute registered.");
+ return;
+ }
+
+ if (!TypeToGroup.TryGetValue(type, out var group))
+ {
+ Error($"Failed to get group for {type.Name}");
+ return;
+ }
+
+ var optionList = attribute.CreateOptionList(property.GetValue(group), property);
+
+ if (optionList == null)
+ {
+ Error($"Failed to get option for {property.Name}");
+ return;
+ }
+
+ var setterOriginal = property.GetSetMethod();
+ var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
+ PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
+
+ var getterOriginal = property.GetGetMethod();
+ var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
+ PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
+
+ // Add indexer patches
+
+ OptionListAttributes.Add(property, attribute);
+ attribute.HolderOptionList = optionList;
+
+ for (int i = 0; i < optionList.Count; i++)
+ {
+ RegisterOption(optionList[i], group, property.Name + i, pluginInfo);
+ }
+ }
+
internal static void RegisterOption(
IModdedOption option,
AbstractOptionGroup group,
@@ -222,4 +268,34 @@ public static class ModdedOptionsManager
__result = attribute.GetValue();
return false;
}
+
+ ///
+ /// Patches the setter of a list property to update the value of the options.
+ ///
+ /// The original setter method.
+ /// The new object value.
+#pragma warning disable CA1707
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
+ public static void PropertyListSetterPatch(MethodBase __originalMethod, object value)
+#pragma warning restore CA1707
+ {
+ var attribute = OptionListAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
+ attribute.SetValue(value);
+ }
+
+ ///
+ /// Patches the getter of a list property to return the value of the options.
+ ///
+ /// The original getter method.
+ /// The result of the property getter.
+ /// False so the original getter gets skipped.
+#pragma warning disable CA1707
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
+ public static bool PropertyListGetterPatch(MethodBase __originalMethod, ref object __result)
+#pragma warning restore CA1707
+ {
+ var attribute = OptionListAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
+ __result = attribute.GetValue();
+ return false;
+ }
}
diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs
index 443d206..bb4e6cc 100644
--- a/MiraAPI/PluginLoading/MiraPluginManager.cs
+++ b/MiraAPI/PluginLoading/MiraPluginManager.cs
@@ -211,6 +211,12 @@ public sealed class MiraPluginManager
{
ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo);
}
+
+ var listAttr = property.GetCustomAttribute();
+ if (listAttr != null)
+ {
+ ModdedOptionsManager.RegisterAttributeOptionList(type, listAttr, property, pluginInfo);
+ }
}
foreach (var field in type.GetFields()
From 887be871f0a55dd88e3d4a31e5cf0ce6096b4510 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 19:28:17 -0600
Subject: [PATCH 025/276] Changed abstract methods to handle casting and
calling abstract overload
---
.../Attributes/ModdedOptionListAttribute.cs | 30 +++++++++++++++++--
1 file changed, 28 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
index 116dd58..de927ab 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
@@ -12,6 +13,10 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr
{
internal IModdedOptionList? HolderOptionList { get; set; }
+ internal PropertyInfo? BaseProperty { get; set; }
+
+ internal AbstractOptionGroup? Group { get; set; }
+
///
/// Gets the function to title of the options.
///
@@ -21,7 +26,20 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr
/// Sets the value of all the options.
///
/// The new values as an object.
- public abstract void SetValue(object value);
+ public void SetValue(object value)
+ {
+ var list = (IList)value;
+ if (list.Count != ((IList)BaseProperty!.GetValue(Group)!).Count ||
+ list.Count != HolderOptionList!.Count)
+ {
+ throw new InvalidOperationException($"Value set to {BaseProperty!.Name} cannot change the list's length.");
+ }
+
+ for (int i = 0; i < list!.Count; i++)
+ {
+ SetValue(HolderOptionList![i], list[i]!);
+ }
+ }
///
/// Sets the value of the specific option.
@@ -34,7 +52,15 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr
/// Gets the value of all the options.
///
/// The value of the options as an object.
- public abstract object GetValue();
+ public object GetValue()
+ {
+ var list = (IList)BaseProperty!.GetValue(Group)!;
+ for (int i = 0; i < list!.Count; i++)
+ {
+ list[i] = GetValue(HolderOptionList![i]);
+ }
+ return list;
+ }
///
/// Gets the value of the specific option.
From e5658df8c551557c67a2c53455d76f3d6d453483 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 19:35:14 -0600
Subject: [PATCH 026/276] Created parent attribute class to share methods
---
.../Attributes/ModdedOptionAttribute.cs | 14 +------
.../Attributes/ModdedOptionListAttribute.cs | 6 +--
.../Attributes/PropertyOptionAttribute.cs | 22 ++++++++++
MiraAPI/GameOptions/ModdedOptionsManager.cs | 41 +++----------------
4 files changed, 31 insertions(+), 52 deletions(-)
create mode 100644 MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs
index e2014fb..62ce43b 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs
@@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.Attributes;
/// The option title.
/// Optional parameter to specify a role Type.
[AttributeUsage(AttributeTargets.Property)]
-public abstract class ModdedOptionAttribute(string title, Type? roleType = null) : Attribute
+public abstract class ModdedOptionAttribute(string title, Type? roleType = null) : PropertyOptionAttribute
{
internal IModdedOption? HolderOption { get; set; }
@@ -23,17 +23,5 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null)
///
protected Type? RoleType => roleType;
- ///
- /// Sets the value of the option.
- ///
- /// The new value as an object.
- public abstract void SetValue(object value);
-
- ///
- /// Gets the value of the option.
- ///
- /// The value of the option as an object.
- public abstract object GetValue();
-
internal abstract IModdedOption? CreateOption(object? value, PropertyInfo property);
}
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
index de927ab..bce0190 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
@@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.Attributes;
///
/// A function to title of the options.
[AttributeUsage(AttributeTargets.Property)]
-public abstract class ModdedOptionListAttribute(Func titler) : Attribute
+public abstract class ModdedOptionListAttribute(Func titler) : PropertyOptionAttribute
{
internal IModdedOptionList? HolderOptionList { get; set; }
@@ -26,7 +26,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr
/// Sets the value of all the options.
///
/// The new values as an object.
- public void SetValue(object value)
+ public override void SetValue(object value)
{
var list = (IList)value;
if (list.Count != ((IList)BaseProperty!.GetValue(Group)!).Count ||
@@ -52,7 +52,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Attr
/// Gets the value of all the options.
///
/// The value of the options as an object.
- public object GetValue()
+ public override object GetValue()
{
var list = (IList)BaseProperty!.GetValue(Group)!;
for (int i = 0; i < list!.Count; i++)
diff --git a/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs
new file mode 100644
index 0000000..643d7b7
--- /dev/null
+++ b/MiraAPI/GameOptions/Attributes/PropertyOptionAttribute.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace MiraAPI.GameOptions.Attributes;
+
+///
+/// Represents an attribute that is used to intercept a property's getter and setter.
+///
+[AttributeUsage(AttributeTargets.Property)]
+public abstract class PropertyOptionAttribute : Attribute
+{
+ ///
+ /// Sets the value of the option.
+ ///
+ /// The new value as an object.
+ public abstract void SetValue(object value);
+
+ ///
+ /// Gets the value of the option.
+ ///
+ /// The value of the option as an object.
+ public abstract object GetValue();
+}
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 3b6708f..00b726d 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -20,8 +20,7 @@ namespace MiraAPI.GameOptions;
///
public static class ModdedOptionsManager
{
- private static readonly Dictionary OptionAttributes = [];
- private static readonly Dictionary OptionListAttributes = [];
+ private static readonly Dictionary OptionAttributes = [];
private static readonly Dictionary TypeToGroup = [];
internal static readonly Dictionary CreatedPlayerOptions = [];
@@ -101,11 +100,11 @@ public static class ModdedOptionsManager
}
var setterOriginal = property.GetSetMethod();
- var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
+ var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
var getterOriginal = property.GetGetMethod();
- var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch));
+ var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
OptionAttributes.Add(property, attribute);
@@ -140,7 +139,7 @@ public static class ModdedOptionsManager
PropertyInfo property,
MiraPluginInfo pluginInfo)
{
- if (OptionListAttributes.ContainsKey(property))
+ if (OptionAttributes.ContainsKey(property))
{
Error($"Property {property.Name} already has an attribute registered.");
return;
@@ -170,7 +169,7 @@ public static class ModdedOptionsManager
// Add indexer patches
- OptionListAttributes.Add(property, attribute);
+ OptionAttributes.Add(property, attribute);
attribute.HolderOptionList = optionList;
for (int i = 0; i < optionList.Count; i++)
@@ -268,34 +267,4 @@ public static class ModdedOptionsManager
__result = attribute.GetValue();
return false;
}
-
- ///
- /// Patches the setter of a list property to update the value of the options.
- ///
- /// The original setter method.
- /// The new object value.
-#pragma warning disable CA1707
- [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
- public static void PropertyListSetterPatch(MethodBase __originalMethod, object value)
-#pragma warning restore CA1707
- {
- var attribute = OptionListAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
- attribute.SetValue(value);
- }
-
- ///
- /// Patches the getter of a list property to return the value of the options.
- ///
- /// The original getter method.
- /// The result of the property getter.
- /// False so the original getter gets skipped.
-#pragma warning disable CA1707
- [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
- public static bool PropertyListGetterPatch(MethodBase __originalMethod, ref object __result)
-#pragma warning restore CA1707
- {
- var attribute = OptionListAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
- __result = attribute.GetValue();
- return false;
- }
}
From 80b8af85cb0c542d03570b03a236470cda8d382c Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 19:59:49 -0600
Subject: [PATCH 027/276] Added IList cast and null check
---
MiraAPI/GameOptions/ModdedOptionsManager.cs | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 00b726d..1706719 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -151,7 +152,16 @@ public static class ModdedOptionsManager
return;
}
- var optionList = attribute.CreateOptionList(property.GetValue(group), property);
+ var propertyVal = property.GetValue(group);
+
+ if (propertyVal == null)
+ {
+ Error("Cannot initialize option list with null value.");
+ return;
+ }
+
+ var propertyList = (IList)propertyVal;
+ var optionList = attribute.CreateOptionList(propertyList, property);
if (optionList == null)
{
From 68fb1d2b58177cb5a676e4293eb719098482a186 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 20:16:11 -0600
Subject: [PATCH 028/276] Added patches to the list's getter and setter
---
.../Attributes/ModdedOptionListAttribute.cs | 10 +----
MiraAPI/GameOptions/ModdedOptionsManager.cs | 45 ++++++++++++++++++-
2 files changed, 46 insertions(+), 9 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
index bce0190..3e26432 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
@@ -29,8 +29,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
public override void SetValue(object value)
{
var list = (IList)value;
- if (list.Count != ((IList)BaseProperty!.GetValue(Group)!).Count ||
- list.Count != HolderOptionList!.Count)
+ if (list.Count != HolderOptionList!.Count)
{
throw new InvalidOperationException($"Value set to {BaseProperty!.Name} cannot change the list's length.");
}
@@ -54,12 +53,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
/// The value of the options as an object.
public override object GetValue()
{
- var list = (IList)BaseProperty!.GetValue(Group)!;
- for (int i = 0; i < list!.Count; i++)
- {
- list[i] = GetValue(HolderOptionList![i]);
- }
- return list;
+ return BaseProperty!.GetValue(Group)!;
}
///
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 1706719..3dbab02 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -168,6 +168,11 @@ public static class ModdedOptionsManager
Error($"Failed to get option for {property.Name}");
return;
}
+ if (propertyList.Count != optionList.Count)
+ {
+ Error("Mismatch in count between values and created options.");
+ return;
+ }
var setterOriginal = property.GetSetMethod();
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
@@ -177,7 +182,15 @@ public static class ModdedOptionsManager
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
- // Add indexer patches
+ var listIndex = propertyList.GetType().GetProperty("Item")!;
+
+ var listSetterOriginal = listIndex.GetSetMethod();
+ var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
+ PluginSingleton.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch));
+
+ var listGetterOriginal = listIndex.GetSetMethod();
+ var listGetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch));
+ PluginSingleton.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch));
OptionAttributes.Add(property, attribute);
attribute.HolderOptionList = optionList;
@@ -277,4 +290,34 @@ public static class ModdedOptionsManager
__result = attribute.GetValue();
return false;
}
+
+ ///
+ /// Patches the setter of a list property to update the value of the option.
+ ///
+ /// The original setter method.
+ /// The index to find in the list.
+ /// The new object value.
+#pragma warning disable CA1707
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
+ public static void PropertyListSetterPatch(MethodBase __originalMethod, int index, object value)
+#pragma warning restore CA1707
+ {
+ // Actually implement
+ }
+
+ ///
+ /// Patches the getter of a list property to return the value of the option.
+ ///
+ /// The original getter method.
+ /// The index to find in the list.
+ /// The result of the property getter.
+ /// False so the original getter gets skipped.
+#pragma warning disable CA1707
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
+ public static bool PropertyListGetterPatch(MethodBase __originalMethod, int index, ref object __result)
+#pragma warning restore CA1707
+ {
+ // Actually implement
+ return true;
+ }
}
From 67727945da9c7b028c6f012ab02eaa5ea0070c29 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 20:35:47 -0600
Subject: [PATCH 029/276] Changed to store a reference to the property's value
instance
---
MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs | 5 +++--
MiraAPI/GameOptions/ModdedOptionsManager.cs | 1 +
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
index 3e26432..4a89375 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
@@ -15,7 +15,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
internal PropertyInfo? BaseProperty { get; set; }
- internal AbstractOptionGroup? Group { get; set; }
+ internal object? Value { get; set; }
///
/// Gets the function to title of the options.
@@ -28,6 +28,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
/// The new values as an object.
public override void SetValue(object value)
{
+ Value = value;
var list = (IList)value;
if (list.Count != HolderOptionList!.Count)
{
@@ -53,7 +54,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
/// The value of the options as an object.
public override object GetValue()
{
- return BaseProperty!.GetValue(Group)!;
+ return Value!;
}
///
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 3dbab02..d9c42bc 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -194,6 +194,7 @@ public static class ModdedOptionsManager
OptionAttributes.Add(property, attribute);
attribute.HolderOptionList = optionList;
+ attribute.Value = propertyVal;
for (int i = 0; i < optionList.Count; i++)
{
From edeec4ca4d191ad6e6e7388844baf0b34a753f44 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 20:57:24 -0600
Subject: [PATCH 030/276] Implemented list value patches
---
MiraAPI/GameOptions/ModdedOptionsManager.cs | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index d9c42bc..785c95d 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -295,30 +295,34 @@ public static class ModdedOptionsManager
///
/// Patches the setter of a list property to update the value of the option.
///
- /// The original setter method.
+ /// The list's instance.
/// The index to find in the list.
/// The new object value.
#pragma warning disable CA1707
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
- public static void PropertyListSetterPatch(MethodBase __originalMethod, int index, object value)
+ public static void PropertyListSetterPatch(object __instance, int index, object value)
#pragma warning restore CA1707
{
- // Actually implement
+ var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
+ pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
+ attribute.SetValue(attribute.HolderOptionList![index], value);
}
///
/// Patches the getter of a list property to return the value of the option.
///
- /// The original getter method.
+ /// The list's instance.
/// The index to find in the list.
/// The result of the property getter.
/// False so the original getter gets skipped.
#pragma warning disable CA1707
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
- public static bool PropertyListGetterPatch(MethodBase __originalMethod, int index, ref object __result)
+ public static bool PropertyListGetterPatch(object __instance, int index, ref object __result)
#pragma warning restore CA1707
{
- // Actually implement
- return true;
+ var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
+ pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
+ __result = attribute.GetValue(attribute.HolderOptionList![index]);
+ return false;
}
}
From 3fd3c68aaf643dfb999895cba2501015166dc85c Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 21:06:14 -0600
Subject: [PATCH 031/276] Removed propertyinfo field
---
MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
index 4a89375..a945b1d 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
@@ -13,8 +13,6 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
{
internal IModdedOptionList? HolderOptionList { get; set; }
- internal PropertyInfo? BaseProperty { get; set; }
-
internal object? Value { get; set; }
///
@@ -32,7 +30,7 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
var list = (IList)value;
if (list.Count != HolderOptionList!.Count)
{
- throw new InvalidOperationException($"Value set to {BaseProperty!.Name} cannot change the list's length.");
+ throw new InvalidOperationException($"Value set to option list cannot change the list's length.");
}
for (int i = 0; i < list!.Count; i++)
From c3384ef8506d393c89ac7626c09c5fbf2e237046 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 21:43:26 -0600
Subject: [PATCH 032/276] Changed parent attribute method signatures
---
.../Attributes/ModdedOptionListAttribute.cs | 12 ++++++------
MiraAPI/GameOptions/ModdedOptionsManager.cs | 4 ++--
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
index a945b1d..74615dd 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionListAttribute.cs
@@ -35,16 +35,16 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
for (int i = 0; i < list!.Count; i++)
{
- SetValue(HolderOptionList![i], list[i]!);
+ SetValue(i, list[i]!);
}
}
///
/// Sets the value of the specific option.
///
- /// The option to set.
+ /// The option to set.
/// The new value as an object.
- public abstract void SetValue(IModdedOption modOpt, object value);
+ public abstract void SetValue(int idx, object value);
///
/// Gets the value of all the options.
@@ -58,9 +58,9 @@ public abstract class ModdedOptionListAttribute(Func titler) : Prop
///
/// Gets the value of the specific option.
///
- /// The option to set.
+ /// The option to set.
/// The value of the option as an object.
- public abstract object GetValue(IModdedOption modOpt);
+ public abstract object GetValue(int idx);
- internal abstract IModdedOptionList? CreateOptionList(object? value, PropertyInfo property);
+ internal abstract IModdedOptionList? CreateOptionList(IList value, PropertyInfo property);
}
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 785c95d..ffb85e3 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -305,7 +305,7 @@ public static class ModdedOptionsManager
{
var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
- attribute.SetValue(attribute.HolderOptionList![index], value);
+ attribute.SetValue(index, value);
}
///
@@ -322,7 +322,7 @@ public static class ModdedOptionsManager
{
var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
- __result = attribute.GetValue(attribute.HolderOptionList![index]);
+ __result = attribute.GetValue(index);
return false;
}
}
From 25794b149bc027903140450ea8f3dabd6520b6cc Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 21 Jun 2026 22:05:14 -0600
Subject: [PATCH 033/276] Created attribute versions for option list
---
.../ModdedEnumOptionListAttribute.cs | 67 +++++++++++++++++++
.../ModdedNumberOptionListAttribute.cs | 55 +++++++++++++++
.../ModdedToggleOptionListAttribute.cs | 38 +++++++++++
3 files changed, 160 insertions(+)
create mode 100644 MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs
create mode 100644 MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs
create mode 100644 MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs
diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs
new file mode 100644
index 0000000..c8ec447
--- /dev/null
+++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionListAttribute.cs
@@ -0,0 +1,67 @@
+using MiraAPI.GameOptions.OptionTypes;
+using System;
+using System.Collections;
+using System.Reflection;
+
+namespace MiraAPI.GameOptions.Attributes;
+
+///
+/// Attribute for creating a list of enum options.
+///
+[AttributeUsage(AttributeTargets.Property)]
+public class ModdedEnumOptionListAttribute(Func titler, Type enumType, string[]? values = null)
+ : ModdedOptionListAttribute(titler)
+{
+ internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
+ {
+ var optList = new ModdedOptionList(
+ value.Count, idx => new(Titler(idx), (int)(value[idx] ?? 0), enumType, values));
+ return optList;
+ }
+
+ ///
+ public override void SetValue(int idx, object value)
+ {
+ var opt = HolderOptionList?[idx] as ModdedEnumOption;
+ opt?.SetValue((int)value);
+ }
+
+ ///
+ public override object GetValue(int idx)
+ {
+ return HolderOptionList?[idx] is ModdedEnumOption opt
+ ? Enum.ToObject(enumType, opt.Value)
+ : throw new InvalidOperationException($"HolderOption for option \"{Titler(idx)}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption");
+ }
+}
+
+///
+/// Attribute for creating a list of enum options.
+///
+/// The enum type.
+[AttributeUsage(AttributeTargets.Property)]
+public class ModdedEnumOptionListAttribute(Func titler, string[]? values = null)
+ : ModdedOptionListAttribute(titler) where T : Enum
+{
+ internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
+ {
+ var optList = new ModdedOptionList>(
+ value.Count, idx => new(Titler(idx), (T)(value[idx] ?? 0), values));
+ return optList;
+ }
+
+ ///
+ public override void SetValue(int idx, object value)
+ {
+ var opt = HolderOptionList?[idx] as ModdedEnumOption;
+ opt?.SetValue((T)value);
+ }
+
+ ///
+ public override object GetValue(int idx)
+ {
+ return HolderOptionList?[idx] is ModdedEnumOption opt
+ ? opt.Value
+ : throw new InvalidOperationException($"HolderOption for option \"{Titler(idx)}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption");
+ }
+}
diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs
new file mode 100644
index 0000000..18d7f37
--- /dev/null
+++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionListAttribute.cs
@@ -0,0 +1,55 @@
+using MiraAPI.GameOptions.OptionTypes;
+using MiraAPI.Utilities;
+using System;
+using System.Collections;
+using System.Reflection;
+
+namespace MiraAPI.GameOptions.Attributes;
+
+///
+/// A number option attribute for the modded options system.
+///
+[AttributeUsage(AttributeTargets.Property)]
+public class ModdedNumberOptionListAttribute(
+ Func titler,
+ float min,
+ float max,
+ float increment = 1,
+ MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
+ string? formatString = null,
+ bool zeroInfinity = false)
+ : ModdedOptionListAttribute(titler)
+{
+ internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
+ {
+ return new ModdedOptionList(value.Count, idx =>
+ {
+ return new(
+ Titler(idx),
+ (float)(value[idx] ?? min + increment),
+ min,
+ max,
+ increment,
+ suffixType,
+ formatString,
+ zeroInfinity);
+ });
+ }
+
+ ///
+ public override void SetValue(int idx, object value)
+ {
+ var opt = HolderOptionList?[idx] as ModdedNumberOption;
+ opt?.SetValue((float)value);
+ }
+
+ ///
+ public override object GetValue(int idx)
+ {
+ if (HolderOptionList?[idx] is ModdedNumberOption opt)
+ {
+ return opt.Value;
+ }
+ throw new InvalidOperationException($"HolderOption for option \"{Titler(idx)}\" is not a ModdedNumberOption");
+ }
+}
diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs
new file mode 100644
index 0000000..7e34080
--- /dev/null
+++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionListAttribute.cs
@@ -0,0 +1,38 @@
+using MiraAPI.GameOptions.OptionTypes;
+using System;
+using System.Collections;
+using System.Reflection;
+
+namespace MiraAPI.GameOptions.Attributes;
+
+///
+/// Attribute for a list of toggle options.
+///
+/// The function to get each option's title.
+[AttributeUsage(AttributeTargets.Property)]
+public class ModdedToggleOptionListAttribute(Func titler) : ModdedOptionListAttribute(titler)
+{
+ internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
+ {
+ var optList = new ModdedOptionList(
+ value.Count, idx => new(Titler(idx), (bool)(value[idx] ?? false)));
+ return optList;
+ }
+
+ ///
+ public override void SetValue(int idx, object value)
+ {
+ var opt = HolderOptionList?[idx] as ModdedToggleOption;
+ opt?.SetValue((bool)value);
+ }
+
+ ///
+ public override object GetValue(int idx)
+ {
+ if (HolderOptionList?[idx] is ModdedToggleOption opt)
+ {
+ return opt.Value;
+ }
+ throw new InvalidOperationException($"Holder option for {Titler(idx)} is not a ModdedToggleOption.");
+ }
+}
From a4daa87bfcdfddaf9f2929e41e3b777b0216bf62 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Mon, 29 Jun 2026 16:41:44 -0600
Subject: [PATCH 034/276] Added implicit operator to loadable asset which will
call LoadAsset
---
MiraAPI/Utilities/Assets/LoadableAsset.cs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/MiraAPI/Utilities/Assets/LoadableAsset.cs b/MiraAPI/Utilities/Assets/LoadableAsset.cs
index 798dfc9..58c326c 100644
--- a/MiraAPI/Utilities/Assets/LoadableAsset.cs
+++ b/MiraAPI/Utilities/Assets/LoadableAsset.cs
@@ -18,4 +18,10 @@ public abstract class LoadableAsset where T : UnityEngine.Object
///
/// The loaded asset.
public abstract T LoadAsset();
+
+ ///
+ /// Converts the into its by loading the asset.
+ ///
+ /// The to get the asset from.
+ public static implicit operator T(LoadableAsset loadable) => loadable.LoadAsset();
}
From 706a836e4abb336e94e45a633a35717f63d7eacd Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Tue, 30 Jun 2026 14:17:29 -0600
Subject: [PATCH 035/276] Invert order of operation for ResetTarget so
SetOutline(false) still has previous target
---
MiraAPI/Hud/CustomActionButton.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index f5cf521..8a58e39 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -643,7 +643,7 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB
///
public virtual void ResetTarget()
{
- Target = null;
SetOutline(false);
+ Target = null;
}
}
From 6598cb69331a8b641c7be4cfe39c4b9aead7332e Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Tue, 30 Jun 2026 14:21:28 -0600
Subject: [PATCH 036/276] Undid chaging property to init setter
---
MiraAPI/GameOptions/OptionTypes/ModdedOption.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
index 644c87c..b3197b5 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
@@ -52,9 +52,9 @@ public abstract class ModdedOption : IModdedOption
public T DefaultValue { get; }
///
- /// Gets the event that is invoked when the value of the option changes.
+ /// Gets or sets the event that is invoked when the value of the option changes.
///
- public Action? ChangedEvent { get; init; }
+ public Action? ChangedEvent { get; set; }
///
public Func Visible { get; set; }
From 5d34f78fe0d7b31deb0448f7eeb477239a792ec0 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Thu, 9 Jul 2026 14:04:11 -0600
Subject: [PATCH 037/276] Created parent class for phone menus
---
MiraAPI/Hud/CustomPhoneMenu.cs | 76 +++++++++++++++++++++++++++++++++
MiraAPI/Hud/CustomPlayerMenu.cs | 49 +--------------------
2 files changed, 78 insertions(+), 47 deletions(-)
create mode 100644 MiraAPI/Hud/CustomPhoneMenu.cs
diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs
new file mode 100644
index 0000000..f05b4a3
--- /dev/null
+++ b/MiraAPI/Hud/CustomPhoneMenu.cs
@@ -0,0 +1,76 @@
+using AmongUs.GameOptions;
+using Reactor.Utilities.Extensions;
+using System;
+using System.Diagnostics.CodeAnalysis;
+using UnityEngine;
+using UnityEngine.Events;
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+
+namespace MiraAPI.Hud;
+
+///
+/// Custom Phone Menu using the as a base.
+///
+/// Used by Il2Cpp. Do not use constructor, this is a .
+[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
+[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
+public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
+{
+ public float xStart = -0.8f;
+ public float yStart = 2.15f;
+ public float xOffset = 1.95f;
+ public float yOffset = -0.65f;
+
+ public ShapeshifterPanel panelPrefab;
+ public UiElement backButton;
+ public UiElement defaultButtonSelected;
+
+ ///
+ /// Creates a .
+ ///
+ /// The type of .
+ /// New object.
+ protected static TMenu Create() where TMenu : CustomPhoneMenu
+ {
+ var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter);
+
+ var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu;
+ var newMenu = Instantiate(ogMenu);
+ var customMenu = newMenu.gameObject.AddComponent();
+
+ customMenu.panelPrefab = newMenu.PanelPrefab;
+ customMenu.xStart = newMenu.XStart;
+ customMenu.yStart = newMenu.YStart;
+ customMenu.xOffset = newMenu.XOffset;
+ customMenu.yOffset = newMenu.YOffset;
+ customMenu.defaultButtonSelected = newMenu.DefaultButtonSelected;
+ customMenu.backButton = newMenu.BackButton;
+
+ var back = customMenu.backButton.GetComponent();
+ back.OnClick.RemoveAllListeners();
+ back.OnClick.AddListener((UnityAction)customMenu.Close);
+
+ customMenu.CloseSound = newMenu.CloseSound;
+ customMenu.logger = newMenu.logger;
+ customMenu.OpenSound = newMenu.OpenSound;
+
+ newMenu.DestroyImmediate();
+
+ customMenu.transform.SetParent(Camera.main!.transform, false);
+ customMenu.transform.localPosition = new Vector3(0f, 0f, -50f);
+ return customMenu;
+ }
+
+ private void OnDisable()
+ {
+ ControllerManager.Instance.CloseOverlayMenu(name);
+ }
+
+ ///
+ public override sealed void Begin(PlayerTask task)
+ {
+ throw new NotImplementedException("Use the other Begin method.");
+ }
+}
diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs
index fb27f14..7c13ba8 100644
--- a/MiraAPI/Hud/CustomPlayerMenu.cs
+++ b/MiraAPI/Hud/CustomPlayerMenu.cs
@@ -22,15 +22,8 @@ namespace MiraAPI.Hud;
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
-public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
+public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr)
{
- public ShapeshifterPanel panelPrefab;
- public float xStart = -0.8f;
- public float yStart = 2.15f;
- public float xOffset = 1.95f;
- public float yOffset = -0.65f;
- public UiElement backButton;
- public UiElement defaultButtonSelected;
public List potentialVictims;
///
@@ -39,45 +32,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
/// New object.
public static CustomPlayerMenu Create()
{
- var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter);
-
- var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu;
- var newMenu = Instantiate(ogMenu);
- var customMenu = newMenu.gameObject.AddComponent();
-
- customMenu.panelPrefab = newMenu.PanelPrefab;
- customMenu.xStart = newMenu.XStart;
- customMenu.yStart = newMenu.YStart;
- customMenu.xOffset = newMenu.XOffset;
- customMenu.yOffset = newMenu.YOffset;
- customMenu.backButton = newMenu.BackButton;
- var back = customMenu.backButton.GetComponent();
- back.OnClick.RemoveAllListeners();
- back.OnClick.AddListener((UnityAction)(() =>
- {
- Instance.Close();
- }));
-
- customMenu.CloseSound = newMenu.CloseSound;
- customMenu.logger = newMenu.logger;
- customMenu.OpenSound = newMenu.OpenSound;
-
- newMenu.DestroyImmediate();
-
- customMenu.transform.SetParent(Camera.main!.transform, false);
- customMenu.transform.localPosition = new Vector3(0f, 0f, -50f);
- return customMenu;
- }
-
- private void OnDisable()
- {
- ControllerManager.Instance.CloseOverlayMenu(name);
- }
-
- ///
- public override void Begin(PlayerTask task)
- {
- throw new NotImplementedException("Use the other Begin method.");
+ return Create();
}
///
From 71234128d021e35dc50864e0c8e180d72aa984d3 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Thu, 9 Jul 2026 15:22:50 -0600
Subject: [PATCH 038/276] Added protected methods to add panels to custom Phone
Menu
---
MiraAPI/Hud/CustomPhoneMenu.cs | 48 ++++++++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs
index f05b4a3..dcdcf27 100644
--- a/MiraAPI/Hud/CustomPhoneMenu.cs
+++ b/MiraAPI/Hud/CustomPhoneMenu.cs
@@ -1,7 +1,9 @@
using AmongUs.GameOptions;
using Reactor.Utilities.Extensions;
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using UnityEngine;
using UnityEngine.Events;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
@@ -18,6 +20,18 @@ namespace MiraAPI.Hud;
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
+ public interface IMenuEntry
+ {
+ ShapeshifterPanel Panel { get; }
+ }
+
+ protected record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry
+ {
+ public static implicit operator ShapeshifterPanel(BasicEntry entry) => entry.Panel;
+ }
+
+ public List menuEntries;
+
public float xStart = -0.8f;
public float yStart = 2.15f;
public float xOffset = 1.95f;
@@ -73,4 +87,38 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
throw new NotImplementedException("Use the other Begin method.");
}
+
+ protected void RegisterPanels(
+ IEnumerable entries,
+ Action entrySetter,
+ Func? menuConfig = null)
+ {
+ menuEntries ??= [];
+
+ int currentEntries = menuEntries.Count;
+
+ var list = entries.ToList();
+
+ for (var i = 0; i < list.Count; i++)
+ {
+ var index = currentEntries + i;
+ var entry = list[i];
+
+ var shapeshifterPanel = Instantiate(panelPrefab, transform);
+ shapeshifterPanel!.transform.localPosition = new Vector3(0f, 0f, -1f);
+ entrySetter(shapeshifterPanel, index, entry);
+ var entryMenuConfig = menuConfig ?? ((s, _) => new BasicEntry(s));
+ var menuEntry = entryMenuConfig(shapeshifterPanel, entry);
+ menuEntries.Add(menuEntry);
+ }
+ }
+
+ protected void RegisterPanels(
+ IEnumerable entries,
+ Action onEntryClick,
+ Action entrySetter,
+ Func? menuConfig = null)
+ {
+ RegisterPanels(entries, (p, i, e) => entrySetter(p, i, e, () => onEntryClick(e)), menuConfig);
+ }
}
From 98a0d3e2a6339c16519b158c806a8423961df70d Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Thu, 9 Jul 2026 16:21:47 -0600
Subject: [PATCH 039/276] Changed to use Register Panels
---
MiraAPI/Hud/CustomPlayerMenu.cs | 31 +++++++++++++------------------
1 file changed, 13 insertions(+), 18 deletions(-)
diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs
index 7c13ba8..22c8d74 100644
--- a/MiraAPI/Hud/CustomPlayerMenu.cs
+++ b/MiraAPI/Hud/CustomPlayerMenu.cs
@@ -2,11 +2,9 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using AmongUs.GameOptions;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Patches.Stubs;
using Reactor.Utilities.Attributes;
-using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
@@ -52,23 +50,20 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr)
}));
DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType);
- var list = PlayerControl.AllPlayerControls.ToArray().Where(playerMatch).ToList();
- potentialVictims = [];
var list2 = new Il2CppSystem.Collections.Generic.List();
-
- for (var i = 0; i < list.Count; i++)
- {
- var player = list[i];
- var num = i % 3;
- var num2 = i / 3;
- var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
- var shapeshifterPanel = Instantiate(panelPrefab, transform);
- shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
- shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); }));
- shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
- potentialVictims.Add(shapeshifterPanel);
- list2.Add(shapeshifterPanel.Button);
- }
+ RegisterPanels(
+ PlayerControl.AllPlayerControls.ToArray().Where(playerMatch),
+ (shapeshifterPanel, i, player) =>
+ {
+ var num = i % 3;
+ var num2 = i / 3;
+ var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
+ shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
+ shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); }));
+ shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
+ list2.Add(shapeshifterPanel.Button);
+ });
+ potentialVictims = menuEntries.Cast().ToList();
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
}
}
From e7184c9592784c6cc15a050a48f129adeac88005 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Thu, 9 Jul 2026 23:44:46 -0600
Subject: [PATCH 040/276] Added RoleID further util methods
---
MiraAPI/Roles/RoleId.cs | 58 ++++++++++++++++++++++++++++++++-
MiraAPI/Utilities/Extensions.cs | 2 +-
2 files changed, 58 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/Roles/RoleId.cs b/MiraAPI/Roles/RoleId.cs
index 189682b..96d2db8 100644
--- a/MiraAPI/Roles/RoleId.cs
+++ b/MiraAPI/Roles/RoleId.cs
@@ -1,4 +1,5 @@
-using System;
+using AmongUs.GameOptions;
+using System;
namespace MiraAPI.Roles;
@@ -38,4 +39,59 @@ public static class RoleId
return roleId;
}
+
+ ///
+ /// Gets the ID of a role.
+ ///
+ /// The role.
+ /// The ID as a ushort.
+ /// Thrown when the role is not registered.
+ public static ushort Get(ICustomRole role)
+ {
+ return Get(role.GetType());
+ }
+
+ ///
+ /// Gets the ID of an .
+ ///
+ /// The type of the role.
+ /// The role ID as a .
+ /// Thrown when the role is not registered.
+ public static RoleTypes GetType() where T : ICustomRole
+ {
+ return (RoleTypes)Get();
+ }
+
+ ///
+ /// Gets the ID of a role.
+ ///
+ /// The type of the role.
+ /// The ID as a .
+ /// Thrown when the role is not registered.
+ public static RoleTypes GetType(Type type)
+ {
+ return (RoleTypes)Get(type);
+ }
+
+ ///
+ /// Gets the role.
+ ///
+ /// The type of the role.
+ /// The object.
+ /// Thrown when the role is not registered.
+ public static RoleBehaviour GetRole() where T : ICustomRole
+ {
+ return RoleManager.Instance.GetRole(GetType());
+ }
+
+ ///
+ /// Gets the role from its type.
+ ///
+ /// The type of the role.
+ /// The object.
+ /// Thrown when the role is not registered.
+ public static RoleBehaviour GetRole(Type type)
+ {
+ return RoleManager.Instance.GetRole(GetType(type));
+ }
}
diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs
index 6c8b74b..9c1cb5f 100644
--- a/MiraAPI/Utilities/Extensions.cs
+++ b/MiraAPI/Utilities/Extensions.cs
@@ -43,7 +43,7 @@ public static class Extensions
}
return new NetData(
- RoleId.Get(role.GetType()),
+ RoleId.Get(role),
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
}
From 908ff4fa67f0cde3a94893846e7a94db8b5e8a50 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 10 Jul 2026 11:04:48 -0600
Subject: [PATCH 041/276] Created modded option visibility attribute
---
.../ModdedOptionVisibilityAttribute.cs | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
create mode 100644 MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
new file mode 100644
index 0000000..bd39a50
--- /dev/null
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Linq;
+using System.Reflection;
+
+namespace MiraAPI.GameOptions.Attributes;
+
+///
+/// Attribute to modify a property with a or .
+///
+///
+/// Initializes a new instance of the class.
+///
+/// The type the member is in.
+/// The member to get the visibility function from.
+[AttributeUsage(AttributeTargets.Property)]
+public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, string? memberName = null) : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The member to get the visibility function from.
+ public ModdedOptionVisiblityAttribute(string memberName)
+ : this(null, memberName)
+ {
+ }
+
+#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
+ internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
+ {
+ return null;
+ }
+
+ internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
+ {
+ return null;
+ }
+#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
+}
From 22d45c7c647ed2473f9d6fcc4200cc54489aeaf4 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 10 Jul 2026 13:28:50 -0600
Subject: [PATCH 042/276] Applied attribute function in options manager
---
MiraAPI/GameOptions/ModdedOptionsManager.cs | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 239166c..60b2502 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -111,6 +111,13 @@ public static class ModdedOptionsManager
OptionAttributes.Add(property, attribute);
attribute.HolderOption = option;
+ var visibilityAttr = property.GetCustomAttribute();
+ var visibilityFunc = visibilityAttr?.GetVisibility(group, property);
+ if (visibilityFunc != null)
+ {
+ option.Visible = visibilityFunc;
+ }
+
RegisterOption(option, group, property.Name, pluginInfo);
}
@@ -196,9 +203,16 @@ public static class ModdedOptionsManager
attribute.HolderOptionList = optionList;
attribute.Value = propertyVal;
+ var visibilityAttr = property.GetCustomAttribute();
+ var visibilityFunc = visibilityAttr?.GetListVisibility(group, property);
for (int i = 0; i < optionList.Count; i++)
{
- RegisterOption(optionList[i], group, property.Name + i, pluginInfo);
+ var option = optionList[i];
+ if (visibilityFunc != null)
+ {
+ option.Visible = () => visibilityFunc(i);
+ }
+ RegisterOption(option, group, property.Name + i, pluginInfo);
}
}
From 0895c58524e9fad653d0e4451fba42c49a24768b Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 10 Jul 2026 13:37:55 -0600
Subject: [PATCH 043/276] Added logic to determine method member validity and
obtain visibility function
---
.../ModdedOptionVisibilityAttribute.cs | 65 ++++++++++++++++++-
1 file changed, 64 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
index bd39a50..322efc4 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
@@ -24,15 +24,78 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
{
}
-#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
{
+ MemberInfo? member = GetVisibilityMember(group, property, out Type type);
+
+ if (member is MethodInfo vMethod)
+ {
+ if (vMethod.ReturnType != typeof(bool))
+ {
+ Error($"Method {memberName} does not return a bool.");
+ return null;
+ }
+ var paramList = vMethod.GetParameters();
+ if (paramList.Length > 0)
+ {
+ Error($"Method {memberName} has too many parameters.");
+ return null;
+ }
+
+ return () => (bool)vMethod.Invoke(group, null)!;
+ }
+ // TODO: Properties and/or fields
+
+ Error($"Valid member {memberName} does not exist in group {type.FullName}.");
return null;
}
internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
{
+ MemberInfo? member = GetVisibilityMember(group, property, out Type type);
+
+ if (member is MethodInfo vMethod)
+ {
+ if (vMethod.ReturnType != typeof(bool))
+ {
+ Error($"Method {memberName} does not return a bool.");
+ return null;
+ }
+ var paramList = vMethod.GetParameters();
+ if (paramList.Length > 1)
+ {
+ Error($"Method {memberName} has too many parameters.");
+ return null;
+ }
+ if (paramList.Length == 0)
+ {
+ return _ => (bool)vMethod.Invoke(group, null)!;
+ }
+
+ if (paramList[0].ParameterType != typeof(int))
+ {
+ Error($"Method {memberName}'s parameter is not an int.");
+ return null;
+ }
+ return i => (bool)vMethod.Invoke(group, [i])!;
+ }
+ // TODO: Properties and/or fields
+
+ Error($"Valid member {memberName} does not exist in group {type.FullName}.");
return null;
}
+
+#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
+ private MemberInfo? GetVisibilityMember(AbstractOptionGroup group, PropertyInfo property, out Type type)
+ {
+ type = holderType ?? group.GetType();
+ memberName ??= $"{property.Name}Visible";
+ BindingFlags flags = BindingFlags.Static | BindingFlags.Public;
+ if (holderType == null)
+ {
+ flags |= BindingFlags.Instance | BindingFlags.NonPublic;
+ }
+ return type.GetMember(memberName, flags).FirstOrDefault();
+ }
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
}
From 7b5e38b10e0fd1922a08c3fd1aea0e6fa61bdba2 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 10 Jul 2026 14:09:34 -0600
Subject: [PATCH 044/276] Added null member case
---
.../ModdedOptionVisibilityAttribute.cs | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
index 322efc4..2e5df8f 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
@@ -26,7 +26,11 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
{
- MemberInfo? member = GetVisibilityMember(group, property, out Type type);
+ if (GetVisibilityMember(group, property, out Type type) is not { } member)
+ {
+ Error($"Member {memberName} does not exist in {type.FullName}");
+ return null;
+ }
if (member is MethodInfo vMethod)
{
@@ -44,15 +48,18 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
return () => (bool)vMethod.Invoke(group, null)!;
}
- // TODO: Properties and/or fields
- Error($"Valid member {memberName} does not exist in group {type.FullName}.");
+ Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
return null;
}
internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
{
- MemberInfo? member = GetVisibilityMember(group, property, out Type type);
+ if (GetVisibilityMember(group, property, out Type type) is not { } member)
+ {
+ Error($"Member {memberName} does not exist in {type.FullName}");
+ return null;
+ }
if (member is MethodInfo vMethod)
{
@@ -79,9 +86,8 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
}
return i => (bool)vMethod.Invoke(group, [i])!;
}
- // TODO: Properties and/or fields
- Error($"Valid member {memberName} does not exist in group {type.FullName}.");
+ Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
return null;
}
From bae3a245f1acddda97119e7e2c920b1bc6cddd77 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 10 Jul 2026 14:15:36 -0600
Subject: [PATCH 045/276] Added case for properties, including list values and
indexers
---
.../ModdedOptionVisibilityAttribute.cs | 61 ++++++++++++++++++-
1 file changed, 60 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
index 2e5df8f..b941da8 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -32,6 +33,23 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
return null;
}
+ if (member is PropertyInfo vProperty)
+ {
+ if (vProperty.PropertyType != typeof(bool))
+ {
+ Error($"Property {memberName} is not a bool.");
+ return null;
+ }
+
+ var indexParams = vProperty.GetIndexParameters();
+ if (indexParams.Length > 0)
+ {
+ Error($"Property {memberName} cannot be an indexer.");
+ return null;
+ }
+
+ return () => (bool)vProperty.GetValue(group)!;
+ }
if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
@@ -39,6 +57,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
Error($"Method {memberName} does not return a bool.");
return null;
}
+
var paramList = vMethod.GetParameters();
if (paramList.Length > 0)
{
@@ -61,6 +80,44 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
return null;
}
+ if (member is PropertyInfo vProperty)
+ {
+ if (vProperty.PropertyType != typeof(bool) && !vProperty.PropertyType.IsAssignableTo(typeof(IList)))
+ {
+ Error($"Property {memberName} is not a bool value or indexer, or list of bools.");
+ return null;
+ }
+
+ var indexParams = vProperty.GetIndexParameters();
+ if (indexParams.Length > 1)
+ {
+ Error($"Indexer {memberName} has too many parameters.");
+ return null;
+ }
+
+ if (vProperty.PropertyType.IsAssignableTo(typeof(IList)))
+ {
+ if (indexParams.Length != 0)
+ {
+ Error($"Property {memberName} cannot be an indexer of lists of bools.");
+ return null;
+ }
+
+ return i => ((IList)vProperty.GetValue(group)!)[i];
+ }
+
+ if (indexParams.Length == 0)
+ {
+ return _ => (bool)vProperty.GetValue(group)!;
+ }
+ if (indexParams[0].ParameterType != typeof(int))
+ {
+ Error($"Indexer {memberName}'s parameter is not an int.");
+ return null;
+ }
+
+ return i => (bool)vProperty.GetValue(group, [i])!;
+ }
if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
@@ -68,22 +125,24 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
Error($"Method {memberName} does not return a bool.");
return null;
}
+
var paramList = vMethod.GetParameters();
if (paramList.Length > 1)
{
Error($"Method {memberName} has too many parameters.");
return null;
}
+
if (paramList.Length == 0)
{
return _ => (bool)vMethod.Invoke(group, null)!;
}
-
if (paramList[0].ParameterType != typeof(int))
{
Error($"Method {memberName}'s parameter is not an int.");
return null;
}
+
return i => (bool)vMethod.Invoke(group, [i])!;
}
From 121744e9ef5028f5ab9b491bee8960c80aa9d152 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 10 Jul 2026 16:07:07 -0600
Subject: [PATCH 046/276] Changed what flags are set for member finding
---
.../Attributes/ModdedOptionVisibilityAttribute.cs | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
index b941da8..3a3d489 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
@@ -153,12 +153,19 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
private MemberInfo? GetVisibilityMember(AbstractOptionGroup group, PropertyInfo property, out Type type)
{
- type = holderType ?? group.GetType();
- memberName ??= $"{property.Name}Visible";
+ Type groupType = group.GetType();
BindingFlags flags = BindingFlags.Static | BindingFlags.Public;
- if (holderType == null)
+
+ type = holderType ?? groupType;
+ memberName ??= $"{property.Name}Visible";
+ if (type.IsAssignableTo(typeof(AbstractOptionGroup)))
{
- flags |= BindingFlags.Instance | BindingFlags.NonPublic;
+ flags |= BindingFlags.Instance;
+
+ if (type == groupType)
+ {
+ flags |= BindingFlags.NonPublic;
+ }
}
return type.GetMember(memberName, flags).FirstOrDefault();
}
From a2f363c8da4a2ba6d7e9d41ed1a2c2039cc72fe7 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Fri, 10 Jul 2026 16:48:50 -0600
Subject: [PATCH 047/276] Added instanceGet out parameter to provide the group
instance when needed, null when static, or the Singleton Instance if another
OptionGroup
---
.../ModdedOptionVisibilityAttribute.cs | 40 ++++++++++++++-----
1 file changed, 29 insertions(+), 11 deletions(-)
diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
index 3a3d489..acf929a 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedOptionVisibilityAttribute.cs
@@ -27,7 +27,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
internal Func? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
{
- if (GetVisibilityMember(group, property, out Type type) is not { } member)
+ if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member)
{
Error($"Member {memberName} does not exist in {type.FullName}");
return null;
@@ -48,7 +48,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
return null;
}
- return () => (bool)vProperty.GetValue(group)!;
+ return () => (bool)vProperty.GetValue(instanceGet?.Invoke())!;
}
if (member is MethodInfo vMethod)
{
@@ -65,7 +65,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
return null;
}
- return () => (bool)vMethod.Invoke(group, null)!;
+ return () => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!;
}
Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
@@ -74,7 +74,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
internal Func? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
{
- if (GetVisibilityMember(group, property, out Type type) is not { } member)
+ if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member)
{
Error($"Member {memberName} does not exist in {type.FullName}");
return null;
@@ -103,12 +103,12 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
return null;
}
- return i => ((IList)vProperty.GetValue(group)!)[i];
+ return i => ((IList)vProperty.GetValue(instanceGet?.Invoke())!)[i];
}
if (indexParams.Length == 0)
{
- return _ => (bool)vProperty.GetValue(group)!;
+ return _ => (bool)vProperty.GetValue(instanceGet?.Invoke())!;
}
if (indexParams[0].ParameterType != typeof(int))
{
@@ -116,7 +116,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
return null;
}
- return i => (bool)vProperty.GetValue(group, [i])!;
+ return i => (bool)vProperty.GetValue(instanceGet?.Invoke(), [i])!;
}
if (member is MethodInfo vMethod)
{
@@ -135,7 +135,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
if (paramList.Length == 0)
{
- return _ => (bool)vMethod.Invoke(group, null)!;
+ return _ => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!;
}
if (paramList[0].ParameterType != typeof(int))
{
@@ -143,7 +143,7 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
return null;
}
- return i => (bool)vMethod.Invoke(group, [i])!;
+ return i => (bool)vMethod.Invoke(instanceGet?.Invoke(), [i])!;
}
Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
@@ -151,11 +151,12 @@ public sealed class ModdedOptionVisiblityAttribute(Type? holderType = null, stri
}
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
- private MemberInfo? GetVisibilityMember(AbstractOptionGroup group, PropertyInfo property, out Type type)
+ private MemberInfo? GetVisibilityMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out Func
/// The instance.
- protected record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry
+ protected sealed record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry
{
public static implicit operator ShapeshifterPanel(BasicEntry entry) => entry.Panel;
public static implicit operator BasicEntry(ShapeshifterPanel panel) => new(panel);
}
- public List menuEntries;
+ protected List menuEntries = [];
public List EntryPanels => menuEntries.Select(e => e.Panel).ToList();
@@ -122,8 +122,6 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
Action entryPanelConfig,
Func? menuEntryMaker = null)
{
- menuEntries ??= [];
-
int currentEntries = menuEntries.Count;
var list = entries.ToList();
From 04329e1c87348538fb7372c88d98737f5ac6462a Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 16:22:55 -0600
Subject: [PATCH 058/276] Added generic to only have certain types of entries
---
MiraAPI/Hud/CustomPhoneMenu.cs | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs
index 1f07601..ade867e 100644
--- a/MiraAPI/Hud/CustomPhoneMenu.cs
+++ b/MiraAPI/Hud/CustomPhoneMenu.cs
@@ -39,9 +39,9 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
public static implicit operator BasicEntry(ShapeshifterPanel panel) => new(panel);
}
- protected List menuEntries = [];
+ protected List MenuEntries { get; set; } = [];
- public List EntryPanels => menuEntries.Select(e => e.Panel).ToList();
+ public List EntryPanels => MenuEntries.Select(e => e.Panel).ToList();
public float xStart = -0.8f;
public float yStart = 2.15f;
@@ -122,7 +122,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
Action entryPanelConfig,
Func? menuEntryMaker = null)
{
- int currentEntries = menuEntries.Count;
+ int currentEntries = MenuEntries.Count;
var list = entries.ToList();
@@ -137,7 +137,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
menuEntryMaker ??= (s, _) => new BasicEntry(s);
var menuEntry = menuEntryMaker(shapeshifterPanel, entry);
- menuEntries.Add(menuEntry);
+ MenuEntries.Add(menuEntry);
var button = shapeshifterPanel.Button;
var nameplate = shapeshifterPanel.gameObject.transform.FindChild("Nameplate");
@@ -196,3 +196,14 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
}
}
}
+
+///
+/// The type of menu entries.
+public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) where TMenu : CustomPhoneMenu.IMenuEntry
+{
+ protected new List MenuEntries
+ {
+ get => base.MenuEntries.Cast().ToList();
+ set => base.MenuEntries = value.Cast().ToList();
+ }
+}
From d65203692be0fcd51d1b4452294a4bfe83502c9a Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 16:31:53 -0600
Subject: [PATCH 059/276] Created Paginable Menu subclass based on TOU
GuesserMenu
---
MiraAPI/Hud/CustomPaginableMenu.cs | 367 +++++++++++++++++++++++++++++
1 file changed, 367 insertions(+)
create mode 100644 MiraAPI/Hud/CustomPaginableMenu.cs
diff --git a/MiraAPI/Hud/CustomPaginableMenu.cs b/MiraAPI/Hud/CustomPaginableMenu.cs
new file mode 100644
index 0000000..4ef1df6
--- /dev/null
+++ b/MiraAPI/Hud/CustomPaginableMenu.cs
@@ -0,0 +1,367 @@
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Utilities.Assets;
+using Reactor.Utilities;
+using Reactor.Utilities.Extensions;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using TMPro;
+using UnityEngine;
+using UnityEngine.Events;
+using UnityEngine.UI;
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+#pragma warning disable S2365 // Properties should not make collection or array copies
+
+namespace MiraAPI.Hud;
+
+///
+/// Paginable using the as a base.
+///
+/// Used by Il2Cpp. Do not use constructor, this is a .
+[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
+[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
+public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr)
+{
+ protected abstract string Name { get; }
+
+ protected int currentPage;
+
+ protected abstract TextBoxTMP? PrefabTextbox { get; }
+
+ private TextBoxTMP? searchTextbox;
+ private string searchText = string.Empty;
+ private TextMeshPro? noResultsText;
+
+ private const int ItemsPerPage = 15;
+
+ protected override float MenuDepth => -60f;
+
+ public record MenuEntry(ShapeshifterPanel Panel, string SortKey) : IMenuEntry;
+
+ ///
+ /// Creates a .
+ ///
+ /// The type of .
+ /// Function that can optionally be run when the mouse is moved outside a menu panel.
+ /// Function that can optionally be run when the mouse is moved over a menu panel.
+ /// New object.
+ protected static new TMenu Create(
+ PanelButtonOnMouse? onMouseOut = null,
+ PanelButtonOnMouse? onMouseOver = null
+ ) where TMenu : CustomPaginableMenu
+ {
+ TMenu customMenu = CustomPhoneMenu.Create(onMouseOut, onMouseOver);
+
+ var nextButton = Instantiate(customMenu.backButton, customMenu.transform).gameObject;
+ nextButton.transform.localPosition = new Vector3(1.85f, -2.185f, customMenu.MenuDepth);
+ nextButton.transform.localScale = new Vector3(0.65f, 0.65f, 1);
+ nextButton.name = "RightArrowButton";
+ nextButton.GetComponent().sprite = MiraAssets.NextButton.LoadAsset();
+ nextButton.gameObject.GetComponent().DestroyImmediate();
+
+ var passiveButton = nextButton.gameObject.GetComponent();
+ passiveButton.OnClick = new Button.ButtonClickedEvent();
+ passiveButton.OnClick.AddListener((UnityAction)customMenu.NextPage);
+
+ var backButton = Instantiate(nextButton, customMenu.transform).gameObject;
+ backButton.transform.localPosition = new Vector3(-1.85f, -2.185f, customMenu.MenuDepth);
+ backButton.name = "LeftArrowButton";
+ backButton.gameObject.GetComponent().Destroy();
+ backButton.GetComponent().flipX = true;
+ var prevPassive = backButton.gameObject.GetComponent();
+ prevPassive.OnClick.AddListener((UnityAction)customMenu.PreviousPage);
+
+ customMenu.PhoneUI.GetChild(0).GetComponent().material =
+ PlayerControl.LocalPlayer.cosmetics.currentBodySprite.BodySprite.material;
+ customMenu.PhoneUI.GetChild(1).GetComponent().material =
+ PlayerControl.LocalPlayer.cosmetics.currentBodySprite.BodySprite.material;
+
+ return customMenu;
+ }
+
+ private static string NormalizeForSearch(string? text)
+ {
+ return string.IsNullOrWhiteSpace(text)
+ ? string.Empty
+ : text.Trim().ToLowerInvariant();
+ }
+
+ [HideFromIl2Cpp]
+ private List GetFilteredEntries()
+ {
+ var query = NormalizeForSearch(searchText);
+ if (string.IsNullOrEmpty(query))
+ {
+ return MenuEntries;
+ }
+
+ return MenuEntries
+ .Where(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(e => e.SortKey.Equals(query, StringComparison.OrdinalIgnoreCase))
+ .ThenByDescending(e => e.SortKey.StartsWith(query, StringComparison.OrdinalIgnoreCase))
+ .ThenByDescending(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase))
+ .ThenBy(e => e.SortKey, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ private static int GetTotalPages(int itemCount)
+ {
+ return Mathf.Max(1, Mathf.CeilToInt(itemCount / (float)ItemsPerPage));
+ }
+
+ private void RefreshControllerOverlay(Il2CppSystem.Collections.Generic.List list)
+ {
+ if (ControllerManager.Instance && backButton != null)
+ {
+ ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list);
+ }
+ }
+
+ [HideFromIl2Cpp]
+ private IEnumerator CoRestoreFocus()
+ {
+ yield return null;
+ searchTextbox?.GiveFocus();
+ }
+
+ private void NextPage()
+ {
+ var filtered = GetFilteredEntries();
+ var pages = GetTotalPages(filtered.Count);
+ currentPage = (currentPage + 1) % pages;
+ var list = ShowPage();
+ RefreshControllerOverlay(list);
+ }
+
+ private void PreviousPage()
+ {
+ var filtered = GetFilteredEntries();
+ var pages = GetTotalPages(filtered.Count);
+ currentPage = (currentPage - 1 + pages) % pages;
+ var list = ShowPage();
+ RefreshControllerOverlay(list);
+ }
+
+ public Il2CppSystem.Collections.Generic.List ShowPage()
+ {
+ foreach (var entry in MenuEntries)
+ {
+ entry.Panel.gameObject.SetActive(false);
+ }
+
+ var filtered = GetFilteredEntries();
+ noResultsText?.gameObject.SetActive(filtered.Count == 0 && !string.IsNullOrWhiteSpace(searchText));
+ var totalPages = GetTotalPages(filtered.Count);
+ currentPage = Mathf.Clamp(currentPage, 0, totalPages - 1);
+
+ var list = filtered.Skip(currentPage * ItemsPerPage).Take(ItemsPerPage).ToList();
+ var list2 = new Il2CppSystem.Collections.Generic.List();
+
+ for (var i = 0; i < list.Count; i++)
+ {
+ var entry = list[i];
+ var num = i % 3;
+ var num2 = i / 3 % 5;
+ entry.Panel.transform.localPosition =
+ new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
+ entry.Panel.gameObject.SetActive(true);
+ list2.Add(entry.Panel.Button);
+ }
+
+ return list2;
+ }
+
+ [HideFromIl2Cpp]
+ protected void EnsureSearchUi()
+ {
+ if (searchTextbox != null)
+ {
+ return;
+ }
+
+ var gridCenterX = xStart + xOffset;
+ var desiredSearchBarCenterY = yStart + 0.55f;
+
+ if (PrefabTextbox == null)
+ {
+ return;
+ }
+
+ var searchRoot = PrefabTextbox.transform.parent != null ? PrefabTextbox.transform.parent.gameObject : PrefabTextbox.gameObject;
+ var searchObj = Instantiate(searchRoot, transform);
+ searchObj.name = $"{Name}SearchBar";
+
+ foreach (var aspect in searchObj.GetComponentsInChildren(true))
+ {
+ aspect.DestroyImmediate();
+ }
+
+ searchTextbox = searchObj.GetComponentInChildren(true);
+ if (searchTextbox == null)
+ {
+ return;
+ }
+
+ try
+ {
+ var placeholder = searchTextbox.transform.parent.GetChild(2).GetComponent();
+ placeholder?.gameObject.SetActive(false);
+ }
+ catch
+ {
+ foreach (var tmp in searchObj.GetComponentsInChildren(true))
+ {
+ if (tmp != searchTextbox.outputText &&
+ (tmp.text.Contains("Search", StringComparison.OrdinalIgnoreCase) ||
+ tmp.text.Contains("Here", StringComparison.OrdinalIgnoreCase)))
+ {
+ tmp.gameObject.SetActive(false);
+ }
+ }
+ }
+
+ searchObj.transform.localScale = new Vector3(0.7f, 0.7f, 1f);
+ searchObj.transform.localPosition = new Vector3(gridCenterX, desiredSearchBarCenterY, -1f);
+
+ var searchBounds = CalcSpriteBoundsInParentSpace(transform, searchObj);
+ var deltaX = gridCenterX - searchBounds.center.x;
+ var deltaY = desiredSearchBarCenterY - searchBounds.center.y;
+ searchObj.transform.localPosition += new Vector3(deltaX, deltaY, 0f);
+ searchBounds = CalcSpriteBoundsInParentSpace(transform, searchObj);
+
+ var wikiClickSound = HudManager.Instance?.MapButton?.ClickSound;
+
+ var searchFocusButton = searchTextbox.gameObject.GetComponent()
+ ?? searchTextbox.gameObject.AddComponent();
+ if (wikiClickSound != null)
+ {
+ searchFocusButton.ClickSound = wikiClickSound;
+ }
+ searchFocusButton.OnClick.RemoveAllListeners();
+ searchFocusButton.OnClick.AddListener((UnityAction)(Action)(() =>
+ {
+ searchTextbox.GiveFocus();
+ }));
+
+ searchTextbox.SetText(string.Empty);
+ searchTextbox.OnChange.RemoveAllListeners();
+ searchTextbox.OnChange.AddListener((UnityAction)(Action)(() =>
+ {
+ searchText = searchTextbox.outputText.text ?? string.Empty;
+ currentPage = 0;
+ var list = ShowPage();
+ RefreshControllerOverlay(list);
+
+ if (searchTextbox != null)
+ {
+ Coroutines.Start(CoRestoreFocus());
+ }
+ }));
+
+ var label = Instantiate(HudManager.Instance?.TaskPanel.taskText, transform);
+ if (label != null)
+ {
+ label.name = $"{Name}SearchLabel";
+ label.text = "Search";
+ label.alignment = TextAlignmentOptions.Center;
+ label.fontSize = label.fontSizeMin = label.fontSizeMax = 2.1f;
+ label.color = Color.white;
+ label.transform.localPosition = new Vector3(gridCenterX, searchBounds.max.y + 0.18f, -1f);
+
+ if (searchTextbox.outputText != null)
+ {
+ label.font = searchTextbox.outputText.font;
+ label.fontMaterial = searchTextbox.outputText.fontMaterial;
+ }
+ }
+
+ noResultsText = Instantiate(HudManager.Instance?.TaskPanel.taskText, transform);
+ if (noResultsText != null)
+ {
+ noResultsText.name = $"{Name}NoResultsText";
+ noResultsText.text = "No results";
+ noResultsText.alignment = TextAlignmentOptions.Center;
+ noResultsText.fontSize = noResultsText.fontSizeMin = noResultsText.fontSizeMax = 2.25f;
+ noResultsText.color = Color.white;
+ noResultsText.transform.localPosition = new Vector3(gridCenterX, yStart + 0.1f, -1f);
+ noResultsText.gameObject.SetActive(false);
+ }
+
+ if (backButton != null && searchTextbox != null)
+ {
+ var clearButtonX = searchBounds.max.x + 0.2f;
+ var clearButtonY = searchBounds.center.y;
+
+ var clearObj = Instantiate(backButton.gameObject, transform);
+ clearObj.name = "ClearSearchButton";
+ clearObj.transform.localScale = new Vector3(0.35f, 0.35f, 1f);
+ clearObj.transform.localPosition = new Vector3(clearButtonX, clearButtonY, -1f);
+
+ clearObj.GetComponent()?.DestroyImmediate();
+ clearObj.GetComponent()?.DestroyImmediate();
+
+ var clearSearchButton = clearObj.GetComponent();
+ if (clearSearchButton != null)
+ {
+ if (wikiClickSound != null)
+ {
+ clearSearchButton.ClickSound = wikiClickSound;
+ }
+
+ clearSearchButton.OnClick.RemoveAllListeners();
+ clearSearchButton.OnClick = new Button.ButtonClickedEvent();
+ clearSearchButton.OnClick.AddListener((UnityAction)(() =>
+ {
+ if (searchTextbox == null)
+ {
+ return;
+ }
+
+ searchTextbox.SetText(string.Empty);
+ searchText = string.Empty;
+ currentPage = 0;
+ var list = ShowPage();
+ RefreshControllerOverlay(list);
+ }));
+ }
+ }
+ }
+
+ [HideFromIl2Cpp]
+ private static Bounds CalcSpriteBoundsInParentSpace(Transform parent, GameObject root)
+ {
+ var first = true;
+ var bounds = default(Bounds);
+
+ foreach (var r in root.GetComponentsInChildren(true))
+ {
+ if (r == null || r.sprite == null) continue;
+
+ var b = r.bounds;
+ var c = b.center;
+ var e = b.extents;
+
+ var p1 = parent.InverseTransformPoint(new Vector3(c.x - e.x, c.y - e.y, c.z));
+ var p2 = parent.InverseTransformPoint(new Vector3(c.x - e.x, c.y + e.y, c.z));
+ var p3 = parent.InverseTransformPoint(new Vector3(c.x + e.x, c.y - e.y, c.z));
+ var p4 = parent.InverseTransformPoint(new Vector3(c.x + e.x, c.y + e.y, c.z));
+
+ if (first)
+ {
+ bounds = new Bounds(p1, Vector3.zero);
+ first = false;
+ }
+
+ bounds.Encapsulate(p1);
+ bounds.Encapsulate(p2);
+ bounds.Encapsulate(p3);
+ bounds.Encapsulate(p4);
+ }
+
+ return bounds;
+ }
+}
From 6175628fdff7c1a7116121e543b38e5a83e08b09 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 19:53:12 -0600
Subject: [PATCH 060/276] Moved interface out of class
---
MiraAPI/Hud/CustomPhoneMenu.cs | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs
index ade867e..26e5cf2 100644
--- a/MiraAPI/Hud/CustomPhoneMenu.cs
+++ b/MiraAPI/Hud/CustomPhoneMenu.cs
@@ -14,6 +14,17 @@ using UnityEngine.Events;
namespace MiraAPI.Hud;
+///
+/// Defines an entry in a with a Panel.
+///
+public interface IMenuEntry
+{
+ ///
+ /// Gets the base Panel of the menu entry.
+ ///
+ ShapeshifterPanel Panel { get; }
+}
+
///
/// Custom Phone Menu using the as a base.
///
@@ -23,11 +34,6 @@ namespace MiraAPI.Hud;
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
- public interface IMenuEntry
- {
- ShapeshifterPanel Panel { get; }
- }
-
///
/// Menu Entry used when specifically only the Panel itself is required.
///
@@ -199,7 +205,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
///
/// The type of menu entries.
-public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) where TMenu : CustomPhoneMenu.IMenuEntry
+public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) where TMenu : IMenuEntry
{
protected new List MenuEntries
{
From 49e5061d8803d2a9a01e164aaa6c68715feb2a5c Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 20:00:37 -0600
Subject: [PATCH 061/276] Created interfaces to define entries stored within
the menu
---
MiraAPI/Hud/CustomPhoneMenu.cs | 36 +++++++++++++++++++++++++++++-----
1 file changed, 31 insertions(+), 5 deletions(-)
diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs
index 26e5cf2..6514672 100644
--- a/MiraAPI/Hud/CustomPhoneMenu.cs
+++ b/MiraAPI/Hud/CustomPhoneMenu.cs
@@ -25,6 +25,32 @@ public interface IMenuEntry
ShapeshifterPanel Panel { get; }
}
+///
+/// Defines a custom menu with a list of entries.
+///
+public interface ICustomMenu
+{
+ ///
+ /// Gets all registered s.
+ ///
+ List MenuEntries { get; }
+}
+
+///
+/// Defines a custom menu with a list of only entries.
+///
+/// Only implement this if you want to define a single new type of menu entries explicitly.
+/// Must reference the member of the superclass that is being hidden by this one to work.
+///
+/// The type of menu entry.
+public interface ICustomMenu : ICustomMenu where TMenu : IMenuEntry
+{
+ ///
+ /// Gets all registered s.
+ ///
+ new List MenuEntries { get; }
+}
+
///
/// Custom Phone Menu using the as a base.
///
@@ -32,7 +58,7 @@ public interface IMenuEntry
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
-public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
+public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), ICustomMenu
{
///
/// Menu Entry used when specifically only the Panel itself is required.
@@ -45,7 +71,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
public static implicit operator BasicEntry(ShapeshifterPanel panel) => new(panel);
}
- protected List MenuEntries { get; set; } = [];
+ public List MenuEntries { get; protected set; } = [];
public List EntryPanels => MenuEntries.Select(e => e.Panel).ToList();
@@ -205,11 +231,11 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
///
/// The type of menu entries.
-public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) where TMenu : IMenuEntry
+public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr), ICustomMenu where TMenu : IMenuEntry
{
- protected new List MenuEntries
+ public new List MenuEntries
{
get => base.MenuEntries.Cast().ToList();
- set => base.MenuEntries = value.Cast().ToList();
+ protected set => base.MenuEntries = value.Cast().ToList();
}
}
From 88e17c4f586aaeafb03ed05b4f3051e57f7c8002 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 20:15:00 -0600
Subject: [PATCH 062/276] Marked method as Obsolete to prevent it from being
used.
---
MiraAPI/Hud/CustomPhoneMenu.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs
index 6514672..141538f 100644
--- a/MiraAPI/Hud/CustomPhoneMenu.cs
+++ b/MiraAPI/Hud/CustomPhoneMenu.cs
@@ -143,11 +143,14 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I
ControllerManager.Instance.CloseOverlayMenu(name);
}
- ///
+#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member
+ [Obsolete("Will always fail. Call or define another Begin method that calls MinigameStubs.Begin", true)]
+ [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Overrides an unusable method.")]
public override sealed void Begin(PlayerTask task)
{
throw new NotImplementedException("Use the other Begin method.");
}
+#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member
protected void RegisterPanels(
IEnumerable entries,
From a5ec81b51f4579414351d9426c772a8f9b1268b3 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 20:18:49 -0600
Subject: [PATCH 063/276] Created basic MultiSelect Custom Menu
---
MiraAPI/Hud/CustomMultiSelectMenu.cs | 136 +++++++++++++++++++++++++++
1 file changed, 136 insertions(+)
create mode 100644 MiraAPI/Hud/CustomMultiSelectMenu.cs
diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs
new file mode 100644
index 0000000..323bc08
--- /dev/null
+++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs
@@ -0,0 +1,136 @@
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Patches.Stubs;
+using MiraAPI.Utilities.Assets;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using UnityEngine;
+using UnityEngine.Events;
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+#pragma warning disable S2365 // Properties should not make collection or array copies
+
+namespace MiraAPI.Hud;
+
+///
+/// Multi-select using the as a base.
+///
+/// Used by Il2Cpp. Do not use constructor, this is a .
+/// The type of object each entry represents.
+[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
+[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
+public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr)
+ : CustomPhoneMenu.MenuEntry>(il2CppPtr) where TEntry : class
+{
+ private int totalSelections;
+ private bool shouldConfirm;
+ private readonly List selectedEntries = [];
+
+ private LoadableAsset? hoverSelectSprite;
+ private LoadableAsset? hoverDeselectSprite;
+ private Color? activeColor;
+ private Color? hoverSelectColor;
+ private Color? hoverDeselectColor;
+
+ public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry;
+
+ protected static TMenu Create(
+ Color? activeColor = null,
+ LoadableAsset? hoverSelectSprite = null,
+ Color? hoverSelectColor = null,
+ LoadableAsset? hoverDeselectSprite = null,
+ Color? hoverDeselectColor = null,
+ PanelButtonOnMouse? onMouseOut = null,
+ PanelButtonOnMouse? onMouseOver = null
+ ) where TMenu : CustomMultiSelectMenu
+ {
+ TMenu customMenu = Create(onMouseOut, onMouseOver);
+
+ customMenu.activeColor = activeColor;
+
+ customMenu.hoverSelectSprite = hoverSelectSprite;
+ customMenu.hoverSelectColor = hoverSelectColor;
+
+ customMenu.hoverDeselectSprite = hoverDeselectSprite ?? hoverSelectSprite;
+ customMenu.hoverDeselectColor = hoverDeselectColor ?? hoverSelectColor;
+
+ return customMenu;
+ }
+
+ protected abstract void SetupPanelEntry(ShapeshifterPanel panel, int i, TEntry entry, Action onClick);
+
+ ///
+ /// Begins/opens the custom multi-select menu.
+ ///
+ /// All entries to give the custom menu.
+ /// Function called when all selections are made/confirmed.
+ /// The number of selections required.
+ /// Wheter the entire selection should be confirmed manually.
+ [HideFromIl2Cpp]
+ protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm)
+ {
+ MinigameStubs.Begin(this, null);
+
+ this.totalSelections = totalSelections;
+ this.shouldConfirm = shouldConfirm;
+ var back = backButton.GetComponent();
+ back.OnClick.AddListener((UnityAction)(() =>
+ {
+ onClick(null);
+ }));
+
+ DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType);
+ var list2 = new Il2CppSystem.Collections.Generic.List();
+ RegisterPanels(
+ entries,
+ (shapeshifterPanel, i, entry) =>
+ {
+ var num = i % 3;
+ var num2 = i / 3;
+ shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
+ SetupPanelEntry(shapeshifterPanel, i, entry, () => OnEntryClick(entry, onClick));
+ list2.Add(shapeshifterPanel.Button);
+ },
+ (p, e) => new MenuEntry(p, e));
+ ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
+ }
+
+ private void OnEntryClick(TEntry entry, Action> onClick)
+ {
+ MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry);
+
+ if (selectedEntries.Remove(menuEntry)) // Unselect previous choice
+ {
+ SetNameplateAppearance(menuEntry, false);
+ return;
+ }
+
+ selectedEntries.Add(menuEntry);
+ if (selectedEntries.Count < totalSelections) // Add choice to list of selections
+ {
+ SetNameplateAppearance(menuEntry, true);
+ return;
+ }
+
+ // Total selections reached
+ onClick(selectedEntries.Select(se => se.Entry).ToList());
+ selectedEntries.Clear();
+ }
+
+ protected override bool IsEntrySelected(IMenuEntry entry)
+ {
+ return selectedEntries.Any(e => e.Panel == entry.Panel);
+ }
+
+ [HideFromIl2Cpp]
+ protected void SetNameplateAppearance(IMenuEntry menuEntry, bool isSelected)
+ {
+ SetNameplateAppearance(
+ menuEntry,
+ isSelected ? hoverDeselectSprite : hoverSelectSprite,
+ isSelected ? hoverDeselectColor : hoverSelectColor,
+ isSelected ? activeColor : Color.clear);
+ }
+}
From e6621bd0369d031730847a950900aaf492842c61 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:07:23 -0600
Subject: [PATCH 064/276] Added overload for single selections
---
MiraAPI/Hud/CustomMultiSelectMenu.cs | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs
index 323bc08..646ea2a 100644
--- a/MiraAPI/Hud/CustomMultiSelectMenu.cs
+++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs
@@ -97,6 +97,17 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr)
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
}
+ ///
+ /// Begins/opens the custom menu, only requiring a single selection.
+ ///
+ /// All entries to give the custom menu.
+ /// Function called when the selection is made.
+ [HideFromIl2Cpp]
+ protected void Begin(IEnumerable entries, Action onClick)
+ {
+ Begin(entries, list => onClick(list?[0]), 1, false);
+ }
+
private void OnEntryClick(TEntry entry, Action> onClick)
{
MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry);
From 810facf50e5fb5fbcd8dd43702060f655d70552f Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:12:56 -0600
Subject: [PATCH 065/276] Added canRepeat parameter
---
MiraAPI/Hud/CustomMultiSelectMenu.cs | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs
index 646ea2a..5fd597c 100644
--- a/MiraAPI/Hud/CustomMultiSelectMenu.cs
+++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs
@@ -26,6 +26,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr)
{
private int totalSelections;
private bool shouldConfirm;
+ private bool canRepeat;
private readonly List selectedEntries = [];
private LoadableAsset? hoverSelectSprite;
@@ -68,13 +69,15 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr)
/// Function called when all selections are made/confirmed.
/// The number of selections required.
/// Wheter the entire selection should be confirmed manually.
+ /// If the same entry can be selected multiple times, else unselect entry on click.
[HideFromIl2Cpp]
- protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm)
+ protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm, bool canRepeat = false)
{
MinigameStubs.Begin(this, null);
this.totalSelections = totalSelections;
this.shouldConfirm = shouldConfirm;
+ this.canRepeat = canRepeat;
var back = backButton.GetComponent();
back.OnClick.AddListener((UnityAction)(() =>
{
@@ -112,7 +115,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr)
{
MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry);
- if (selectedEntries.Remove(menuEntry)) // Unselect previous choice
+ if (!canRepeat && selectedEntries.Remove(menuEntry)) // Unselect previous choice
{
SetNameplateAppearance(menuEntry, false);
return;
From 4fb9897cc696b1c36e6cb2f42f13e9124e6ada13 Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:19:59 -0600
Subject: [PATCH 066/276] Made CustomPlayerMenu extend MultiSelectMenu
---
MiraAPI/Hud/CustomPlayerMenu.cs | 35 +++++++++------------------------
1 file changed, 9 insertions(+), 26 deletions(-)
diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs
index 0317dfc..b060631 100644
--- a/MiraAPI/Hud/CustomPlayerMenu.cs
+++ b/MiraAPI/Hud/CustomPlayerMenu.cs
@@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Il2CppInterop.Runtime.Attributes;
-using MiraAPI.Patches.Stubs;
using Reactor.Utilities.Attributes;
using UnityEngine;
-using UnityEngine.Events;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
@@ -20,7 +18,7 @@ namespace MiraAPI.Hud;
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
-public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr)
+public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu(il2CppPtr)
{
public List potentialVictims;
@@ -41,29 +39,14 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr)
[HideFromIl2Cpp]
public void Begin(Func playerMatch, Action onClick)
{
- MinigameStubs.Begin(this, null);
-
- var back = backButton.GetComponent();
- back.OnClick.AddListener((UnityAction)(() =>
- {
- onClick(null);
- }));
-
- DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType);
- var list2 = new Il2CppSystem.Collections.Generic.List();
- RegisterPanels(
- PlayerControl.AllPlayerControls.ToArray().Where(playerMatch),
- (shapeshifterPanel, i, player) =>
- {
- var num = i % 3;
- var num2 = i / 3;
- var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
- shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
- shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); }));
- shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
- list2.Add(shapeshifterPanel.Button);
- });
+ Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick);
potentialVictims = EntryPanels;
- ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
+ }
+
+ protected override void SetupPanelEntry(ShapeshifterPanel panel, int i, PlayerControl player, Action onClick)
+ {
+ var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
+ panel.SetPlayer(i, player.Data, (Il2CppSystem.Action)onClick);
+ panel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
}
}
From 54f594f94733a3a3145a49fa0f3fb87e40a6ebbe Mon Sep 17 00:00:00 2001
From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:28:11 -0600
Subject: [PATCH 067/276] Added 2 selections overload
---
MiraAPI/Hud/CustomMultiSelectMenu.cs | 13 +++++++++++++
MiraAPI/Hud/CustomPlayerMenu.cs | 14 ++++++++++++++
2 files changed, 27 insertions(+)
diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs
index 5fd597c..3383bd1 100644
--- a/MiraAPI/Hud/CustomMultiSelectMenu.cs
+++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs
@@ -111,6 +111,19 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr)
Begin(entries, list => onClick(list?[0]), 1, false);
}
+ ///
+ /// Begins/opens the custom menu, requiring two selections.
+ ///
+ /// All entries to give the custom menu.
+ /// Function called when both selections are made.
+ /// Wheter the set of both selections should be confirmed manually.
+ /// If the same entry can be selected both times, else unselect entry on click.
+ [HideFromIl2Cpp]
+ protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm, bool canRepeat = false)
+ {
+ Begin(entries, list => onClick(list?[0], list?[1]), 2, shouldConfirm, canRepeat);
+ }
+
private void OnEntryClick(TEntry entry, Action