From 852f168bfd848b3000981cc873a837ac377cc824 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Tue, 7 Jan 2020 22:00:51 -0600 Subject: [PATCH] Added more to the start-dev documentation page --- IPA.Loader/Config/Stores/Attributes.cs | 242 +++++++++--------- .../Loader/Features/ConfigProviderFeature.cs | 2 +- docs/articles/dev-resources/Plugin.cs | 6 + docs/articles/dev-resources/PluginConfig.cs | 15 ++ docs/articles/start-dev.md | 166 +++++++++++- 5 files changed, 306 insertions(+), 125 deletions(-) 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. + +***