-
Notifications
You must be signed in to change notification settings - Fork 165
Writing Mod Organizer Plugins
Important: The Python section below is out-of-date for latest MO2 (2.3). If you intend to write a plugin, please come to the Discord server: https://discord.gg/cYwdcxj or visit the MO2 Plugin API Docs pages: https://www.modorganizer.org/python-plugins-doc/
Plugins are a way to extend Mod Organizers functionality. You can write plugins in either C++ or Python, theoretically with either being as powerful as the other. However, there's a possibility that a bug may stop a Python plugin working, and in such cases, the bug should be reported so a fix can be implemented. Creating a new plugin from scratch, especially as a user instead of a regular Mod Organizer developer, should be easier and faster in Python as there is no build system to set up. Python plugins should also keep working across all future Mod Organizer versions using the same API without modification, but C++ plugins may need recompiling.
Plugins are passive, that is: they react to events emitted by the core application or extend an existing functionality (like adding support for additional types of installers). Plugins should integrate with Mod Organizers concepts. While you could write an INI editor that works with the global INI files instead of the profile specific ones, what would be the point?
You will find more high-level information about the plugin interface (including what you can do with it) as well as a few samples in this document.
As you write actual plugins you will need API documentation.
This documentation exists in the form of fairly well documented (if I may say so myself) C++ header files in the uibase
project.
The python API is mostly identical to the C++ one, except that protected methods and attributes have an underscore (_
) prepended to their name with the same classes and functions.
Also (with one unfortunate exception) the interface name and the file name are always identical.
Do not be intimidated if you do not know C++ and want to write a python plugin. Almost everything is the same except for data types:
- All Qt data types are translated to their
PyQt5
equivalent, exceptQString
,QList
andQVariant
:-
QString
becomes regular pythonstr
s,QList
(and its derivatives, such as QStringList and QVariantList) regular pythonlist
s, andQVariant
are dynamically converted to the corresponding Python type (e.g., aQVariant
holding abool
value is converted to abool
in python).
-
- Custom types (including
enum
s) are exposed as python classes. - Standard C++ types are exposed as standard python types (all integer types become
int
,bool
remainsbool
, ...).
If in doubt about function arguments you receive, use python introspection to figure out what you get. With return you may have to be a bit more careful because the error message the core application reports regarding bad python plugins aren't always great.
The Python stubs for the mobase
package are available at https://github.com/ModOrganizer2/pystubs-generation.
MO2 uses CPython 3.x as an interpreter.
Depending on where/how the plugin integrates with MO you need to write a different type of plugin, in practice this means you need to implement a different interface. As mentioned above, plugins are passive: the plugin type decides how/when MO makes requests to/invokes your plugin. All plugins however gain access to MOs own plugin interface so all plugins get to make the same requests to MO.
Interface: iplugininstaller.h
, iplugininstallersimple.h
, iplugininstallercomplex.h
Examples: installer_bain, installer_bundle, installer_fomod, installer_ncc, installer_quick, installer_manual
An installer is invoked when the user tries to install a mod, either by double clicking in the download view or through the "Install Mod..." button or "Reinstall mod" from the mod lists context menu. There are actually two ways to write an installer, simple or complex:
- With simple installers, MO does the unpacking of the file but this works only with standard archive formats. The plugin can then select the files and folders that requires extraction, and where to extract them.
- Complex installers are more flexible but require a bit more work.
Interface: ipluginpreview.h
Examples: preview_base
These plugins add support for previewing files in the data pane. Right now all image formats supported by Qt are implemented (including .dds
) but no audio files and no 3d mesh formats.
WIP
Interface: ipluginmodpage.h
Examples: page_tesalliance
Mod Page plugins implement interfaces to modding communities where mods can be downloaded, checked for updates and so on. This interface is not finished and some of the bits that are don't actually get used. The goal is that the whole Nexus integration can be implemented through this interface and can then be removed from the core application. This is a task for the distant future, unless someone wants to volunteer.
WIP
Interface: iplugingame.h
Examples: game_oblivion, game_fallout3, game_falloutnv, game_skyrim, game_fallout4 etc.
These plugins (shall eventually) implement all the game specific features and further game plugins are able to add support for further games. The plugin is also responsible to help MO determine if (and where) the game is installed in the first place. Since supporting a game properly requires extensions in many places of the UI. To allow this without creating one huge plugin interface that involves every aspect of MO, game plugins expose a feature list. The list of possible features can be found in the "game_features" project and each feature can itself be considered a plugin interface. (documentation may follow. or not)
As an example for a game feature take BSA invalidation: If the game requires BSA invalidation it will implement this feature. Wherever the core can support bsa invalidation it will query whether the current game has this feature and if so query the implementation on specifics (like "How should the invalidation BSA be called" and "what's the right bsa version"). Of course, the goal is for feature interfaces to be as generic as possible without limiting usefulness.
Interface: iplugintool.h
Examples: tool_configurator, tool_inieditor, fnistool
This is the simplest of plugin interfaces. Such plugins simply place an icon inside the tools submenu and get invoked when the user clicks it. They are expected to have a user interface of some sort. These are almost like independent applications except they can access all Mod Organizer interfaces like querying and modifying the current profile, mod list, load order, use MO to install mods and so on. A tool plugin can (and should!) integrate its UI as a window inside MO and thus doesn't have to initialize a windows application itself.
Interface: ipluginproxy.h
Examples: plugin_python
Proxy Plugins expose the plugin api to foreign languages. This is what allows you to write plugins using python in the first place. The python proxy is easily the most complicated plugin and requires constant updating so if you're considering writing a Haskell plugin because that's your programming language of choice, I'm fairly certain learning python is easier than writing the haskell proxy. Just saying. And no, you can not write a proxy for a third language in Python, don't be silly.
Interface: iplugin.h
Examples: check_fnis, bsa_extractor, diagnose_basic, tool_inibakery
"Free" plugins implement none of the interfaces and thus initially don't integrate with MO at all. They are initialized by MO and get access to the MO interface. This makes sense if you only want to implement one of the extension interfaces (see below) or register handlers for events.
Any Plugin can implement any number of the following extension interfaces. Python-based plugins can treat these the same way as any other plugin interface as multiple inheritance works differently.
Interface: iplugindiagnose.h
Examples: diagnose_basic, installer_ncc, plugin_python, script_extender_plugin_checker
This interface lets the plugin report issues that are then listed in the "Problems" icon in the main window. If possible the plugin can also provide an automatic or guided fix to the problem. The diagnose_basic plugin does nothing but analyze the MO installation and report problems it discovers (like: "there are files in your overwrite directory") but usually a plugin will want to report issues relevant for its own operation. For example installer_ncc requires a specific version of .Net and will report a problem if it's not installed. This should always be the prefered way to communicate problems the user has to fix but should never be used for problems he can't fix (i.e. "this plugin doesn't work with this game"). An empty problem list should always be achievable.
Interface: ipluginfilemapper.h
Examples: tool_inibakery, game_gamebryo
This interface allows plugins to add virtual file (or directory) links to the virtual file system in addition to the mod files. Profile-local save games, ini-files and load-orders are all implemented this way in MO2.
Of course, a plugin doesn't only expose it's interface to MO, communication also goes the other way around (the plugin requesting information from and giving commands to the core).
Therefore MO exposes its own API to plugins.
Every plugin has to provide an "init" function and as a reward, it gets a reference to an IOrganizer
object (header is imoinfo.h). This object is the entry point to all further interfaces.
This way your plugin can also register to handle events.
Note: these interfaces do (should) not use Qt signals/slots as it would be fairly complicated to integrate with other programming languages.
The IOrganizer interface gives top-level information about the MO installation, current state (i.e. MO version, which profile is active), content of the virtual file system, ... This interface also allows a plugin to access its settings (which the user can configure in the MO settings dialog) and to store data persistently (wouldn't want each plugin to individually create a file to store data between invocations)
This is the same interface implemented by the game plugin that manages the active game. Therefore it may contain functions not useful for use in plugins. It's possible this will be replaced by a more compact interface in the future.
Interface to a mod (the items listed in the left pane of the MO interface). Right now this is mostly a write-only interface as it is mostly used to set up a mod (for example by a complex installer plugin)
This interface lets a plugin download files from the internet using the integrated download manager. Once the "Mod Page" plugin type is further along this interface should also allow downloading from such a mod page.
This interface allows the plugin to request information from a mod repository (which is basically a modding page like nexus, but it is conceivable a mod repository may be a repository without its own website, comparable to package repositories used by linux distributions). Please note that this interface is currently oriented solely to work with www.nexusmods.com so there is a good chance it may have to be changed to work with other repositories.
Gives access to the list of mods (their state depending on the active plugin).
Gives access to the current list of plugins (esps). Please note that since this is very specific to the gamebryo games so it will eventually have to change.
Let me guess, you first jumped down here before reading the rest, right? ;)
helloworld.h
#pragma once
#include <iplugintool.h>
class HelloWorld : public MOBase::IPluginTool
{
// need to call Q_OBJECT macro. This is required for the Qt moc preprocessor
// and will cause ugly compiler errors if missing
Q_OBJECT
// List all interfaces being implemented. Again: hard to diagnose if missing
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool)
// compiled Qt plugins require an id and json file for meta information
Q_PLUGIN_METADATA(IID "org.tannin.HelloWorld" FILE "helloworld.json")
public:
HelloWorld();
// IPlugin interface
virtual bool init(MOBase::IOrganizer *moInfo) override;
virtual QString name() const override;
virtual QString author() const override;
virtual QString description() const override;
virtual MOBase::VersionInfo version() const override;
virtual bool isActive() const override;
virtual QList<MOBase::PluginSetting> settings() const override;
// IPluginTool interface
virtual QString displayName() const override;
virtual QString tooltip() const override;
virtual QIcon icon() const override;
public slots:
virtual void display() const override;
};
helloworld.cpp
#include "helloworld.h"
#include <QtPlugin>
#include <QMessageBox>
using namespace MOBase;
HelloWorld::HelloWorld()
{
// constructor. Please note that this is called before MO is started up completely so
// you can not do anything that would interface with the main application here.
}
bool HelloWorld::init(IOrganizer *organizer)
{
// initialize the plugin. This happens after the main application is largely initialized, except
// for the virtual directory structure. That is loaded in parallel and may take a moment to be complete.
// usually you will want to save the "organizer" reference to a member variable because this is your
// gateway to most functions of MO
// return true if the plugin started correctly. If you return false this will be considered a "problem", but
// the main application does not do anything about it except for printing a warning.
// if you want to give more detailed information to the user, print your own warning or - much more convenient
// for the user - implement the IPluginDiagnose interface.
return true;
}
QString HelloWorld::name() const
{
// the "internal" name of your plugin. This is the name under which it will show up in the settings dialog
// do NOT make this localizable
return "Hello World";
}
QString HelloWorld::author() const
{
// your name
return "Tannin";
}
QString HelloWorld::description() const
{
// a description of your plugin. This should be short and descriptive
return tr("Gives a friendly greeting");
}
VersionInfo HelloWorld::version() const
{
// version of the plugin. Please ensure to update this with every release
return VersionInfo(1, 0, 0, VersionInfo::RELEASE_FINAL);
}
bool HelloWorld::isActive() const
{
// return true if the plugin is active. This allows you to disable the plugin in cases that don't constitute
// an error.
// For example if you write a plugin that works only with Skyrim you can return false here if the active game
// is not skyrim.
return true;
}
QList<PluginSetting> HelloWorld::settings() const
{
// return a list of user-configurable settings for this plugins to be exposed through the settings dialog of MO.
// you can access the values for these settings through IOrganizer::pluginSetting
return QList<PluginSetting>();
}
QString HelloWorld::displayName() const
{
// the name of this tool as displayed in the toolbar
return tr("Hello World");
}
QString HelloWorld::tooltip() const
{
// tooltip for the toolbar icon
return tr("Gives a friendly greeting");
}
QIcon HelloWorld::icon() const
{
// icon in the toolbar. You should manage this icon (and all other graphics/assets you need) in a resource file
// that gets included in the dll so you don't have to ship multiple files.
// Please check the other plugins to see how this works.
return QIcon();
}
void HelloWorld::display() const
{
// display gets called when the user activates your plugin.
// This is basically the main entry point of your tool plugin.
// You can always use parentWidget() to refer to the main window.
QMessageBox::information(parentWidget(), tr("Hello"), tr("Hello World"));
}
// again a Qt thing. The first parameter is the plugin name, the second the class name.
// usually the two will be the same except for casing
Q_EXPORT_PLUGIN2(helloWorld, HelloWorld)
As you can see we use tr() around strings that need to be localized. Qt tools can then be used to extract those strings, send them to translators and automatically get the translations applied if a translation in the selected Language exists.
helloworld.json
{}
As you see you don't actually need to specify any meta data for the plugin and the file does not actually need to be there.
The documentation for Python plugin is available at https://www.modorganizer.org/python-plugins-doc.