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) andpgsql:. - 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'ssharedlibrary instead ofgame, so authserver and dbimport can load it without the game code.LINK: the client library.RUNTIME_FILES: the DLLs the client library needs (libmysql.dlland 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
- 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 inlonelyice.iniunder[remote], with[server] locationset to the storage id (see lonelyice.ini). - 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 inAC_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. - 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).
- On every server start the launcher passes, as environment overrides,
<id>:host;port;user;password;<prefix><name>forLoginDatabaseInfo,CharacterDatabaseInfo,WorldDatabaseInfoandPlayerbotsDatabaseInfo(auth,characters,world,playerbots), plus thestorage.configvalues.
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
- Install the plugin, choose its storage in Settings → Storage and watch the check result.
- 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.
- Run the plugins you care about on it; their SQL goes through your backend as well.
- Read the log for
sql.driverandsql.updatesmessages.