-
Notifications
You must be signed in to change notification settings - Fork 60
Plugins
Kuriimu 2 utilizes plugins to extend its overall capabilities and are written in any .NET compatible language, preferably C#.
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 software.
A file plugin consists of at least two classes, each of which have their own purpose in our framework.
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.
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();
}
}
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.
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.
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;
}
}
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.
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.
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
.
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.
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.
TODO: Add a project template for developers