diff --git a/IPA.Loader/Config/Stores/Attributes.cs b/IPA.Loader/Config/Stores/Attributes.cs
index d9361054..7cc2ee39 100644
--- a/IPA.Loader/Config/Stores/Attributes.cs
+++ b/IPA.Loader/Config/Stores/Attributes.cs
@@ -1,121 +1,121 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-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 { }
-
- ///
- /// 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.
- ///
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
- public sealed class UseConverterAttribute : Attribute
- {
- ///
- /// Gets the type of the converter to use.
- ///
- public Type ConverterType { get; }
- ///
- /// Gets the target type of the converter if it is avaliable at instantiation time, otherwise
- /// .
- ///
- public Type ConverterTargetType { get; }
-
- ///
- /// Gets whether or not this converter is a generic .
- ///
- public bool IsGenericConverter => ConverterTargetType != null;
-
- ///
- /// Creates a new with a given .
- ///
- /// tpy type to assign to
- public UseConverterAttribute(Type converterType)
- {
- ConverterType = converterType;
-
- var baseT = ConverterType.BaseType;
- while (baseT != null && baseT != typeof(object) &&
- (!baseT.IsGenericType || baseT.GetGenericTypeDefinition() != typeof(ValueConverter<>)))
- baseT = baseT.BaseType;
- if (baseT == typeof(object)) ConverterTargetType = null;
- else ConverterTargetType = baseT.GetGenericArguments()[0];
-
- var implInterface = ConverterType.GetInterfaces().Contains(typeof(IValueConverter));
-
- if (ConverterTargetType == null && !implInterface) throw new ArgumentException("Type is not a value converter!");
- }
- }
-
- ///
- /// 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;
- }
- }
-
-
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+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 { }
+
+ ///
+ /// 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.
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class UseConverterAttribute : Attribute
+ {
+ ///
+ /// Gets the type of the converter to use.
+ ///
+ public Type ConverterType { get; }
+ ///
+ /// Gets the target type of the converter if it is avaliable at instantiation time, otherwise
+ /// .
+ ///
+ public Type ConverterTargetType { get; }
+
+ ///
+ /// Gets whether or not this converter is a generic .
+ ///
+ public bool IsGenericConverter => ConverterTargetType != null;
+
+ ///
+ /// Creates a new with a given .
+ ///
+ /// the type to assign to
+ public UseConverterAttribute(Type converterType)
+ {
+ ConverterType = converterType;
+
+ var baseT = ConverterType.BaseType;
+ while (baseT != null && baseT != typeof(object) &&
+ (!baseT.IsGenericType || baseT.GetGenericTypeDefinition() != typeof(ValueConverter<>)))
+ baseT = baseT.BaseType;
+ if (baseT == typeof(object)) ConverterTargetType = null;
+ else ConverterTargetType = baseT.GetGenericArguments()[0];
+
+ var implInterface = ConverterType.GetInterfaces().Contains(typeof(IValueConverter));
+
+ if (ConverterTargetType == null && !implInterface) throw new ArgumentException("Type is not a value converter!");
+ }
+ }
+
+ ///
+ /// 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/Loader/Features/ConfigProviderFeature.cs b/IPA.Loader/Loader/Features/ConfigProviderFeature.cs
index caec1aad..95c4ff38 100644
--- a/IPA.Loader/Loader/Features/ConfigProviderFeature.cs
+++ b/IPA.Loader/Loader/Features/ConfigProviderFeature.cs
@@ -57,7 +57,7 @@ namespace IPA.Loader.Features
}
catch (Exception e)
{
- InvalidMessage = $"Error generated while creating delegate: {e}";
+ InvalidMessage = $"Error while registering config provider: {e}";
return false;
}
}
diff --git a/docs/articles/dev-resources/Plugin.cs b/docs/articles/dev-resources/Plugin.cs
index 1022758b..fc0e1f3e 100644
--- a/docs/articles/dev-resources/Plugin.cs
+++ b/docs/articles/dev-resources/Plugin.cs
@@ -37,6 +37,12 @@ namespace Demo
}
*/
+ [Init]
+ public void Init(Logger logger)
+ {
+ // logger will be the same instance as log currently is
+ }
+
/*
[OnEnable]
public void OnEnable()
diff --git a/docs/articles/dev-resources/PluginConfig.cs b/docs/articles/dev-resources/PluginConfig.cs
index e2821257..bdb7c616 100644
--- a/docs/articles/dev-resources/PluginConfig.cs
+++ b/docs/articles/dev-resources/PluginConfig.cs
@@ -15,11 +15,23 @@ namespace Demo
{
public static PluginConfig Instance { get; set; }
+ public class SubThingsObject
+ {
+ /*
+ public double DoubleValue { get; set; } = 2.718281828459045;
+ */
+
+ public virtual double DoubleValue { get; set; } = 2.718281828459045;
+ }
+
/*
public int IntValue { get; set; } = 42;
public float FloatValue { get; set; } = 3.14159f;
+ [NonNullable]
+ public SubThingsObject SubThings { get; set; } = new SubThingsObject();
+
[UseConverter(typeof(ListConverter))]
public List ListValue { get; set; } = new List();
@@ -31,6 +43,9 @@ namespace Demo
public virtual float FloatValue { get; set; } = 3.14159f;
+ [NonNullable]
+ public virtual SubThingsObject SubThings { get; set; } = new SubThingsObject();
+
[UseConverter(typeof(ListConverter))]
public virtual List ListValue { get; set; } = new List();
diff --git a/docs/articles/start-dev.md b/docs/articles/start-dev.md
index d615da9b..09b222b0 100644
--- a/docs/articles/start-dev.md
+++ b/docs/articles/start-dev.md
@@ -10,7 +10,7 @@ title: Making your own mod
What follows is a *very* barebones, and frankly not very useful plugin class, even as a starting point,
but it should be enough to give a decent idea of how to do quick upgrades of existing mods for those who want to.
-[!code-cs[Plugin.cs](./dev-resources/Plugin.cs?range=1-3,6-8,12-16,29-37,39,44-45,46-49,54-57,59-)]
+[!code-cs[Plugin.cs](./dev-resources/Plugin.cs?range=1-3,6-8,12-16,29-37,39,50-51,52-55,60-63,65-)]
There are basically 4 major concepts here:
@@ -20,6 +20,166 @@ There are basically 4 major concepts here:
used for initialization.
4. The lifecycle event attributes and .
-Read the docs at those links for a better idea of what they do.
+I reccommend you read the docs for each of those to get an idea for what they do.
-TODO: expand this to explain more, and expand on the base example
+It is worth noting that this example is of a mod that *cannot* be enabled and disabled at runtime, as marked by
+[RuntimeOptions.SingleStartInit](xref:IPA.RuntimeOptions.SingleStartInit).
+
+### What can be changed
+
+Before we go adding more functionality, its worth mentioning that that is not the *only* way to have a plugin set up.
+
+For starters, we can add another *method* marked `[Init]`, and it will be called after the constructor, with the same
+injected parameters, if those are applicable.
+
+[!code-cs[Plugin.cs#Init(Logger)](./dev-resources/Plugin.cs?range=40-44)]
+
+If you only had a method marked `[Init]`, and no constructors marked `[Init]`, then the plugin type must expose a
+public default constructor. If multiple constructors are marked `[Init]`, only the one with the most parameters will
+be called.
+
+You may also mark as many methods as you wish with `[Init]` and all of them will be called, in no well-defined order on
+initialization. The same is true for `[OnStart]` and `[OnExit]`, respectively.
+
+## From Scratch
+
+If you are starting from scratch, you will need one other thing to get your plugin up and running: a manifest.
+
+A basic manifest for that might look a little like this:
+
+[!code-json[manifest.json](./dev-resources/manifest.json?range=1,3,4,6-12,14-19,23-)]
+
+There is a lot going on there, but most of it should be decently obvious. Among the things that *aren't* immediately obvious,
+are
+
+- `id`: This represents a unique identifier for the mod, for use by package managers such as BeatMods. It may be null if the
+ mod chooses not to support those.
+- `features`: Don't worry about this for now, this is a not-very-simple thing that will be touched on later.
+
+In addition, there are a few gatchas with it:
+
+- `description`: This can be either a string or an array representing different lines. Markdown formatting is permitted.
+- `gameVersion`: This should match *exactly* with the application version of the game being targeted. While this is not enforced
+ by BSIPA, mod repositories like BeatMods may require it match, and it is good practice regardless.
+- `version`: This must be a valid SemVer version number for your mod.
+
+In order for your plugin to load, the manifest must be embedded into the plugin DLL as an embedded resource. This can be set in
+the Visual Studio file properties panel under `Build Action`, or in the `.csproj` like so:
+
+[!code-xml[Demo.csproj#manifest](./dev-resources/Demo.csproj?range=12-13,15)]
+
+At this point, if the main plugin source file and the manifest are in the same source location, and the plugin class is using the
+project's default namespace, the plugin will load just fine. However, this is somewhat difficult both to explain and verify, so I
+recommend you use the the `misc.plugin-hint` field in your manifest. It can be used like so:
+
+[!code-json[manifest.json#misc.plugin-hint](./dev-resources/manifest.json?range=20-22)]
+
+With this, you can set `plugin-hint` to the full typename of your plugin type, and it will correctly load. This is a hint though,
+and will also try it as a namespace if it fails to find the plugin type. If that fails, it will then fall back to using the manifest's
+embedded namespace.
+
+### A less painful description
+
+If you want to have a relatively long or well-formatted description for your mod, it may start to become painful to embed it in a list
+of JSON strings in the manifest. Luckily, there is a way to handle this.
+
+The first step is to create another embedded file, but this time it should be a Markdown file, perhaps `description.md`. It may contain
+something like this:
+
+[!code-markdown[description.md](./dev-resources/description.md)]
+
+Then, in your manifest description, have the first line be something look like this, but replacing `Demo.description.md` with the fully
+namespaced name of the resource:
+
+[!code-json[manifest.json#description](./dev-resources/manifest.json?range=5)]
+
+Now, when loaded into memory, if anything reads your description metadata, they get the content of that file instead of the content of the
+manifest key.
+
+### Configuring your plugin
+
+Something that many plugins want and need is configuration. Fortunately, BSIPA provides a fairly powerful configuration system out of the
+box. To start using it, first create a config class of some kind. Lets take a look at a fairly simple example of this:
+
+[!code-cs[PluginConfig.cs#basic](./dev-resources/PluginConfig.cs?range=9-10,12,15-17,28-30,69-)]
+
+Notice how the class is both marked `public` **and** is not marked `sealed`. For the moment, both of these are necessary. Also notice that
+all of the members are properties. While this doesn't change much now, it will be significant in the near future.
+
+Now, how do we get this object off of disk? Simple. Back in your plugin class, change your `[Init]` constructor to look like this:
+
+[!code-cs[Plugin.cs#config-init](./dev-resources/Plugin.cs?range=17-24,26)]
+
+For this to compile, though, we will need to add a few `using`s:
+
+[!code-cs[Plugin.cs#usings](./dev-resources/Plugin.cs?range=4,5)]
+
+With just this, you have your config automatically loading from disk! It's even reloaded when it gets changed mid-game! You can now access
+it from anywhere by simply accessing `PluginConfig.Instance`. Make sure you don't accidentally reassign this though, as then you will loose
+your only interaction with the user's preferences.
+
+By default, it will be named the same as is in your plugin's manifest's `name` field, and will use the built-in `json` provider. This means
+that the file that will be loaded from will be `UserData/Demo Plugin.json` for our demo plugin. You can, however, control both of those by
+applying attributes to the parameter, namely to control the name, and
+ to control the type. If the type preferences aren't registered though, it will just fall back to JSON.
+
+***
+
+But what about more complex types than just `int` and `float`? What if you want sub-objects?
+
+Those are supported natively, and so are very easy to set up. We just add this to the config class:
+
+[!code-cs[PluginConfig.cs#sub-basic](./dev-resources/PluginConfig.cs?range=18-19,21,25,31,33)]
+
+Now this object will be automatically read from disk too.
+
+But there is one caveat to this: because `SubThingsObject` is a reference type, *`SubThings` can be null*.
+
+This is often undesireable. The obvious solution may be to simply change it to a `struct`, but that is both not supported *and* potentially
+undesirable for other reasons we'll get to later.
+
+Instead, you can use . Change the definition of `SubThings` to this:
+
+[!code-cs[PluginConfig.cs#sub-basic-nonnull](./dev-resources/PluginConfig.cs?range=32-33)]
+
+And add this to the `using`s:
+
+[!code-cs[PluginConfig.cs#includes-attributes](./dev-resources/PluginConfig.cs?range=4)]
+
+This attribute tells the serializer that `null` is an invalid value for the config object. This does, however, require that *you* take extra care
+ensure that it never becomes null in code, as that will break the serializer.
+
+***
+
+What about collection types?
+
+Well, you can use those too, but you have to use something new: a converter.
+
+You may be familiar with them if you have used something like the popular Newtonsoft.Json library before. In BSIPA, they lie in the
+ namespace. All converters either implement or derive from
+. You will mostly use them with an .
+
+To use them, we'll want to import them:
+
+[!code-cs[PluginConfig.cs#includes-attributes](./dev-resources/PluginConfig.cs?range=1,3,5)]
+
+Then add a field, for example a list field:
+
+[!code-cs[PluginConfig.cs#list-basic](./dev-resources/PluginConfig.cs?range=35-36)]
+
+This uses a converter that is provided with BSIPA for s specifically. It converts the list to
+an ordered array, which is then written to disk as a JSON array.
+
+We could also potentially want use something like a . Lets start by looking at the definition
+for such a member, then deciphering what exactly it means:
+
+[!code-cs[PluginConfig.cs#set-basic](./dev-resources/PluginConfig.cs?range=38-39)]
+
+The converter we're using here is , a base type for converters of all kinds of
+collections. In fact, the is derived from this, and uses it for most of its implementation.
+If a type implements , can convert it.
+
+It, like most other BSIPA provided aggregate converters, provides a type argument overload
+to compose other converters with it to handle unusual element types.
+
+***