Skip to content

Writing Mod Organizer Plugins

Al edited this page Dec 22, 2021 · 6 revisions

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/

Writing Mod Organizer Plugins

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.

General

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?

Technical Documentation

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.

Python specific

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, except QString, QList and QVariant:
    • QString becomes regular python strs, QList (and its derivatives, such as QStringList and QVariantList) regular python lists, and QVariant are dynamically converted to the corresponding Python type (e.g., a QVariant holding a bool value is converted to a bool in python).
  • 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 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.

Types of plugins

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.

Installers

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.

Previewer

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.

Mod Page

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.

Game

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.

Tool

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.

Proxies

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.

Free Plugins

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.

Extension Interfaces

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.

Diagnose

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.

File Mappings (only in MO2)

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.

Mod Organizer Interfaces

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.

IOrganizer (in imoinfo.h)

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)

IPluginGame

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.

IModInterface

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)

IDownloadManager

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.

IModRepositoryBridge

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.

IModList

Gives access to the list of mods (their state depending on the active plugin).

IPluginList

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.

Examples

Let me guess, you first jumped down here before reading the rest, right? ;)

Minimal (C++)

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.

Minimal (Python)

The documentation for Python plugin is available at https://www.modorganizer.org/python-plugins-doc.