diff --git a/R4Utils/ValueEqualityCollections/Extensions.cs b/R4Utils/ValueEqualityCollections/Extensions.cs
new file mode 100644
index 0000000..dfb6161
--- /dev/null
+++ b/R4Utils/ValueEqualityCollections/Extensions.cs
@@ -0,0 +1,21 @@
+namespace R4Utils.ValueEqualityCollections;
+
+public static class ValueEqualityCollectionsExtensions
+{
+ ///
+ /// Transform this collection into a collection that performs value equality on its items for deducing collection equality.
+ ///
+ /// The resulting will consider ordering whenever compared to another instance also considering ordering.
+ ///
+ public static ValueEqualityCollection AsOrderedValueEqualityCollection(this IList collection)
+ where T : IEquatable => new(collection, ValueEqualityCollection.OrderMode.Consider);
+
+ ///
+ /// Transform this collection into a collection that performs value equality on its items for deducing collection equality.
+ ///
+ /// As the underlying does not support ordering (does not inherit ),
+ /// the resulting will ignore ordering whenever it is compared.
+ ///
+ public static ValueEqualityCollection AsValueEqualityCollection(this ICollection collection)
+ where T : IEquatable => new(collection, ValueEqualityCollection.OrderMode.Ignore);
+}
diff --git a/R4Utils/ValueEqualityCollections/ValueEqualityCollection.cs b/R4Utils/ValueEqualityCollections/ValueEqualityCollection.cs
new file mode 100644
index 0000000..c7e2a64
--- /dev/null
+++ b/R4Utils/ValueEqualityCollections/ValueEqualityCollection.cs
@@ -0,0 +1,137 @@
+using System.Collections;
+using System.Numerics;
+
+namespace R4Utils.ValueEqualityCollections;
+
+///
+/// A wrapper around an that uses value equality of its elements for equality of the collections.
+///
+public class ValueEqualityCollection : ICollection, IEquatable>,
+ IEqualityOperators, ValueEqualityCollection, bool> where T : IEquatable
+{
+ ///
+ /// Defines strategies of dealing with ordering when comparing two instances.
+ ///
+ public enum OrderMode
+ {
+ ///
+ /// Consider ordering when comparing this instance with another one.
+ ///
+ Consider,
+
+ ///
+ /// Ignore ordering when comparing this instance with another one.
+ ///
+ /// If any of the compared instances has this set, the ordering will be ignored.
+ ///
+ Ignore,
+ }
+
+ private ICollection Collection { get; }
+
+ ///
+ /// Whether to consider ordering when comparing this instance with another one.
+ ///
+ /// Regardless of this property, ordering will only be considered if both collections implement .
+ ///
+ public OrderMode Ordering { get; }
+
+ ///
+ /// Create a new wrapper around that uses items equality for instance equality.
+ ///
+ public ValueEqualityCollection(ICollection collection, OrderMode ordering)
+ {
+ Collection = collection;
+ Ordering = ordering;
+ }
+
+ public override string ToString() =>
+ $"{nameof(ValueEqualityCollection)}[{nameof(Ordering)}={Ordering}]({Collection})";
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)Collection).GetEnumerator();
+ }
+
+ public static bool operator ==(ValueEqualityCollection? left, ValueEqualityCollection? right) =>
+ left?.Equals(right) ?? right is null;
+
+ public static bool operator !=(ValueEqualityCollection? left, ValueEqualityCollection? right) =>
+ !(left == right);
+
+ public bool Equals(ValueEqualityCollection? other)
+ {
+ if (other is null) return false;
+ return ReferenceEquals(this, other) || EqualityDispatch(other);
+ }
+
+ private bool EqualityDispatch(ValueEqualityCollection other)
+ {
+ if (Ordering is OrderMode.Consider && other.Ordering is OrderMode.Consider && Collection is IList list1 &&
+ other.Collection is IList list2)
+ {
+ return OrderedEquality(list1, list2);
+ }
+
+ return ScrambledEquals(Collection, other.Collection);
+ }
+
+ private static bool OrderedEquality(IList list1, IList list2) => list1.SequenceEqual(list2);
+
+ // Source: https://stackoverflow.com/a/3670089/13849454
+ private static bool ScrambledEquals(ICollection collection1, ICollection collection2)
+ {
+ var cnt = new Dictionary();
+ // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
+ foreach (var s in collection1)
+ {
+ if (!cnt.TryAdd(s, 1))
+ {
+ cnt[s]++;
+ }
+ }
+
+ foreach (var s in collection2)
+ {
+ if (cnt.TryGetValue(s, out var value))
+ {
+ cnt[s] = --value;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return cnt.Values.All(c => c == 0);
+ }
+
+
+ // ICollection implementation
+
+ public IEnumerator GetEnumerator() => Collection.GetEnumerator();
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is null) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ return obj.GetType() == GetType() && Equals((ValueEqualityCollection)obj);
+ }
+
+ // TODO: Look into this, this might in fact be wrong.
+ public override int GetHashCode() => Collection.Aggregate(0, HashCode.Combine);
+
+ public void Add(T item) => Collection.Add(item);
+
+ public void Clear() => Collection.Clear();
+
+ public bool Contains(T item) => Collection.Contains(item);
+
+ public void CopyTo(T[] array, int arrayIndex) => Collection.CopyTo(array, arrayIndex);
+
+ public bool Remove(T item) => Collection.Remove(item);
+
+ public int Count => Collection.Count;
+
+ public bool IsReadOnly => Collection.IsReadOnly;
+}
diff --git a/R4UtilsTester/ValueEqualityCollections/TestValueEqualityCollection.cs b/R4UtilsTester/ValueEqualityCollections/TestValueEqualityCollection.cs
new file mode 100644
index 0000000..c108f99
--- /dev/null
+++ b/R4UtilsTester/ValueEqualityCollections/TestValueEqualityCollection.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using R4Utils.ValueEqualityCollections;
+
+namespace R4UtilsTester.ValueEqualityCollections;
+
+[TestFixture]
+public class TestValueEqualityCollection
+{
+ private static void AssertEqual(ValueEqualityCollection wrapper1, ValueEqualityCollection wrapper2,
+ bool unequal = false) where T : IEquatable
+ {
+ if (unequal)
+ {
+ Assert.AreNotEqual(wrapper1, wrapper2);
+ Assert.IsFalse(wrapper1.Equals(wrapper2));
+ Assert.IsFalse(wrapper1 == wrapper2);
+ Assert.IsTrue(wrapper1 != wrapper2);
+ }
+ else
+ {
+ Assert.AreEqual(wrapper1, wrapper2);
+ Assert.IsTrue(wrapper1.Equals(wrapper2));
+ Assert.IsTrue(wrapper1 == wrapper2);
+ Assert.IsFalse(wrapper1 != wrapper2);
+ }
+ }
+
+ [Test]
+ public void TestEqualArraysEqual()
+ {
+ int[] data1 = [1, 2, 3];
+ int[] data2 = [1, 2, 3];
+ var wrapper1 = data1.AsOrderedValueEqualityCollection();
+ var wrapper2 = data2.AsOrderedValueEqualityCollection();
+ AssertEqual(wrapper1, wrapper2);
+ }
+
+ [Test]
+ public void TestConsidersOrderingOfElements()
+ {
+ int[] data1 = [1, 2, 3];
+ int[] data2 = [2, 1, 3];
+ var wrapper1 = data1.AsOrderedValueEqualityCollection();
+ var wrapper2 = data2.AsOrderedValueEqualityCollection();
+ AssertEqual(wrapper1, wrapper2, true);
+ }
+
+ [Test]
+ public void TestHandlesSetsCorrectly()
+ {
+ HashSet data1 = [1, 2, 3];
+ HashSet data2 = [2, 1, 3];
+ var wrapper1 = data1.AsValueEqualityCollection();
+ var wrapper2 = data2.AsValueEqualityCollection();
+ AssertEqual(wrapper1, wrapper2);
+ }
+
+ [Test]
+ public void TestHandlesSetsMixedWithArraysCorrectly()
+ {
+ HashSet data1 = [1, 2, 3];
+ int[] data2 = [2, 1, 3];
+ var wrapper1 = data1.AsValueEqualityCollection();
+ var wrapper2 = data2.AsOrderedValueEqualityCollection();
+ AssertEqual(wrapper1, wrapper2);
+ }
+
+ [Test]
+ public void TestIgnoreOrdering()
+ {
+ bool[] bools = [true, false];
+ foreach (var data1IgnoreOrdering in bools)
+ {
+ foreach (var data2IgnoreOrdering in bools)
+ {
+ int[] data1 = [1, 2, 3];
+ int[] data2 = [2, 1, 3];
+ var wrapper1 = data1IgnoreOrdering
+ ? data1.AsValueEqualityCollection()
+ : data1.AsOrderedValueEqualityCollection();
+ var wrapper2 = data2IgnoreOrdering
+ ? data2.AsValueEqualityCollection()
+ : data2.AsOrderedValueEqualityCollection();
+ Assert.AreEqual(wrapper1.Ordering,
+ data1IgnoreOrdering
+ ? ValueEqualityCollection.OrderMode.Ignore
+ : ValueEqualityCollection.OrderMode.Consider);
+ Assert.AreEqual(wrapper2.Ordering,
+ data2IgnoreOrdering
+ ? ValueEqualityCollection.OrderMode.Ignore
+ : ValueEqualityCollection.OrderMode.Consider);
+ if (data1IgnoreOrdering || data2IgnoreOrdering)
+ {
+ AssertEqual(wrapper1, wrapper2);
+ }
+ else
+ {
+ AssertEqual(wrapper1, wrapper2, true);
+ }
+ }
+ }
+ }
+
+ [Test]
+ public void TestRecursiveWrapper()
+ {
+ HashSet data1 = [1, 2, 3];
+ int[] data2 = [2, 1, 3];
+ var wrapper1 = data1.AsValueEqualityCollection().AsValueEqualityCollection();
+ var wrapper2 = data2.AsOrderedValueEqualityCollection();
+ AssertEqual(wrapper1, wrapper2);
+ }
+
+ [Test]
+ public void TestDuplicateElements()
+ {
+ bool[] bools = [true, false];
+ foreach (var data1IgnoreOrdering in bools)
+ {
+ foreach (var data2IgnoreOrdering in bools)
+ {
+ int[] data1 = [1, 2, 3];
+ int[] data2 = [1, 2, 3, 3];
+ var wrapper1 = data1IgnoreOrdering
+ ? data1.AsValueEqualityCollection()
+ : data1.AsOrderedValueEqualityCollection();
+ var wrapper2 = data2IgnoreOrdering
+ ? data2.AsValueEqualityCollection()
+ : data2.AsOrderedValueEqualityCollection();
+ Assert.AreEqual(wrapper1.Ordering,
+ data1IgnoreOrdering
+ ? ValueEqualityCollection.OrderMode.Ignore
+ : ValueEqualityCollection.OrderMode.Consider);
+ Assert.AreEqual(wrapper2.Ordering,
+ data2IgnoreOrdering
+ ? ValueEqualityCollection.OrderMode.Ignore
+ : ValueEqualityCollection.OrderMode.Consider);
+ AssertEqual(wrapper1, wrapper2, true);
+ }
+ }
+ }
+}