AzerothCore 模块系统详解:从零开始开发自定义模块 原创

温馨提示:
本文最后更新于 2026-05-09,已超过 0 天没有更新。 若文章内的图片失效(无法正常加载),请留言反馈或直接 联系我

AzerothCore 作为最受欢迎的开源魔兽世界服务器模拟器之一,其最强大的特性之一就是模块化架构。与传统的直接修改核心代码不同,AzerothCore 的模块系统允许开发者在不触碰核心源码的前提下,扩展服务器功能、添加自定义脚本、甚至修改游戏逻辑。本文将深入解析 AzerothCore 模块系统的工作原理,并手把手教你从零开始开发自己的第一个模块。


一、什么是 AzerothCore 模块?

1.1 模块的定义

AzerothCore 模块是一个独立的 C++ 项目,它通过 CMake 子项目的方式编译链接到主服务器中。模块可以:

  • 注册自定义脚本(Script):实现副本 Boss 逻辑、自定义 NPC、任务脚本等
  • 添加钩子函数(Hooks):在游戏事件的关键节点注入自定义逻辑
  • 扩展数据库表:通过模块自带的 SQL 迁移脚本管理自定义数据
  • 添加控制台命令:注册 GM 命令用于管理和调试

1.2 模块 vs 直接修改核心

特性 直接修改核心 模块方式
核心更新兼容性 ❌ 每次更新需手动合并冲突 ✅ 独立维护,不受核心更新影响
代码隔离性 ❌ 混入核心代码 ✅ 完全独立的代码库
分享与分发 ❌ 难以分享 ✅ 可作为独立仓库发布
编译方式 修改源文件后全量编译 CMake 自动链接,增量编译
维护成本

1.3 模块目录结构

一个标准的 AzerothCore 模块目录结构如下:

my-custom-module/
├── CMakeLists.txt              # CMake 构建配置
├── include.sh                  # 模块元数据(名称、作者、版本)
├── sql/                        # 数据库迁移脚本
│   └── base/
│       └── my_module_base.sql
├── src/                        # C++ 源代码
│   ├── my_module.cpp           # 主模块类
│   └── my_module_scripts.cpp   # 自定义脚本
└── README.md                   # 文档说明

二、模块系统底层原理

2.1 CMake 集成机制

AzerothCore 在根 CMakeLists.txt 中通过 add_subdirectory() 自动扫描 modules/ 目录下的所有子项目:

