Wrapping an AzerothCore module
The idea
Most AzerothCore modules build as plugins unchanged. Instead of forking a module, you write a small repository,
<module>-plugin, that says how to build it, the way a distribution's package spec builds a program from its original
sources:
mod-transmog-plugin/
plugin.json manifest; "source" pins the module's repository and commit
CMakeLists.txt fetches the module, applies patches/, calls AddPlugin, lays out conf/ and data/
plugin/plugin.cpp the entry point: the module's own script loader
settings.json the module's main options in the launcher's Settings
patches/*.patch changes the module needs as a plugin, if any
icon.png 64x64, shown by the launcher and the catalog
LICENSE the module's license
README.md
The module's own files are never copied into your repository. The wrappers LonelyIce ships (mod-transmog-plugin,
mod-ah-bot-plugin, mod-npc-beastmaster-plugin and others) all follow this layout, each with a settings.json;
copy one as a starting point.
The format reference is Modules from their own repositories.
1. Write the manifest
{
"format": 1,
"id": "mod-transmog",
"version": "1.0.2",
"name": { "en": "Transmogrification", "ru": "Трансмогрификация" },
"description": { "en": "Changes the look of equipped items to that of other items." },
"authors": [ "AzerothCore" ],
"license": "AGPL-3.0",
"homepage": "https://github.com/azerothcore/mod-transmog",
"locales": [ "en", "de", "es", "fr", "ko", "ru", "zh-CN", "zh-TW" ],
"source": {
"repo": "https://github.com/azerothcore/mod-transmog.git",
"commit": "0d85cbc53d63ce2df8527169ce6ae47f5f6f6ba8"
},
"core": { "abi": "lonelyice-ac-2" },
"platforms": [ "windows-x64" ],
"server": { "library": "mod_transmog" },
"databases": { "auth": "data/sql/db-auth", "characters": "data/sql/db-characters", "world": "data/sql/db-world" },
"config": "conf/transmog.conf.dist",
"settings": "settings.json"
}
source.repoandsource.commitpin the module. Always a full commit hash, never a branch.authors,licenseandhomepageare the module's, not yours.databasesandconfigname the module's own files as they are in its repository (data/sql/db-world,conf/transmog.conf.dist); the build lays them out at the same paths inside the plugin.settingsnames your wrapper'ssettings.json(section 4).localeslists the languages the module's texts are translated into.versionis the plugin's version. Raise it whenever the pinned commit or a patch changes.- The shipped wrappers use the module's name as the id (
mod-transmog). The catalog accepts such ids; it warns only because third-party ids are usuallyauthor.feature.
2. Write CMakeLists.txt
This is CMakeLists.txt of mod-transmog-plugin. For another module, replace every mod-transmog with the target
name you choose:
include(FetchContent)
find_package(Git REQUIRED)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/plugin.json" manifest)
string(JSON id GET "${manifest}" id)
string(JSON repo GET "${manifest}" source repo)
string(JSON commit GET "${manifest}" source commit)
file(GLOB patches "${CMAKE_CURRENT_SOURCE_DIR}/patches/*.patch")
set(patch_command "")
if (patches)
# Runs again whenever the module is fetched anew or reconfigured, so it starts from the pinned commit every time.
set(patch_command PATCH_COMMAND "${GIT_EXECUTABLE}" reset --quiet --hard COMMAND "${GIT_EXECUTABLE}" clean --quiet -fd
COMMAND "${GIT_EXECUTABLE}" apply --whitespace=nowarn ${patches})
endif()
# SOURCE_SUBDIR names no folder: the module is only fetched, its own build files are not used.
FetchContent_Declare(mod-transmog GIT_REPOSITORY "${repo}" GIT_TAG "${commit}" SOURCE_SUBDIR none ${patch_command})
FetchContent_MakeAvailable(mod-transmog)
set(module "${mod-transmog_SOURCE_DIR}")
file(GLOB_RECURSE sources "${module}/src/*.cpp" "${module}/src/*.h")
AddPlugin(mod-transmog SOURCES ${sources} plugin/plugin.cpp)
if (NOT TARGET mod-transmog)
return()
endif()
target_include_directories(mod-transmog PRIVATE "${module}/src")
# The module's conf and sql folders, laid out after the plugin's own files.
set(copy "")
foreach(sub conf data)
if (EXISTS "${module}/${sub}")
list(APPEND copy COMMAND ${CMAKE_COMMAND} -E copy_directory "${module}/${sub}" "${AC_PLUGINS_OUTPUT_DIR}/${id}/${sub}")
endif()
endforeach()
add_custom_target(mod-transmog-module-files ALL ${copy} VERBATIM)
set_target_properties(mod-transmog-module-files PROPERTIES FOLDER "plugins")
add_dependencies(mod-transmog-module-files mod-transmog-files)
add_dependencies(mod-transmog mod-transmog-module-files)
What each part does:
| Part | Purpose |
|---|---|
FetchContent_Declare(... SOURCE_SUBDIR none ...) |
Clones the module at source.commit into build/_deps/<name>-src without running its CMake files. |
PATCH_COMMAND |
Resets the checkout to the pinned commit, then applies every patches/*.patch with git apply. A patch that no longer applies stops the configure step. |
AddPlugin(...) |
Builds the module's sources plus your entry point as the plugin library. It returns without a target when the build has no game library, hence the if (NOT TARGET ...) guard. |
target_include_directories |
The module's src/ on the include path (AddPlugin adds only your wrapper's src/). |
*-module-files target |
Copies the module's conf/ and data/ into the laid-out plugin. It depends on <target>-files, the target AddPlugin creates to lay out your own files, because that target first deletes the conf and data folders of the output. |
If the module needs more than game, add LINK (another plugin's target, for example mod-playerbots) and list
that plugin in depends.
3. Write the entry point
plugin/plugin.cpp:
#include "PluginApi.h"
void Addmod_transmogScripts();
AC_PLUGIN(Addmod_transmogScripts)
The function is the module's own script loader, the one the core's module system calls: Add + the module's
folder name with - replaced by _ + Scripts. Look it up in the module's src/*_loader.cpp (or wherever it
is defined).
4. Describe the settings
The module's .conf.dist already declares its options; settings.json in the wrapper's root picks the ones players
change and gives them a group in the launcher's Settings. AddPlugin lays it out with the plugin. An excerpt of
mod-transmog-plugin's:
{
"group": { "en": "Transmogrification", "ru": "Трансмогрификация" },
"hint": {
"en": "Changes take effect after a restart, or at once with .reload config followed by .transmog reload",
"ru": "Изменения вступают в силу после перезапуска или сразу после .reload config и затем .transmog reload"
},
"fields": [
{ "key": "Transmogrification.Enable", "type": "bool", "default": 1, "apply": "restart",
"label": { "en": "Transmogrification enabled", "ru": "Трансмогрификация включена" },
"hint": { "en": "Off: transmogrified looks are hidden; the saved data stays", "ru": "Выкл.: изменённый облик не виден; сохранённые данные остаются" } },
{ "key": "Transmogrification.CopperCost", "type": "int", "min": 0, "default": 0, "apply": "restart",
"label": { "en": "Extra price, copper", "ru": "Доплата, в меди" } },
{ "key": "Transmogrification.AllowMixedWeaponTypes", "type": "choice", "default": 0, "apply": "restart",
"label": { "en": "Weapon types", "ru": "Типы оружия" },
"options": [
{ "value": "0", "label": { "en": "Strict: same weapon type", "ru": "Строго: тот же тип оружия" } },
{ "value": "1", "label": { "en": "Modern: e.g. swords to axes", "ru": "Как в поздних версиях: например, мечи в топоры" } },
{ "value": "2", "label": { "en": "Full: any weapon to any weapon", "ru": "Полностью: любое оружие в любое" } }
] }
]
}
- Every
keymust exist in the module's.conf.dist; the launcher writes it intomodules/transmog.confbeside the server's config (configs/modules), the file the core reads over the.dist. - Choose
applyfrom how the module reads the option, not from what would be convenient. mod-transmog reads its options once, inOnStartup, and again only on its own.transmog reload; the launcher sendsreload configat most, so every field isrestart, and the group's hint names the manual way. A module that re-reads its options inOnAfterConfigLoadcan usereload. Nevernowfor a module's option: the core re-reads configs only onreload configor a restart. - Write the labels yourself, in the languages of
locales; the module's comments are no labels.
The field reference is Plugin settings.
5. Build and check it
cmake -S . -B build -DBOOST_ROOT=<boost> -DOPENSSL_ROOT_DIR=<openssl> "-DLONELYICE_PLUGIN_DIRS=C:/dev/mod-transmog-plugin"
cmake --build build --config RelWithDebInfo --target mod-transmog
The plugin is laid out in build/bin/RelWithDebInfo/plugins/mod-transmog/. Before publishing, check on a test
server:
- The server log shows
Plugin mod-transmog 1.0.0 (...), and the module's own start-up messages. - The module's SQL applies on the default SQLite storage. Module SQL is written for MySQL; statements the translation does not support are listed in Shipping SQL. A file that fails needs a patch.
- The module finds its files. Modules often hard-code
modules/<name>/...paths, which do not exist for a plugin. - The config is read:
modules/<conf name>beside the server's config (configs/modules/transmog.confin LonelyIce) overrides the.distlaid out in the plugin key by key. - The Settings group appears, and a changed value takes effect after the
applyyou chose.
6. Patch only what a plugin needs
Patches stay small. Typical ones:
-
Paths. Files the module reads from
modules/<name>/are in the plugin's folder instead. The plugin loader knows it:sPluginMgr->Find("<id>")->dir(PluginMgr.h). Frommod-npc-beastmaster-plugin:+#include "PluginMgr.h" ... - const std::string path = "modules/mod-npc-beastmaster/conf/profanity.txt"; + // Loaded as a plugin: the list is in the plugin's folder. + PluginInfo const *plugin = sPluginMgr->Find("mod-npc-beastmaster"); + const std::string path = + plugin ? (plugin->dir / "conf" / "profanity.txt").string() + : "modules/mod-npc-beastmaster/conf/profanity.txt"; -
Calls into core code the core does not export from its shared libraries: change the module's call.
-
SQL that does not translate for SQLite.
To make a patch:
- Edit the checkout in
build/_deps/<name>-src. - Save
git diffthere aspatches/NNNN-what-it-does.patch. - Delete
build/_deps/<name>-*and configure again, so the module is fetched and patched from scratch. - List the patch and its reason in the wrapper's README.
A module that needs larger changes is forked instead; source.repo then points at the fork.
Updating the module
- Set
source.committo the new commit and raiseversion. - Delete
build/_deps/<name>-*and configure again. A patch that no longer applies stops the configure step; rework it as above. - Test again, including the module's new SQL files.
Licenses
- The wrapper repository (build files and patches) is under the module's license, with the module's
LICENSEfile. Patches are derived from the module's code. - The manifest's
licenseis the module's license, and the package carries it. - A module without a license: the project's wrappers of such modules (
mod-instance-reset-plugin,mod-multibot-bridge-plugin) put their own files (build files, patches, manifest) under GPL-2.0-or-later, while the module's code stays under its authors' terms. Without a license nothing grants you the right to redistribute that code, so ask the module's authors to add one (or for permission) before you publish a package built from it. - When publishing, the site asks you to confirm that the source is public and that you may publish it under its license.
Publishing
settings.json (section 4), icon.png, LICENSE and README.md in the wrapper root are laid out with the plugin
and go into the package.
Pack and publish as for any plugin (Publishing a package). The zip is built from your
wrapper, so the publish form never prefills source.repo and source.commit, which are the module's: it shows them
as "Wraps". On the first upload the repository stays empty unless homepage is your wrapper repository; later
uploads reuse the repository of your previous submission. Give your wrapper repository and the tag you built from, and mention the pinned module commit in "What
changed".