diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs
index a085c508..49dd6727 100644
--- a/IPA.Loader/Config/SelfConfig.cs
+++ b/IPA.Loader/Config/SelfConfig.cs
@@ -4,6 +4,7 @@ using IPA.Utilities;
using IPA.Config.Stores;
// END: section ignore
using Newtonsoft.Json;
+using IPA.Config.Stores.Attributes;
namespace IPA.Config
{
@@ -79,6 +80,9 @@ namespace IPA.Config
&& CommandLineValues.Updates.AutoCheckUpdates;
}
+ // LINE: ignore
+ [NonNullable]
+ [JsonProperty(Required = Required.DisallowNull)] // the JsonProperty annotations are for the generated schema
public virtual Updates_ Updates { get; set; } = new Updates_();
public class Debug_
@@ -117,6 +121,9 @@ namespace IPA.Config
|| CommandLineValues.Debug.ShowTrace;
}
+ // LINE: ignore
+ [NonNullable]
+ [JsonProperty(Required = Required.DisallowNull)]
public virtual Debug_ Debug { get; set; } = new Debug_();
public virtual bool YeetMods { get; set; } = true;
@@ -124,7 +131,6 @@ namespace IPA.Config
public static bool YeetMods_ => (Instance?.YeetMods ?? true)
&& CommandLineValues.YeetMods;
- [JsonProperty(Required = Required.Default)]
public virtual string LastGameVersion { get; set; } = null;
// LINE: ignore
public static string LastGameVersion_ => Instance?.LastGameVersion;
diff --git a/IPA.Loader/Config/Stores/Attributes.cs b/IPA.Loader/Config/Stores/Attributes.cs
index 420e7b2a..5c8e22b1 100644
--- a/IPA.Loader/Config/Stores/Attributes.cs
+++ b/IPA.Loader/Config/Stores/Attributes.cs
@@ -6,5 +6,73 @@ using System.Threading.Tasks;
namespace IPA.Config.Stores.Attributes
{
+ ///
+ /// Causes a field or property in an object being wrapped by to be
+ /// ignored during serialization and deserialization.
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class IgnoreAttribute : Attribute { }
+
+ ///
+ /// Indicates that a field or property in an object being wrapped by
+ /// that would otherwise be nullable (i.e. a reference type or a type) should never be null, and the
+ /// member will be ignored if the deserialized value is .
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class NonNullableAttribute : Attribute { }
+
+ ///
+ /// Specifies a name for the serialized field or property in an object being wrapped by
+ /// that is different from the member name itself.
+ ///
+ ///
+ ///
+ /// When serializing the following object, we might get the JSON that follows.
+ ///
+ /// public class PluginConfig
+ /// {
+ /// public virtual bool BooleanField { get; set; } = true;
+ /// }
+ ///
+ ///
+ /// {
+ /// "BooleanField": true
+ /// }
+ ///
+ ///
+ ///
+ /// However, if we were to add a to that field, we would get the following.
+ ///
+ /// public class PluginConfig
+ /// {
+ /// [SerializedName("bool")]
+ /// public virtual bool BooleanField { get; set; } = true;
+ /// }
+ ///
+ ///
+ /// {
+ /// "bool": true
+ /// }
+ ///
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class SerializedNameAttribute : Attribute
+ {
+ ///
+ /// Gets the name to replace the member name with.
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// Creates a new with the given .
+ ///
+ /// the value to assign to
+ public SerializedNameAttribute(string name)
+ {
+ Name = name;
+ }
+ }
+
}
diff --git a/IPA.Loader/Config/Stores/GeneratedStore.cs b/IPA.Loader/Config/Stores/GeneratedStore.cs
index 3ff8f941..9b63614e 100644
--- a/IPA.Loader/Config/Stores/GeneratedStore.cs
+++ b/IPA.Loader/Config/Stores/GeneratedStore.cs
@@ -1,4 +1,5 @@
using IPA.Config.Data;
+using IPA.Config.Stores.Attributes;
using IPA.Logging;
using System;
using System.Collections.Generic;
@@ -13,12 +14,13 @@ using System.Runtime.CompilerServices;
using System.IO;
using Boolean = IPA.Config.Data.Boolean;
using System.Collections;
+using IPA.Utilities;
#if NET3
using Net3_Proxy;
using Array = Net3_Proxy.Array;
#endif
-[assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedStore.GeneratedAssemblyName)]
+[assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedExtension.AssemblyVisibilityTarget)]
namespace IPA.Config.Stores
{
@@ -48,6 +50,15 @@ namespace IPA.Config.Stores
///
///
///
+ /// Only fields and properties that are or will be considered, and only properties
+ /// where both the getter and setter are or are considered. Any fields or properties
+ /// with an applied to them are also ignored. Having properties be is not strictly
+ /// necessary, however it allows the generated type to keep track of changes and lock around them so that the config will auto-save.
+ ///
+ ///
+ /// All of the attributes in the namespace are handled as described by them.
+ ///
+ ///
/// If the declares a or ,
/// method Changed(), then that method may be called to artificially signal to the runtime that the content of the object
/// has changed. That method will also be called after the write locks are released when a property is set anywhere in the owning
@@ -222,9 +233,18 @@ namespace IPA.Config.Stores
{
public string Name;
public MemberInfo Member;
+ public Type Type;
+ public bool AllowNull;
public bool IsVirtual;
public bool IsField;
- public Type Type;
+ public bool IsNullable;
+
+ // invalid for objects with IsNullabe false
+ public Type NullableWrappedType => Nullable.GetUnderlyingType(Type);
+ // invalid for objects with IsNullabe false
+ public PropertyInfo Nullable_HasValue => Type.GetProperty(nameof(Nullable.HasValue));
+ // invalid for objects with IsNullabe false
+ public PropertyInfo Nullable_Value => Type.GetProperty(nameof(Nullable.Value));
}
private static Func MakeCreator(Type type)
@@ -251,15 +271,48 @@ namespace IPA.Config.Stores
var structure = new List();
- // TODO: incorporate attributes/base types
- // TODO: ignore probs without setter
+ // TODO: support converters
+
+ bool ProcessAttributesFor(MemberInfo member, Type memberType, out string name, out bool allowNull, out bool isNullable)
+ {
+ var attrs = member.GetCustomAttributes(true);
+ var ignores = attrs.Select(o => o as IgnoreAttribute).NonNull();
+ if (ignores.Any()) // we ignore
+ {
+ name = null;
+ allowNull = false;
+ isNullable = false;
+ return false;
+ }
+
+ var nonNullables = attrs.Select(o => o as NonNullableAttribute).NonNull();
+
+ name = member.Name;
+ isNullable = memberType.IsConstructedGenericType
+ && memberType.GetGenericTypeDefinition() == typeof(Nullable<>);
+ allowNull = !nonNullables.Any() && (!memberType.IsValueType || isNullable);
+
+ var nameAttr = attrs.Select(o => o as SerializedNameAttribute).NonNull().FirstOrDefault();
+ if (nameAttr != null)
+ name = nameAttr.Name;
+
+ return true;
+ }
- // only looks at public properties
- foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
+ // only looks at public/protected properties
+ foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
+ if (prop.GetSetMethod(true)?.IsPrivate ?? true)
+ { // we enter this block if the setter is inacessible or doesn't exist
+ continue; // ignore props without setter
+ }
+ if (prop.GetGetMethod(true)?.IsPrivate ?? true)
+ { // we enter this block if the getter is inacessible or doesn't exist
+ continue; // ignore props without getter
+ }
+
var smi = new SerializedMemberInfo
{
- Name = prop.Name,
Member = prop,
IsVirtual = (prop.GetGetMethod(true)?.IsVirtual ?? false) ||
(prop.GetSetMethod(true)?.IsVirtual ?? false),
@@ -267,21 +320,26 @@ namespace IPA.Config.Stores
Type = prop.PropertyType
};
+ if (!ProcessAttributesFor(smi.Member, smi.Type, out smi.Name, out smi.AllowNull, out smi.IsNullable)) continue;
+
structure.Add(smi);
}
- // only look at public fields
- foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
+ // only look at public/protected fields
+ foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
+ if (field.IsPrivate) continue;
+
var smi = new SerializedMemberInfo
{
- Name = field.Name,
Member = field,
IsVirtual = false,
IsField = true,
Type = field.FieldType
};
+ if (!ProcessAttributesFor(smi.Member, smi.Type, out smi.Name, out smi.AllowNull, out smi.IsNullable)) continue;
+
structure.Add(smi);
}
#endregion
@@ -788,18 +846,18 @@ namespace IPA.Config.Stores
}
}
- // TODO: implement Nullable
-
EmitLoad();
var endSerialize = il.DefineLabel();
- if (!member.Type.IsValueType)
+ if (member.AllowNull)
{
var passedNull = il.DefineLabel();
il.Emit(OpCodes.Dup);
il.Emit(OpCodes.Brtrue, passedNull);
+ // TODO: add special check for nullables
+
il.Emit(OpCodes.Pop);
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Br, endSerialize);
@@ -972,7 +1030,7 @@ namespace IPA.Config.Stores
// TODO: support Nullable
- if (member.Type.IsValueType)
+ if (!member.AllowNull)
{
il.Emit(OpCodes.Pop);
EmitLogError(il, $"Member {member.Name} ({member.Type}) not nullable", tailcall: false,
diff --git a/IPA.Loader/Loader/DisabledConfig.cs b/IPA.Loader/Loader/DisabledConfig.cs
index 82d6ee3f..b27b3261 100644
--- a/IPA.Loader/Loader/DisabledConfig.cs
+++ b/IPA.Loader/Loader/DisabledConfig.cs
@@ -1,5 +1,6 @@
using IPA.Config;
using IPA.Config.Stores;
+using IPA.Config.Stores.Attributes;
using IPA.Utilities;
using System;
using System.Collections.Generic;