-
Notifications
You must be signed in to change notification settings - Fork 60
Plugins
Kuriimu2 utilizes DLLs as plugins to extend its file format support. Those DLLs are written in any .NET compatible language, but preferably C#, and loaded at runtime using the System.Reflection namespace.
To prepare a plugin project in Visual Studio Community 2019, first you must set up a custom nuget package source.
- Go to
Tools > NuGet Package Manager > Package Manager Settings > Package Sources
. - Add a new package source called "Kuriimu2".
- 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.
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 programs.
A file plugin consists of at least two classes, each of which have their own purpose in our framework.
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
{
// 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("Formatname", "Pluginauthor", "Short description");
// Allows an in-depth 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,
ITemporaryStreamProvider temporaryStreamProvider)
{
var fileStream = await fileSystem.OpenFileAsync(filePath);
using var br = new BinaryReaderX(fileStream);
return br.ReadString(5) == "magic";
}
// Creates the plugin state, which implements all the format specific actions and properties
public IPluginState CreatePluginState(IPluginManager pluginManager)
{
return new ExampleState();
}
}
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 ITemporaryStreamProvider
is retrieved to open temporary files. This allows for storing data on the disk instead in memory. After a temporary file is closed it will get deleted from the disk again automatically.
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 Guid. This allows a plugin to utilize already existing ones to be incorporated into the loading or saving process.
The state class executes the main actions and provides the type-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 formats contents.
Optionally it can also implement any type-specific interface to add more functionality to a certain type of state.
public class ExampleArchiveState : IArchiveState, ILoadFiles, ISaveFiles
{
// Exposes the loaded files from the archive format to any consuming user interface
public IReadOnlyList<ArchiveFileInfo> Files { get; private set; }
// Indicates if the contents of the format were changed by the user interface
public bool ContentChanged { get; set; }
public async Task Load(IFileSystem fileSystem, UPath filePath, ITemporaryStreamProvider temporaryStreamProvider,
IProgressContext progress)
{
var input = await fileSystem.OpenFileAsync(filePath);
// ... Load archive files from input into 'Files' property
}
public Task Save(IFileSystem fileSystem, UPath savePath, IProgressContext progress)
{
var output = fileSystem.OpenFile(savePath, FileMode.Create);
// ... Save archive files to output
return Task.CompletedTask;
}
}
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 type-specific actions to be implemented on an opt-in basis.
See Plugin States for more detailed information on plugin states and state-specific interfaces.
This interface implements an asynchronous method Load
to have the plugin set the type-specific properties.
Like IdentifyAsync
, this method retrieves the parent file system, the path to the file to load, and an ITemporaryStreamProvider
for the same purpose explained at IIdentifyFiles.
Additionally it retrieves an IProgressContext
, which may communicate progress of the loading process to the user interface.
This interface implements an asynchronous method Save
to have the plugin save the contents of the type-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 an IProgressContext
, which may communicate progress of the saving process to the user interface.
Implementing a basic file plugin for Kuriimu2 is just combining different interfaces depending on its needs.
The general implementation order for plugin interfaces goes as follows:
- Plugin State:
IArchiveState
,IImageState
, etc. - File Capability:
ILoadFiles
,ISaveFiles
, etc. - Type Functionality:
IAddEntries
,IImportImages
,IAddFiles
, etc.
This is for style, readability, and consistency between plugins.
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.
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 Guid, or after the automatic plugin selection of the plugin manager couldn't resolve a fitting plugin for the given file.
TODO: Add a project template for developers