Browse Source

Added more to the start-dev documentation page

pull/46/head
Anairkoen Schno 4 years ago
parent
commit
eb905e93ad
5 changed files with 306 additions and 125 deletions
  1. +121
    -121
      IPA.Loader/Config/Stores/Attributes.cs
  2. +1
    -1
      IPA.Loader/Loader/Features/ConfigProviderFeature.cs
  3. +6
    -0
      docs/articles/dev-resources/Plugin.cs
  4. +15
    -0
      docs/articles/dev-resources/PluginConfig.cs
  5. +163
    -3
      docs/articles/start-dev.md

+ 121
- 121
IPA.Loader/Config/Stores/Attributes.cs View File

@ -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
{
/// <summary>
/// Causes a field or property in an object being wrapped by <see cref="GeneratedExtension.Generated{T}(Config, bool)"/> to be
/// ignored during serialization and deserialization.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class IgnoreAttribute : Attribute { }
/// <summary>
/// Indicates that a field or property in an object being wrapped by <see cref="GeneratedExtension.Generated{T}(Config, bool)"/>
/// that would otherwise be nullable (i.e. a reference type or a <see cref="Nullable{T}"/> type) should never be null, and the
/// member will be ignored if the deserialized value is <see langword="null"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class NonNullableAttribute : Attribute { }
/// <summary>
/// Indicates that a given field or property in an object being wrapped by <see cref="GeneratedExtension.Generated{T}(Config, bool)"/>
/// should be serialized and deserialized using the provided converter instead of the default mechanism.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class UseConverterAttribute : Attribute
{
/// <summary>
/// Gets the type of the converter to use.
/// </summary>
public Type ConverterType { get; }
/// <summary>
/// Gets the target type of the converter if it is avaliable at instantiation time, otherwise
/// <see langword="null"/>.
/// </summary>
public Type ConverterTargetType { get; }
/// <summary>
/// Gets whether or not this converter is a generic <see cref="ValueConverter{T}"/>.
/// </summary>
public bool IsGenericConverter => ConverterTargetType != null;
/// <summary>
/// Creates a new <see cref="UseConverterAttribute"/> with a given <see cref="ConverterType"/>.
/// </summary>
/// <param name="converterType">tpy type to assign to <see cref="ConverterType"/></param>
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!");
}
}
/// <summary>
/// Specifies a name for the serialized field or property in an object being wrapped by
/// <see cref="GeneratedExtension.Generated{T}(Config, bool)"/> that is different from the member name itself.
/// </summary>
/// <example>
/// <para>
/// When serializing the following object, we might get the JSON that follows.
/// <code>
/// public class PluginConfig
/// {
/// public virtual bool BooleanField { get; set; } = true;
/// }
/// </code>
/// <code>
/// {
/// "BooleanField": true
/// }
/// </code>
/// </para>
/// <para>
/// However, if we were to add a <see cref="SerializedNameAttribute"/> to that field, we would get the following.
/// <code>
/// public class PluginConfig
/// {
/// [SerializedName("bool")]
/// public virtual bool BooleanField { get; set; } = true;
/// }
/// </code>
/// <code>
/// {
/// "bool": true
/// }
/// </code>
/// </para>
/// </example>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class SerializedNameAttribute : Attribute
{
/// <summary>
/// Gets the name to replace the member name with.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Creates a new <see cref="SerializedNameAttribute"/> with the given <see cref="Name"/>.
/// </summary>
/// <param name="name">the value to assign to <see cref="Name"/></param>
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
{
/// <summary>
/// Causes a field or property in an object being wrapped by <see cref="GeneratedExtension.Generated{T}(Config, bool)"/> to be
/// ignored during serialization and deserialization.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class IgnoreAttribute : Attribute { }
/// <summary>
/// Indicates that a field or property in an object being wrapped by <see cref="GeneratedExtension.Generated{T}(Config, bool)"/>
/// that would otherwise be nullable (i.e. a reference type or a <see cref="Nullable{T}"/> type) should never be null, and the
/// member will be ignored if the deserialized value is <see langword="null"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class NonNullableAttribute : Attribute { }
/// <summary>
/// Indicates that a given field or property in an object being wrapped by <see cref="GeneratedExtension.Generated{T}(Config, bool)"/>
/// should be serialized and deserialized using the provided converter instead of the default mechanism.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class UseConverterAttribute : Attribute
{
/// <summary>
/// Gets the type of the converter to use.
/// </summary>
public Type ConverterType { get; }
/// <summary>
/// Gets the target type of the converter if it is avaliable at instantiation time, otherwise
/// <see langword="null"/>.
/// </summary>
public Type ConverterTargetType { get; }
/// <summary>
/// Gets whether or not this converter is a generic <see cref="ValueConverter{T}"/>.
/// </summary>
public bool IsGenericConverter => ConverterTargetType != null;
/// <summary>
/// Creates a new <see cref="UseConverterAttribute"/> with a given <see cref="ConverterType"/>.
/// </summary>
/// <param name="converterType">the type to assign to <see cref="ConverterType"/></param>
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!");
}
}
/// <summary>
/// Specifies a name for the serialized field or property in an object being wrapped by
/// <see cref="GeneratedExtension.Generated{T}(Config, bool)"/> that is different from the member name itself.
/// </summary>
/// <example>
/// <para>
/// When serializing the following object, we might get the JSON that follows.
/// <code>
/// public class PluginConfig
/// {
/// public virtual bool BooleanField { get; set; } = true;
/// }
/// </code>
/// <code>
/// {
/// "BooleanField": true
/// }
/// </code>
/// </para>
/// <para>
/// However, if we were to add a <see cref="SerializedNameAttribute"/> to that field, we would get the following.
/// <code>
/// public class PluginConfig
/// {
/// [SerializedName("bool")]
/// public virtual bool BooleanField { get; set; } = true;
/// }
/// </code>
/// <code>
/// {
/// "bool": true
/// }
/// </code>
/// </para>
/// </example>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class SerializedNameAttribute : Attribute
{
/// <summary>
/// Gets the name to replace the member name with.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Creates a new <see cref="SerializedNameAttribute"/> with the given <see cref="Name"/>.
/// </summary>
/// <param name="name">the value to assign to <see cref="Name"/></param>
public SerializedNameAttribute(string name)
{
Name = name;
}
}
}

