diff --git a/IPA.Loader/Config/Stores/Attributes.cs b/IPA.Loader/Config/Stores/Attributes.cs
index def1d292..d2708b30 100644
--- a/IPA.Loader/Config/Stores/Attributes.cs
+++ b/IPA.Loader/Config/Stores/Attributes.cs
@@ -23,7 +23,7 @@ namespace IPA.Config.Stores.Attributes
///
/// Indicates that a given field or property in an object being wrapped by
- /// should be serialized and deserialized using the provided converter instead of the default mechanism.
+ /// should be serialized and deserialized using the provided converter instead of the default mechanism.
///
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class UseConverterAttribute : Attribute
@@ -40,6 +40,11 @@ namespace IPA.Config.Stores.Attributes
ConverterType.BaseType.GetGenericArguments()[0] :
null;
+ ///
+ /// Gets whether or not this converter is a generic .
+ ///
+ public bool IsGenericConverter => ConverterTargetType != null;
+
///
/// Creates a new with a given .
///
diff --git a/IPA.Loader/Config/Stores/CollectionConverter.cs b/IPA.Loader/Config/Stores/CollectionConverter.cs
new file mode 100644
index 00000000..29a47794
--- /dev/null
+++ b/IPA.Loader/Config/Stores/CollectionConverter.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using IPA.Config.Data;
+
+namespace IPA.Config.Stores.Converters
+{
+ ///
+ /// A base class for all type converters, providing most of the functionality.
+ ///
+ /// the type of the items in the collection
+ /// the instantiated type of collection
+ public class CollectionConverter : ValueConverter
+ where TCollection : ICollection
+ {
+ ///
+ /// Creates a using the default converter for the
+ /// element type. Equivalent to calling
+ /// with .
+ ///
+ ///
+ public CollectionConverter() : this(Converter.Default) { }
+ ///
+ /// Creates a using the specified underlying converter.
+ ///
+ /// the to use to convert the values
+ public CollectionConverter(ValueConverter underlying)
+ => BaseConverter = underlying;
+
+ ///
+ /// Gets the converter for the collection's value type.
+ ///
+ protected ValueConverter BaseConverter { get; }
+ ///
+ /// Creates a collection of type using the and
+ /// .
+ ///
+ /// the initial size of the collecion
+ /// the object that will own the new collection
+ /// a new instance of
+ ///
+ protected virtual TCollection Create(int size, object parent)
+ => Activator.CreateInstance();
+ ///
+ /// Populates the colleciton with the deserialized values from
+ /// with the parent .
+ ///
+ /// the collection to populate
+ /// the values to populate it with
+ /// the object that will own the new objects
+ ///
+ protected void PopulateFromValue(TCollection col, List list, object parent)
+ {
+ foreach (var it in list)
+ col.Add(BaseConverter.FromValue(it, parent));
+ }
+ ///
+ /// Deserializes a in into a new
+ /// owned by .
+ ///
+ /// the to convert to a
+ /// the object that will own the resulting
+ /// a new holding the deserialized content of
+ ///
+ public override TCollection FromValue(Value value, object parent)
+ {
+ if (!(value is List list)) throw new ArgumentException("Argument not a List", nameof(value));
+
+ var col = Create(list.Count, parent);
+ PopulateFromValue(col, list, parent);
+ return col;
+ }
+ ///
+ /// Serializes a into a .
+ ///
+ /// the to serialize
+ /// the object owning
+ /// the that was serialized into
+ ///
+ public override Value ToValue(TCollection obj, object parent)
+ => Value.From(obj.Select(t => BaseConverter.ToValue(t, parent)));
+ }
+ ///
+ /// A which default constructs a converter for use as the value converter.
+ ///
+ /// the value type of the collection
+ /// the type of the colleciton
+ /// the type of the converter to use for
+ ///
+ public class CollectionConverter : CollectionConverter
+ where TCollection : ICollection
+ where TConverter : ValueConverter, new()
+ {
+ ///
+ /// Creates a using the default converter for the
+ /// element type. Equivalent to calling
+ /// with a default-constructed .
+ ///
+ ///
+ public CollectionConverter() : base(new TConverter()) { }
+ }
+
+ public class ISetConverter : CollectionConverter>
+ {
+ public ISetConverter() : base() { }
+ public ISetConverter(ValueConverter underlying) : base(underlying) { }
+ protected override ISet Create(int size, object parent)
+ => new HashSet();
+ }
+
+ public class ListConverter : CollectionConverter>
+ {
+ public ListConverter() : base() { }
+ public ListConverter(ValueConverter underlying) : base(underlying) { }
+ protected override List Create(int size, object parent)
+ => new List(size);
+ }
+
+ public class IListConverter : CollectionConverter>
+ {
+ public IListConverter() : base() { }
+ public IListConverter(ValueConverter underlying) : base(underlying) { }
+ protected override IList Create(int size, object parent)
+ => new List(size);
+ }
+
+}
diff --git a/IPA.Loader/Config/Stores/Converters.cs b/IPA.Loader/Config/Stores/Converters.cs
index 605bd08d..5f5a0a0b 100644
--- a/IPA.Loader/Config/Stores/Converters.cs
+++ b/IPA.Loader/Config/Stores/Converters.cs
@@ -1,4 +1,5 @@
using IPA.Config.Data;
+using IPA.Config.Stores.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -171,6 +172,24 @@ namespace IPA.Config.Stores.Converters
=> obj == null ? null : baseConverter.ToValue(obj.Value, parent);
}
+ ///
+ /// A converter for a that default-constructs a converter of type
+ /// to use as the underlying converter. Use this in the .
+ ///
+ /// the underlying type of the
+ /// the type to use as an underlying converter
+ ///
+ public class NullableConverter : NullableConverter
+ where T : struct
+ where TConverter : ValueConverter, new()
+ {
+ ///
+ /// Creates a converter with a new as the underlying converter.
+ ///
+ ///
+ public NullableConverter() : base(new TConverter()) { }
+ }
+
internal class StringConverter : ValueConverter
{
public override string FromValue(Value value, object parent)
diff --git a/IPA.Loader/Config/Stores/GeneratedStore.cs b/IPA.Loader/Config/Stores/GeneratedStore.cs
index 83903abf..67114b74 100644
--- a/IPA.Loader/Config/Stores/GeneratedStore.cs
+++ b/IPA.Loader/Config/Stores/GeneratedStore.cs
@@ -325,10 +325,10 @@ namespace IPA.Config.Stores
var converterAttr = attrs.Select(o => o as UseConverterAttribute).NonNull().FirstOrDefault();
if (converterAttr != null)
- {
+ { // TODO: figure out how to represent chaining
member.Converter = converterAttr.ConverterType;
member.ConverterTarget = converterAttr.ConverterTargetType;
- member.IsGenericConverter = member.ConverterTarget != null;
+ member.IsGenericConverter = converterAttr.IsGenericConverter;
}
return true;
diff --git a/IPA.Loader/Config/Stores/ValueConverter.cs b/IPA.Loader/Config/Stores/ValueConverter.cs
index ec5b9628..bd3dd555 100644
--- a/IPA.Loader/Config/Stores/ValueConverter.cs
+++ b/IPA.Loader/Config/Stores/ValueConverter.cs
@@ -19,6 +19,9 @@ namespace IPA.Config.Stores
/// be (ideally) the the top of the serialization tree, or some other generated object in that tree, rather than some arbitrary
/// object in the middle that is not managed by the generatd config system.
///
+ ///
+ /// Converters do not need to perform null checks, as the serializer and deserializer will do that automatically.
+ ///
///
public interface IValueConverter
{
diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj
index 5a97c74b..a4dec5e0 100644
--- a/IPA.Loader/IPA.Loader.csproj
+++ b/IPA.Loader/IPA.Loader.csproj
@@ -97,6 +97,7 @@
+
diff --git a/IPA.Loader/Loader/DisabledConfig.cs b/IPA.Loader/Loader/DisabledConfig.cs
index b27b3261..eb8a9473 100644
--- a/IPA.Loader/Loader/DisabledConfig.cs
+++ b/IPA.Loader/Loader/DisabledConfig.cs
@@ -1,6 +1,7 @@
using IPA.Config;
using IPA.Config.Stores;
using IPA.Config.Stores.Attributes;
+using IPA.Config.Stores.Converters;
using IPA.Utilities;
using System;
using System.Collections.Generic;
@@ -24,6 +25,8 @@ namespace IPA.Loader
public virtual bool Reset { get; set; } = true;
+ [NonNullable]
+ [UseConverter(typeof(CollectionConverter>))]
public virtual HashSet DisabledModIds { get; set; } = new HashSet();
protected virtual void OnReload()