# 核心 CMakeLists.txt 中的模块加载逻辑
if(USE_MODULES)
  file(GLOB MODULES RELATIVE ${CMAKE_SOURCE_DIR}/modules modules/*)
  foreach(MODULE ${MODULES})
    if(EXISTS ${CMAKE_SOURCE_DIR}/modules/${MODULE}/CMakeLists.txt)
      add_subdirectory(modules/${MODULE})
    endif()
  endforeach()
endif()

这意味着只要将模块文件夹放入 modules/ 目录并包含 CMakeLists.txt,核心编译系统就会自动发现并链接它。

2.2 模块生命周期

模块的生命周期由 ACModule 基类管理:

class ACModule {
public:
    virtual ~ACModule() = default;

    // 模块初始化时调用
    virtual void OnStartup() {}

    // 世界初始化完成时调用
    virtual void OnWorldReady() {}

    // 服务器关闭时调用
    virtual void OnShutdown() {}
};

核心在启动流程的不同阶段调用这些方法,模块可以在此注册脚本、读取配置、初始化数据。

2.3 钩子系统(Hook System)

AzerothCore 提供了丰富的钩子函数,允许模块在游戏事件的关键节点注入逻辑。常用钩子包括:

钩子名称 触发时机 参数
OnPlayerLogin 玩家登录完成 Player* player
OnPlayerLogout 玩家开始登出 Player* player
OnCreatureKill 怪物被击杀 Unit* killer, Creature* creature
OnSpellCast 施法开始时 Player* caster, Spell* spell
OnAreaTrigger 进入区域触发器 Player* player, uint32 triggerId

三、实战:开发第一个模块

3.1 创建模块骨架

我们以开发一个“登录欢迎系统”模块为例——当玩家登录时发送自定义欢迎消息,并记录登录次数。

include.sh

# 模块元数据
AC_MODULE_NAME="Login Welcome Module"
AC_MODULE_AUTHOR="YourName"
AC_MODULE_DESCRIPTION="Sends a welcome message on player login and tracks login count"
AC_MODULE_VERSION="1.0.0"

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

project(login_welcome_module)

# 收集源文件
file(GLOB_RECURSE SOURCES
    ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/src/*.h
)

# 创建静态库
add_library(${PROJECT_NAME} STATIC ${SOURCES})

# 链接到 AzerothCore
target_link_libraries(${PROJECT_NAME} game)

# 包含目录
target_include_directories(${PROJECT_NAME} PRIVATE
    ${CMAKE_SOURCE_DIR}/src/server/game
    ${CMAKE_SOURCE_DIR}/src/server/shared
)

# 安装 SQL 文件
install(
    DIRECTORY sql/base DESTINATION modules/login_welcome_module/sql
    FILES_MATCHING PATTERN "*.sql"
)

3.2 实现主模块类

src/login_welcome_module.h

#ifndef LOGIN_WELCOME_MODULE_H
#define LOGIN_WELCOME_MODULE_H

#include "Module.h"

class LoginWelcomeModule : public ACModule {
public:
    static LoginWelcomeModule* instance();

    void OnStartup() override;
    void OnWorldReady() override;

private:
    LoginWelcomeModule() = default;
};

#endif

src/login_welcome_module.cpp

#include "login_welcome_module.h"
#include "ScriptMgr.h"
#include "Player.h"
#include "Chat.h"
#include "DatabaseEnv.h"

LoginWelcomeModule* LoginWelcomeModule::instance() {
    static LoginWelcomeModule instance;
    return &instance;
}

void LoginWelcomeModule::OnStartup() {
    TC_LOG_INFO("module.login_welcome", "Login Welcome Module loaded");
}

void LoginWelcomeModule::OnWorldReady() {
    // 注册玩家脚本钩子
    new LoginWelcomePlayerScript();
}

// 模块实例注册(必须)
void AddLoginWelcomeModuleScripts() {
    LoginWelcomeModule::instance();
}

3.3 实现玩家脚本

src/login_welcome_scripts.cpp

#include "login_welcome_module.h"
#include "ScriptMgr.h"
#include "Player.h"
#include "Chat.h"
#include "DatabaseEnv.h"

class LoginWelcomePlayerScript : public PlayerScript {
public:
    LoginWelcomePlayerScript() : PlayerScript("LoginWelcomePlayerScript") {}

    void OnLogin(Player* player, bool /*firstLogin*/) override {
        // 获取登录次数
        uint32 loginCount = GetLoginCount(player->GetGUID());

        // 发送欢迎消息
        ChatHandler(player).PSendSysMessage(
            "|cff00ff00================================|r"
        );
        ChatHandler(player).PSendSysMessage(
            "|cff00ff00欢迎来到服务器, %s!|r", player->GetName().c_str()
        );
        ChatHandler(player).PSendSysMessage(
            "|cff00ff00这是您的第 %d 次登录|r", loginCount + 1
        );
        ChatHandler(player).PSendSysMessage(
            "|cff00ff00================================|r"
        );

        // 更新登录次数
        UpdateLoginCount(player->GetGUID(), loginCount + 1);
    }

private:
    uint32 GetLoginCount(ObjectGuid guid) {
        QueryResult result = CharacterDatabase.PQuery(
            "SELECT login_count FROM custom_login_stats WHERE guid = %u",
            guid.GetCounter()
        );
        if (result)
            return (*result)[0].GetUInt32();
        return 0;
    }

    void UpdateLoginCount(ObjectGuid guid, uint32 count) {
        CharacterDatabase.PExecute(
            "REPLACE INTO custom_login_stats (guid, login_count, last_login) "
            "VALUES (%u, %u, UNIX_TIMESTAMP())",
            guid.GetCounter(), count
        );
    }
};

3.4 数据库脚本

sql/base/custom_login_stats.sql