+ 1
- 1
IPA.Loader/Loader/Features/ConfigProviderFeature.cs View File

@ -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;
}
}


+ 6
- 0
docs/articles/dev-resources/Plugin.cs View File

@ -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()


+ 15
- 0
docs/articles/dev-resources/PluginConfig.cs View File

@ -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<string>))]
public List<string> ListValue { get; set; } = new List<string>();
@ -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<string>))]
public virtual List<string> ListValue { get; set; } = new List<string>();


+ 163
- 3
docs/articles/start-dev.md View File

@ -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 <xref:IPA.OnStartAttribute> and <xref:IPA.OnExitAttribute>.
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 <xref:IPA.Config.Config> parameter, namely <xref:IPA.Config.Config.NameAttribute> to control the name, and
<xref:IPA.Config.Config.PreferAttribute> 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 <xref:IPA.Config.Stores.Attributes.NonNullableAttribute>. 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
<xref:IPA.Config.Stores.Converters> namespace. All converters either implement <xref:IPA.Config.Stores.IValueConverter> or derive from
<xref:IPA.Config.Stores.ValueConverter`1>. You will mostly use them with an <xref:IPA.Config.Stores.Attributes.UseConverterAttribute>.
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 <xref:System.Collections.Generic.List`1>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 <xref:System.Collections.Generic.HashSet`1>. 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 <xref:IPA.Config.Stores.Converters.CollectionConverter`2>, a base type for converters of all kinds of
collections. In fact, the <xref:IPA.Config.Stores.Converters.ListConverter`1> is derived from this, and uses it for most of its implementation.
If a type implements <xref:System.Collections.Generic.ICollection`1>, <xref:IPA.Config.Stores.Converters.CollectionConverter`2> can convert it.
It, like most other BSIPA provided aggregate converters, provides a type argument overload <xref:IPA.Config.Stores.Converters.CollectionConverter`3>
to compose other converters with it to handle unusual element types.
***

Loading…
Cancel
Save