From d8162dcb32dfcce6819b18e35ad01667fe70b2e1 Mon Sep 17 00:00:00 2001 From: Az <56699741+AlchlcDvl@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:23:15 -0400 Subject: [PATCH] avoid patching indexers instead --- MiraAPI.Example/.editorconfig | 3 + MiraAPI.Example/ExampleEventHandlers.cs | 3 +- MiraAPI.Example/Options/ExampleOptions.cs | 3 +- ....cs => ModdedStringOptionListAttribute.cs} | 0 MiraAPI/GameOptions/CollectionBuilder.cs | 8 + MiraAPI/GameOptions/ModdedList.cs | 291 +++++++++++++++++- MiraAPI/GameOptions/ModdedOptionsManager.cs | 52 +--- 7 files changed, 303 insertions(+), 57 deletions(-) rename MiraAPI/GameOptions/Attributes/OptionList/{ModdedStringOptionList.cs => ModdedStringOptionListAttribute.cs} (100%) create mode 100644 MiraAPI/GameOptions/CollectionBuilder.cs diff --git a/MiraAPI.Example/.editorconfig b/MiraAPI.Example/.editorconfig index 171e612..7a18861 100644 --- a/MiraAPI.Example/.editorconfig +++ b/MiraAPI.Example/.editorconfig @@ -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 diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs index 3647899..20c0def 100644 --- a/MiraAPI.Example/ExampleEventHandlers.cs +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; +using System.Globalization; using System.Linq; using MiraAPI.Events; using MiraAPI.Events.Mira; diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index 9e2e1a9..9b424c0 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -42,8 +42,7 @@ public class ExampleOptions : AbstractOptionGroup public Api WorstApi { get; set; } = Api.MiraAPI; [ModdedEnumOptionList("Value State {0}", ["None", "Primary", "Secondary"])] - public ModdedList ValueStates { get; set; } = - [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None]; + public ModdedArray ValueStates { get; set; } = [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None]; [ModdedPlayerOption("options.exampleOptions1.players")] public NetworkedPlayerInfo PlayerSelected { get; set; } diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedStringOptionList.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedStringOptionListAttribute.cs similarity index 100% rename from MiraAPI/GameOptions/Attributes/OptionList/ModdedStringOptionList.cs rename to MiraAPI/GameOptions/Attributes/OptionList/ModdedStringOptionListAttribute.cs diff --git a/MiraAPI/GameOptions/CollectionBuilder.cs b/MiraAPI/GameOptions/CollectionBuilder.cs new file mode 100644 index 0000000..99d42d9 --- /dev/null +++ b/MiraAPI/GameOptions/CollectionBuilder.cs @@ -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; +} diff --git a/MiraAPI/GameOptions/ModdedList.cs b/MiraAPI/GameOptions/ModdedList.cs index b28b1cb..65f60ef 100644 --- a/MiraAPI/GameOptions/ModdedList.cs +++ b/MiraAPI/GameOptions/ModdedList.cs @@ -1,9 +1,294 @@ +using System; +using System.Collections; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace MiraAPI.GameOptions; /// -/// A simple list that extends . +/// A fixed-size array created specifically for modded options that automatically synchronizes with option attributes. /// -/// The type of the values that the list holds. -public sealed class ModdedList : List; +/// The type of the values that the array holds. +[CollectionBuilder(typeof(ModdedArrayBuilder), nameof(ModdedArrayBuilder.Create))] +public sealed class ModdedArray : IList, IReadOnlyList, IList +{ + private readonly T[] items; + private int version; + + /// + public int Count => items.Length; + + /// + public bool IsReadOnly => false; + + /// + bool IList.IsFixedSize => true; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => items.SyncRoot; + + /// + /// Initializes a new instance of the class with the provided capacity. + /// + /// The capacity of the list. + public ModdedArray(int capacity) + { + items = new T[capacity]; + } + + /// + /// Initializes a new instance of the class with initial values. + /// + /// The initial collection of values to initialise with. + public ModdedArray(IEnumerable collection) + { + items = [.. collection]; + } + + /// + /// Initializes a new instance of the class with initial values. + /// + /// The initial span of values to initialise with. + public ModdedArray(Span span) + { + items = [.. span]; + } + + /// + /// Initializes a new instance of the class with initial values. + /// + /// The initial span of values to initialise with. + public ModdedArray(ReadOnlySpan span) + { + items = [.. span]; + } + + /// + /// Gets or sets the element at the specified index, syncing with the attribute if registered. + /// + /// The index of the value. + 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++; + } + } + + /// + object? IList.this[int index] + { + get => this[index]; + set => this[index] = (T)value!; + } + + /// + public bool Contains(T item) + { + return Array.IndexOf(items, item) >= 0; + } + + /// + public void CopyTo(T[] array, int arrayIndex) + { + items.CopyTo(array, arrayIndex); + } + + /// + public int IndexOf(T item) + { + return Array.IndexOf(items, item); + } + + /// + public Enumerator GetEnumerator() + { + return new(this); + } + + /// + bool IList.Contains(object? value) + { + return ((IList)items).Contains(value); + } + + /// + int IList.IndexOf(object? value) + { + return ((IList)items).IndexOf(value); + } + + /// + void ICollection.CopyTo(Array array, int index) + { + items.CopyTo(array, index); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + int IList.Add(object? value) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + void IList.Clear() + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + void IList.Insert(int index, object? value) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + void IList.Remove(object? value) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + void IList.RemoveAt(int index) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public void Insert(int index, T item) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public void RemoveAt(int index) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public void Add(T item) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public void Clear() + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + public bool Remove(T item) + { + throw new NotSupportedException("ModdedArray is of a fixed size."); + } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator : IEnumerator + { + private readonly ModdedArray array; + private readonly int version; + + private int index; + private T? current; + + internal Enumerator(ModdedArray moddedArray) + { + array = moddedArray; + version = moddedArray.version; + index = 0; + current = default; + } + + /// + public readonly T Current => current!; + + /// + 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; + } + } + + /// + public readonly void Dispose() + { + } + + /// + 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; + } + + /// + public void Reset() + { + if (version != array.version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + + index = 0; + current = default; + } + } +} + +/// +/// Provides a builder for to support C# 12 collection expressions. +/// +public static class ModdedArrayBuilder +{ + /// + /// Creates a new from a span of values. + /// + /// The the type of the values. + /// The values to initialise with. + /// The instance initialised with the provided values. + public static ModdedArray Create(ReadOnlySpan values) + { + return new(values); + } +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index b30aa1c..3e11b3d 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -25,18 +25,15 @@ namespace MiraAPI.GameOptions; /// public static class ModdedOptionsManager { - private static readonly ConditionalWeakTable ListInstanceCache = []; private static readonly Dictionary GetterLookups = []; private static readonly Dictionary SetterLookups = []; private static readonly Dictionary TypeToGroup = []; private static readonly HashSet OptionAttributes = []; - private static readonly HashSet 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 ListInstanceCache = []; internal static readonly Dictionary CreatedPlayerOptions = []; internal static readonly Dictionary CreatedStringOptions = []; internal static readonly Dictionary> 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.Instance.Harmony.Patch(listGetterOriginal, prefix: CachedListGetterPatch); - - var listSetterOriginal = listIndex.GetSetMethod(true); - - if (listSetterOriginal != null) - PluginSingleton.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; } - - /// - /// Patches the setter of a list property to update the value of the option. - /// - /// The list's instance. - /// The index to find in the list. - /// The new object value. - [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); - } - - /// - /// Patches the getter of a list property to return the value of the option. - /// - /// The list's instance. - /// The index to find in the list. - /// The result of the property getter. - /// False so the original getter gets skipped. - [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; - } }