mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-19 15:51:48 +00:00
avoid patching indexers instead
This commit is contained in:
7 files changed
+303
-57
No files matched your search
@@ -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,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using MiraAPI.Events;
|
||||
using MiraAPI.Events.Mira;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
File renamed without changes.
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user