-
Notifications
You must be signed in to change notification settings - Fork 161
Writing Mod Organizer Plugins
Plugins are a way to extend Mod Organizers functionality. You can write plugins in either C++ or Python, although some functionality will not be available from Python.
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 exactly identical (except where explicitly noted) with the same classes and functions. Also (with one unfortunate exception) the interface name and the file name are always identical.
Don't be intimidated if you don't 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 (except QString and QList)
- QString becomes a regular python str, QList is a regular list on the python side
- Custom types (including enums) are exposed as python classes
- standard C++ types are exposed as standard python types (all integer types become int, bool remains bool, ...)
If in doubt about function arguments you receive, use python introspection to figure out what you get. With return you have to be a bit more careful because the error message the core application reports regarding bad python plugins aren't great.
MO uses cpython 2.7 as an interpreter.
Btw.: The mo interface module (called mobase) is already imported in the context. Everything else you should import yourself.
Depending on where/how the plugin integrates with MO you need to write a different type of plugin. In code this means you need to implement a different interface. As I 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 and only in C++. Complex installers are more flexible but 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. The goal is that the whole Nexus integration can be implemented through this interface and can then be removed from the core application.
WIP
Interface: iplugingame.h
Examples: game_oblivion, game_fallout3, game_falloutnv, game_skyrim, game_fallout4
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 usefuleness.
Interface: iplugintool.h
Examples: tool_configurator, tool_inieditor
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.
Interface: iplugindiagnose.h
Examples: diagnose_basic, installer_ncc, plugin_python
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 provied an "init" function and as a reward it gets a reference to an IOrganizer uobject (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 but the file needs to be there.
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QMessageBox
class HelloWorld(mobase.IPluginTool):
def __init__(self):
super(HelloWorld, self).__init__()
self.__parentWidget = None
def init(self, organizer):
return True
def name(self):
return "Hello World"
def author(self):
return "Tannin"
def description(self):
return self.__tr("Gives a friendly greeting")
def version(self):
return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final)
def isActive(self):
return True
def settings(self):
return []
def displayName(self):
return self.__tr("Hello World")
def tooltip(self):
return self.__tr("Gives a friendly greeting")
def icon(self):
return QIcon()
def setParentWidget(self, widget):
self.__parentWidget = widget
def display(self):
QMessageBox.information(self.__parentWidget, self.__tr("Hello"), self.__tr("Hello World"))
def __tr(self, str):
return QCoreApplication.translate("HelloWorld", str)
def createPlugin():
return HelloWorld()
You will notice that the code is largely the same as in C++: you create a class that inherits from IPluginTool, then implement its functions. The comments from the C++ code apply all the same. The main things to notice:
- the python needs none of the Qt plugin boiler plate code
- Otoh we declare our own __tr function. This is because the C++ class inheritance (IPluginTool inherits from QObject and thus has a tr function) is not carried over into python. This may be fixed in the future.
- enums are named slightly differently in python
- Unlike the C++ variant you need to implement setParentWidget. The code will always be the same: Just store the pointer for later use.
- Do implement ALL functions from the plugin interface. Failing to do so will likely crash MO.
- You need to provide a factory function called "createPlugin", with the shown signature.