-- 登录统计表
CREATE TABLE IF NOT EXISTS `custom_login_stats` (
  `guid` int(10) unsigned NOT NULL,
  `login_count` int(10) unsigned NOT NULL DEFAULT 0,
  `last_login` int(10) unsigned NOT NULL DEFAULT 0,
  PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 
COMMENT='Custom login statistics table';

3.5 编译与安装

# 1. 将模块放入 modules 目录
cd /path/to/azerothcore-wotlk
mkdir -p modules
git submodule add <your-module-repo-url> modules/login-welcome-module

# 2. 重新配置 CMake
cmake -B build -DCMAKE_BUILD_TYPE=Release

# 3. 编译
cd build
make -j$(nproc)

# 4. 运行数据库迁移
mysql -u root -p ac_world < ../modules/login-welcome-module/sql/base/custom_login_stats.sql

# 5. 启动服务器
./authserver & ./worldserver

四、高级模块开发技巧

4.1 配置文件支持

模块可以通过 mod-*.conf 文件提供可配置参数:

// 读取配置文件
bool enabled = sConfigMgr->GetBoolDefault("LoginWelcome.Enabled", true);
std::string welcomeMsg = sConfigMgr->GetStringDefault(
    "LoginWelcome.Message", 
    "欢迎来到我们的服务器!"
);

对应的配置文件 mod-login-welcome.conf

LoginWelcome.Enabled = 1
LoginWelcome.Message = "欢迎来到 AzerothCore 服务器!"
LoginWelcome.ShowLoginCount = 1

4.2 注册自定义 GM 命令

class LoginWelcomeCommandScript : public CommandScript {
public:
    LoginWelcomeCommandScript() : CommandScript("LoginWelcomeCommandScript") {}

    ChatCommandTable GetCommands() const override {
        static ChatCommandTable commandTable = {
            HandleResetLoginCount,  // .login reset <player>
        };
        return commandTable;
    }

    static bool HandleResetLoginCount(ChatHandler* handler, const char* args) {
        // 实现命令逻辑
        return true;
    }
};

4.3 模块间通信

多个模块可以通过全局事件总线进行通信:

// 发送事件
sScriptMgr->OnPlayerEvent(player, PLAYER_EVENT_LOGIN, count);

// 在另一个模块中监听
void OnPlayerEvent(Player* player, uint32 eventId, uint32 data) override {
    if (eventId == PLAYER_EVENT_LOGIN) {
        // 处理登录事件
    }
}

五、最新 RBAC 更新与模块兼容性

在最近的 AzerothCore 更新中(2026年5月),核心修复了 RBAC 权限系统对 .whispers 命令的过滤逻辑。这一改动意味着模块在注册自定义 GM 命令时,需要特别注意权限配置:

5.1 RBAC 权限检查最佳实践

// 在模块命令中加入权限检查
static bool HandleMyCommand(ChatHandler* handler, const char* args) {
    // 检查玩家是否有对应的 RBAC 权限
    if (!handler->GetSession()->GetSecurity() >= RBAC_SEC_MODERATOR) {
        handler->SendSysMessage("权限不足");
        return false;
    }

    // 执行命令逻辑
    return true;
}

5.2 注册自定义 RBAC 权限

-- 在模块 SQL 中注册自定义权限
INSERT INTO `rbac_permissions` (`id`, `name`) VALUES
(50000, 'Command: mymodule');

-- 关联到角色
INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES
(192, 50000);  -- 192 = Sec Level 2 (Moderator)

六、模块开发常见陷阱

6.1 空指针检查

核心对象(如 Player、Creature)在某些钩子中可能为 nullptr:

void OnCreatureKill(Unit* killer, Creature* creature) override {
    Player* player = killer->ToPlayer();
    if (!player) return;  // 必须检查!可能是宠物击杀
    // ...
}

6.2 数据库事务

涉及多个表的操作应使用事务:

Transaction trans = CharacterDatabase.BeginTransaction();
trans->Append(...);
trans->Append(...);
CharacterDatabase.CommitTransaction(trans);

6.3 线程安全

WorldSession 钩子在特定上下文中执行,避免使用全局锁:

// ❌ 不好:使用互斥锁阻塞世界线程
std::lock_guard<std::mutex> lock(g_mutex);

// ✅ 好:使用异步队列
sWorld->AddTask([this, data]() { ProcessData(data); });

七、优秀开源模块推荐

模块名称 功能 仓库
mod-eluna Lua 脚本引擎 github.com/azerothcore/mod-eluna
mod-anticheat 反作弊系统 github.com/azerothcore/mod-anticheat
mod-npcbeastmaster NPC 宠物训练师 github.com/azerothcore/mod-npcbeastmaster
mod-transmog 幻化系统 github.com/azerothcore/mod-transmog
mod-bg-reward 战场奖励系统 github.com/azerothcore/mod-bg-reward

八、总结

AzerothCore 的模块系统是扩展服务器功能的最佳方式。通过模块化开发,你可以:

  • 保持核心代码干净,方便跟随官方更新
  • 独立测试和调试,降低开发风险
  • 轻松分享和复用,构建社区生态
  • 灵活的权限管理,与 RBAC 系统无缝集成

无论你是想添加自定义 NPC、实现新的副本机制,还是开发独特的游戏系统,模块系统都为你提供了强大的基础。开始动手吧!


本文基于 AzerothCore 最新版本编写,适用于 2026 年 5 月后的主分支。