avoid patching indexers instead

This commit is contained in:
Az committed 2026-10-17 18:23:15 -04:00
1 parent 5106efe2ac
commit d8162dcb32
7 files changed
+303 -57

No files matched your search

+3
View File
@@ -2,3 +2,6 @@
# SA1602: Enumeration items should be documented.
dotnet_diagnostic.SA1602.severity = none
# SA1601: Partial elements should be documented
dotnet_diagnostic.SA1601.severity = none
+1 -2
View File
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Globalization;
using System.Linq;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
+1 -2
View File
@@ -42,8 +42,7 @@ public class ExampleOptions : AbstractOptionGroup
public Api WorstApi { get; set; } = Api.MiraAPI;
[ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])]
public ModdedList<ValueState> ValueStates { get; set; } =
[ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
public ModdedArray<ValueState> ValueStates { get; set; } = [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
[ModdedPlayerOption("options.exampleOptions1.players")]
public NetworkedPlayerInfo PlayerSelected { get; set; }
+8
View File
@@ -0,0 +1,8 @@
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)]
internal sealed class CollectionBuilderAttribute(Type builderType, string methodName) : Attribute
{
public Type BuilderType { get; } = builderType;
public string MethodName { get; } = methodName;
}
+288 -3
View File
@@ -1,9 +1,294 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace MiraAPI.GameOptions;
/// <summary>
/// A simple list that extends <see cref="List{T}"/>.
/// A fixed-size array created specifically for modded options that automatically synchronizes with option attributes.
/// </summary>
/// <typeparam name="T">The type of the values that the list holds.</typeparam>
public sealed class ModdedList<T> : List<T>;
/// <typeparam name="T">The type of the values that the array holds.</typeparam>
[CollectionBuilder(typeof(ModdedArrayBuilder), nameof(ModdedArrayBuilder.Create))]
public sealed class ModdedArray<T> : IList<T>, IReadOnlyList<T>, IList
{
private readonly T[] items;
private int version;
/// <inheritdoc/>
public int Count => items.Length;
/// <inheritdoc/>
public bool IsReadOnly => false;
/// <inheritdoc/>
bool IList.IsFixedSize => true;
/// <inheritdoc/>
bool ICollection.IsSynchronized => false;
/// <inheritdoc/>
object ICollection.SyncRoot => items.SyncRoot;
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with the provided capacity.
/// </summary>
/// <param name="capacity">The capacity of the list.</param>
public ModdedArray(int capacity)
{
items = new T[capacity];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="collection">The initial collection of values to initialise with.</param>
public ModdedArray(IEnumerable<T> collection)
{
items = [.. collection];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="span">The initial span of values to initialise with.</param>
public ModdedArray(Span<T> span)
{
items = [.. span];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="span">The initial span of values to initialise with.</param>
public ModdedArray(ReadOnlySpan<T> span)
{
items = [.. span];
}
/// <summary>
/// Gets or sets the element at the specified index, syncing with the attribute if registered.
/// </summary>
/// <param name="index">The index of the value.</param>
public T this[int index]
{
get => ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute)
? (T)attribute.GetValue(index)
: items[index];
set
{
if (ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute))
attribute.SetValue(index, value!);
else
items[index] = value;
version++;
}
}
/// <inheritdoc/>
object? IList.this[int index]
{
get => this[index];
set => this[index] = (T)value!;
}
/// <inheritdoc/>
public bool Contains(T item)
{
return Array.IndexOf(items, item) >= 0;
}
/// <inheritdoc/>
public void CopyTo(T[] array, int arrayIndex)
{
items.CopyTo(array, arrayIndex);
}
/// <inheritdoc/>
public int IndexOf(T item)
{
return Array.IndexOf(items, item);
}
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public Enumerator GetEnumerator()
{
return new(this);
}
/// <inheritdoc/>
bool IList.Contains(object? value)
{
return ((IList)items).Contains(value);
}
/// <inheritdoc/>
int IList.IndexOf(object? value)
{
return ((IList)items).IndexOf(value);
}
/// <inheritdoc/>
void ICollection.CopyTo(Array array, int index)
{
items.CopyTo(array, index);
}
/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc/>
int IList.Add(object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Clear()
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Insert(int index, object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Remove(object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.RemoveAt(int index)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Insert(int index, T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void RemoveAt(int index)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Add(T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Clear()
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public bool Remove(T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <summary>
/// Enumerates the elements of a <see cref="ModdedArray{T}"/>.
/// </summary>
public struct Enumerator : IEnumerator<T>
{
private readonly ModdedArray<T> array;
private readonly int version;
private int index;
private T? current;
internal Enumerator(ModdedArray<T> moddedArray)
{
array = moddedArray;
version = moddedArray.version;
index = 0;
current = default;
}
/// <inheritdoc/>
public readonly T Current => current!;
/// <inheritdoc/>
readonly object? IEnumerator.Current
{
get
{
if (index == 0 || index == array.Count + 1)
throw new InvalidOperationException("Enumeration has not started or has already finished.");
return Current;
}
}
/// <inheritdoc/>
public readonly void Dispose()
{
}
/// <inheritdoc/>
public bool MoveNext()
{
if (version != array.version)
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
if ((uint)index < (uint)array.Count)
{
current = array[index];
index++;
return true;
}
index = array.Count + 1;
current = default;
return false;
}
/// <inheritdoc/>
public void Reset()
{
if (version != array.version)
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
index = 0;
current = default;
}
}
}
/// <summary>
/// Provides a builder for <see cref="ModdedArray{T}"/> to support C# 12 collection expressions.
/// </summary>
public static class ModdedArrayBuilder
{
/// <summary>
/// Creates a new <see cref="ModdedArray{T}"/> from a span of values.
/// </summary>
/// <typeparam name="T">The the type of the values.</typeparam>
/// <param name="values">The values to initialise with.</param>
/// <returns>The <see cref="ModdedArray{T}"/> instance initialised with the provided values.</returns>
public static ModdedArray<T> Create<T>(ReadOnlySpan<T> values)
{
return new(values);
}
}
+2 -50
View File
@@ -25,18 +25,15 @@ namespace MiraAPI.GameOptions;
/// </summary>
public static class ModdedOptionsManager
{
private static readonly ConditionalWeakTable<object, ModdedOptionListAttribute> ListInstanceCache = [];
private static readonly Dictionary<MethodBase, PropertyOptionAttribute> GetterLookups = [];
private static readonly Dictionary<MethodBase, PropertyOptionAttribute> SetterLookups = [];
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
private static readonly HashSet<PropertyInfo> OptionAttributes = [];
private static readonly HashSet<Type> PatchedListTypes = [];
private static readonly HarmonyMethod CachedGetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyGetterPatch));
private static readonly HarmonyMethod CachedSetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertySetterPatch));
private static readonly HarmonyMethod CachedListGetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyListGetterPatch));
private static readonly HarmonyMethod CachedListSetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyListSetterPatch));
internal static readonly ConditionalWeakTable<object, ModdedOptionListAttribute> ListInstanceCache = [];
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
internal static readonly Dictionary<OptionBehaviour, ModdedStringOption> CreatedStringOptions = [];
internal static readonly Dictionary<Type, List<AbstractOptionGroup>> GameModeOptionGroups = [];
@@ -278,7 +275,7 @@ public static class ModdedOptionsManager
var listType = propertyList.GetType();
var valueType = listType.GetGenericArguments()[0];
if (!listType.IsGenericType || listType.GetGenericTypeDefinition() != typeof(ModdedList<>))
if (!listType.IsGenericType || listType.GetGenericTypeDefinition() != typeof(ModdedArray<>))
{
Error($"Please use ModdedList<{valueType.Name}> for modded option lists.");
return;
@@ -296,19 +293,6 @@ public static class ModdedOptionsManager
SetterLookups[setterOriginal] = attribute;
}
if (PatchedListTypes.Add(listType))
{
var listIndex = listType.GetProperty("Item")!;
var listGetterOriginal = listIndex.GetGetMethod(true);
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: CachedListGetterPatch);
var listSetterOriginal = listIndex.GetSetMethod(true);
if (listSetterOriginal != null)
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listSetterOriginal, postfix: CachedListSetterPatch);
}
attribute.HolderOptionList = optionList;
attribute.Value = propertyList;
attribute.ValueType = valueType;
@@ -453,36 +437,4 @@ public static class ModdedOptionsManager
__result = attribute.GetValue();
return false;
}
/// <summary>
/// Patches the setter of a list property to update the value of the option.
/// </summary>
/// <param name="__instance">The list's instance.</param>
/// <param name="index">The index to find in the list.</param>
/// <param name="value">The new object value.</param>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static void PropertyListSetterPatch(object __instance, int index, object value)
{
if (ListInstanceCache.TryGetValue(__instance, out var match))
match.SetValue(index, value);
}
/// <summary>
/// Patches the getter of a list property to return the value of the option.
/// </summary>
/// <param name="__instance">The list's instance.</param>
/// <param name="index">The index to find in the list.</param>
/// <param name="__result">The result of the property getter.</param>
/// <returns>False so the original getter gets skipped.</returns>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static bool PropertyListGetterPatch(object __instance, int index, ref object __result)
{
if (!ListInstanceCache.TryGetValue(__instance, out var match))
return true;
__result = match.GetValue(index);
return false;
}
}