Skip to content

Plugins

realonepiecefreak edited this page Dec 5, 2021 · 26 revisions

Plugins

  1. Description
  2. How-To: Setup Plugin Development
  3. How-To: File Plugin
  4. Best practices

Description

Kuriimu 2 utilizes plugins to extend its overall capabilities and are written in any .NET compatible language, preferably C#.

How-To: Setup Plugin Development

To prepare a plugin project in Visual Studio Community 2019, first you must set up a custom nuget package source.

  1. Go to Tools > NuGet Package Manager > Package Manager Settings > Package Sources.
  2. Add a new package source called "Kuriimu2".
  3. Set the source directory to "[Kuriimu2RepoDir]\nuget".

If you have not yet build the necessary nuget packages, just build Kuriimu2.sln and the available nuget packages will be build into "[Kuriimu2RepoDir]\nuget".

Then when creating your plugin, simply install the needed nuget packages. You will always need at least Kontract and then most likely Komponent if you're going to be dealing with files.

How-To: File Plugin

The first type of plugin is the widely used file-based one. It is a plugin that loads a given file and interprets it into a more general and/or readable structure for either the UI or other software.

A file plugin consists of at least two classes, each of which have their own purpose in our framework.

Framework Handling

A plugin which implements IIdentifyFiles is called an "Identifiable Plugin". Otherwise it is a "Blind Plugin".

In our framework a file will get passed through every identifiable plugin first. The framework will use the first plugin which returned a successful result from the identification to load the file.

Should no identifiable plugin be able to identify the given file, our framework offers the possibility of manual selection of a blind plugin by the user. Every user interface may offer a dialog to choose such a blind plugin in that situation.

If there are no blind plugins or the user cancels the selection, our framework will open the file in a generic hex plugin, built into it. This plugin simply allows access to the file as a Stream and has no further capabilities.

Our framework currently has no hot-plug capability. This means that every plugin is loaded into the runtime when a plugin manager is created. Each user interface has one plugin manager, which is created at the start up of the user interface.

Plugin Class

The plugin class is the main entry point into a file plugin. It exposes meta data to the file format it supports, allows for a deterministic identification of a given file, and creates the plugin state.

To represent a file format, the plugin class has to inherit from IFilePlugin.
To allow deterministic identification of a file, the plugin class has to additionally inherit from IIdentifyFiles.

public class ExamplePlugin : IFilePlugin, IIdentifyFiles
{
    // The type of files this plugin represents
    public PluginType PluginType => PluginType.[Type];

    // This is the unique plugin id, with which a plugin can be identified in the framework
    public Guid PluginId => Guid.Parse("a-valid-Guid-here");

    // These file extensions allow for an additional identification of the format
    public string[] FileExtensions => new[] { "*.ext" };

    // Additional plugin meta data
    public PluginMetadata Metadata => new PluginMetadata("Format name", "Plugin author", "Short description");

    // Allows an identification of one or more files of the parent file system to identify
    // if the given file is the supported file format.
    public async Task<bool> IdentifyAsync(IFileSystem fileSystem, UPath filePath, IdentifyContext context)
    {
        var fileStream = await fileSystem.OpenFileAsync(filePath);
        using var br = new BinaryReaderX(fileStream);

        return br.ReadString(4) == "MGCK";
    }

    // Creates the plugin state, which implements all the format specific actions and properties
    public IPluginState CreatePluginState(IBaseFileManager pluginManager)
    {
        return new ExampleState();
    }
}

IIdentifyFiles

The method IdentifyAsync allows for an asynchronous identification of the files contents, if the plugin can handle this file.
It retrieves the parent file system (either the folder from the disk or another archive plugin's contents) and the file path into this file system to the requested file. This allows for a file format consisting of multiple files to also be identifiable from within other archives.
Additionally an IdentifyContext is retrieved, which grants access to progress, logging, and more.

IFilePlugin

This interface implements all the necessary metadata properties, as well as the CreateState method.
This method retrieves an IPluginManager which is capable of opening and saving files either by our automatic identification or by a given PluginId. This allows a plugin to utilize already existing ones to be incorporated into the loading or saving process.

State class

The state class executes the main actions and provides the format-specific data for a file format.

A state class may at least implement any given IPluginState, like IArchiveState, and ILoadFiles. Optionally it can implement ISaveFiles to add the possibility of saving changes to the files contents. Optionally it can also implement any state-specific interface to add more functionality.

public class ExampleArchiveState : IArchiveState, ILoadFiles, ISaveFiles
{
    // Exposes the loaded files from the archive format to any consuming user interface
    public IList<IArchiveFileInfo> Files { get; private set; }

    // Indicates if the contents of the format were changed
    public bool ContentChanged { get; private set; }

    public async Task Load(IFileSystem fileSystem, UPath filePath, LoadContext context)
    {
        var input = await fileSystem.OpenFileAsync(filePath);

        // ... Load archive files from input into 'Files' property
    }

    public Task Save(IFileSystem fileSystem, UPath savePath, SaveContext context)
    {
        var output = fileSystem.OpenFile(savePath, FileMode.Create);

        // ... Save archive files to output

        return Task.CompletedTask;
    }
}

Plugin states

A plugin state implements properties of a certain type of file format.
Additionally to a plugin state, there are also state-specific interfaces, which allow format-specific actions to be implemented on an opt-in basis. See Plugin States for more detailed information on plugin states and state-specific interfaces.

ILoadFiles

This interface implements an asynchronous method Load to have the plugin set the state-specific properties.
Like IdentifyAsync, this method retrieves the parent file system, the path to the file to load, and a LoadContext for the same purpose explained at IIdentifyFiles.

ISaveFiles

This interface implements an asynchronous method Save to have the plugin save the contents of the state-specific properties back to a file.
This method retrieves the file system and file path to save the file at. Additional files may be created in the same file system.
Additionally it retrieves a SaveContext, whoch may have the same task as the LoadContext.

Best practices

  1. Implementation Order
  2. Supported features only
  3. Use a project template

Implementation Order

Implementing a basic file plugin for Kuriimu 2 is just combining different interfaces depending on its needs.

For style, readability, and consistency between plugins, the following implementation order of interfaces is recommended:

  • Plugin State: IArchiveState, IImageState, etc.
  • File Capability: ILoadFiles, ISaveFiles, etc.
  • Type Functionality: IAddEntries, IImportImages, IAddFiles, etc.

You should normally only implement a single IPluginState per plugin definition. This helps keep the plugins purpose specific without bloating Load and Save with tons of type checking when two separate plugins would be much cleaner.

Supported features only

You should only include the interfaces that your plugin can properly support. An example of incorrect usage would be to implement IIdentifyFiles but then have the IdentifyAsync method do a poor job where it returns false-positives very often for files that the plugin doesn't actually deal with.
If one feature can not be properly supported, the user interface and framework will take care to not make this action possible for the user. So don't be afraid to leave features out!

A special case are plugins which do not implement IIdentifyFiles. Those are referred to as "Blind Plugins" and a user can still use such a plugin to load a file. Either by opening a file directly via the plugin manager and giving it the plugins ID, or after the automatic plugin selection of the plugin manager couldn't resolve a fitting plugin for the given file.

Use a project template

TODO: Add a project template for developers

Clone this wiki locally