Guides › Development

Storage plugins

What a storage plugin is

The core has no database driver except SQLite. Other engines come from plugins: a plugin library registers a backend driver with the core as soon as it loads, and its manifest offers the backend to the launcher as a storage, listed next to the built-in files in the setup wizard and in Settings → Storage.

lonelyice.mysql (repository mod-lonelyice-mysql) is the working example; this guide follows it. The interfaces themselves are described in the plugin API.

Limits

Read these before starting:

  • The backends are a fixed enum in the core: DatabaseBackend::MySQL, SQLite, PostgreSQL (DatabaseBackend.h). A plugin can register or replace the driver of one of them; it cannot add a new engine.
  • Connection strings are parsed by the core. It knows the schemes sqlite:, mysql: (or no scheme) and pgsql:.
  • All SQL above the driver is MySQL dialect. The core translates it only for SQLite; for MySQL and PostgreSQL the dialect passes it through unchanged. A PostgreSQL driver would therefore receive MySQL SQL: it is not usable without work in the core.

In practice, a storage plugin today is a MySQL-compatible backend, or a replacement of the MySQL driver.

1. Implement the backend

A backend is a class implementing IDbConnectionBackend (IDbConnectionBackend.h), one object per physical connection:

Member Purpose
Open(bool create), Close(), Reconnect(), Ping() Connection life. Open reports DbErrorClass::DatabaseMissing for a missing database, so the core can create it.
Prepare, Execute, Query Prepared statements and plain SQL. Query returns an empty RowSet when no rows matched and nullptr only on error.
Begin, Commit, Rollback Transactions.
HasAnyTable, TableExists, ListColumns Schema questions the updater asks.
CreateScriptTarget() The IScriptTarget through which the updater applies .sql files on this connection.
ServerInfo(), Backend() Description for the log, and which DatabaseBackend this is.

SQL handed to these methods has already been translated for the backend. The MySQL plugin's script target returns true from RunsScriptsAsWritten(): the files are MySQL already, so every statement is sent as written and no mysql program is started.

2. Register it when the library loads

plugin/plugin.cpp of lonelyice.mysql:

#include "IDbConnectionBackend.h"
#include "MySQLBackend.h"
#include "PluginApi.h"

namespace
{
    // Registered as soon as the library loads, in every program that opens the databases (server.apps).
    void AddMySQLBackend()
    {
        DbBackendDriver driver;
        driver.create = &CreateMySQLBackend;
        driver.caps = { 0, true, true };
        driver.init = &MySQLLibrary::Init;
        driver.end = &MySQLLibrary::End;
        driver.version = &MySQLLibrary::Version;
        RegisterBackendDriver(DatabaseBackend::MySQL, driver);
    }
}

AC_PLUGIN_ON_LOAD(AddMySQLBackend)
DbBackendDriver field Meaning
create Factory: std::unique_ptr<IDbConnectionBackend> (*)(DatabaseConnectionInfo const&).
caps DbBackendCaps { maxAsyncWorkers, needsKeepAlive, supportsReconnect }; 0 workers means unlimited.
init, end Called once before the first connection and at shutdown (for example the client library's init).
version Text for the log, such as MySQL 8.0.36.

AC_PLUGIN_ON_LOAD runs the function right after the library is loaded, before any database is opened, in every program that loads the plugin. The log shows Database backend 'mysql' registered. The driver holds plain function pointers only.

DatabaseConnectionInfo carries what the connection string held: backend, host, port_or_socket, user, password, database, ssl.

3. Build it against shared

AddPlugin(mod-lonelyice-mysql
  SOURCES
    plugin/plugin.cpp
    src/MySQLBackend.cpp
    src/MySQLScriptTarget.cpp
    src/MySQLStatement.cpp
  CORE
    shared
  LINK
    "${MYSQL_PLUGIN_LIBRARY}"
  RUNTIME_FILES
    ${MYSQL_PLUGIN_RUNTIME})
target_include_directories(mod-lonelyice-mysql PRIVATE "${MYSQL_PLUGIN_INCLUDE_DIR}")
  • CORE shared: the library links against the core's shared library instead of game, so authserver and dbimport can load it without the game code.
  • LINK: the client library.
  • RUNTIME_FILES: the DLLs the client library needs (libmysql.dll and the OpenSSL it loads on Windows). They are copied next to the plugin library; the loader adds the plugin's folder to the DLL search path.

The plugin's CMakeLists.txt returns early when MySQL is not found, so a build without it simply has no MySQL plugin.

4. Write the manifest

{
  "format": 1,
  "id": "lonelyice.mysql",
  "version": "1.2.0",
  "name": { "en": "MySQL database", "ru": "База данных MySQL" },
  "core": { "abi": "lonelyice-ac-2" },
  "platforms": [ "windows-x64" ],
  "provides": [ "database:mysql" ],
  "storage": {
    "id": "mysql",
    "name": { "en": "MySQL server", "ru": "Сервер MySQL" },
    "port": 3306
  },
  "server": { "library": "lonelyice_mysql", "apps": [ "worldserver", "authserver", "dbimport" ] }
}
Field Meaning
server.apps Every program that opens the databases must load the backend. The LonelyIce server process loads plugins made for worldserver or authserver.
provides Informational: database:<scheme>.
storage.id The connection string scheme the launcher writes: mysql or pgsql (see Limits). local is reserved.
storage.name Localized name in the storage list.
storage.port Default port shown in the form.
storage.config Optional config values the server needs with this storage, passed as AC_* overrides. {bin} is replaced by the plugin's server/<platform> folder, {exe} by .exe on Windows and nothing elsewhere.

The format reference is storage.

What the launcher does with it

  1. The storage list (wizard's Data step, Settings → Storage) shows the storages of the enabled plugins in plugins/. The form asks for server, port, user, password and a database prefix, kept in lonelyice.ini under [remote], with [server] location set to the storage id (see lonelyice.ini).
  2. When the fields stop changing, the launcher checks the storage by running the core itself: LonelyIce --server --storage-check -c <config> with the connection strings in the environment and the plugins folder in AC_PLUGINS_DIR. Your backend is loaded like on a real start, so the check tests your code. See Server game data for the report lines.
  3. On Apply, missing databases are created, existing ones brought up to date and the client's tables unpacked into the world database (a database server has no virtual DBC tables).
  4. On every server start the launcher passes, as environment overrides, <id>:host;port;user;password;<prefix><name> for LoginDatabaseInfo, CharacterDatabaseInfo, WorldDatabaseInfo and PlayerbotsDatabaseInfo (auth, characters, world, playerbots), plus the storage.config values.

If the configured storage's plugin is missing or disabled, the launcher refuses to start the server and says which plugin it needs. The launcher's backups work on the built-in files only and are off with a database server.

Without LonelyIce

The plugin works in a plain server of the fork: put its folder into PluginsDir. worldserver, authserver and dbimport load it and then accept connection strings with its scheme in LoginDatabaseInfo, WorldDatabaseInfo and CharacterDatabaseInfo, for example mysql:127.0.0.1;3306;acore;acore;acore_world.

Testing

  1. Install the plugin, choose its storage in Settings → Storage and watch the check result.
  2. Start the server with an empty database prefix on a fresh database server: the core must create and fill all databases through your script target.
  3. Run the plugins you care about on it; their SQL goes through your backend as well.
  4. Read the log for sql.driver and sql.updates messages.