AzerothCore Module 开发实战:从零构建自定义功能模块 原创

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

一、什么是 AzerothCore Module?

AzerothCore 的 Module 系统是其最强大的扩展机制之一。与直接修改核心源码不同,Module 允许开发者以插件化的方式添加功能,而无需修改 TrinityCore 分支的核心代码。这意味着:

  • 不污染核心:所有修改隔离在 Module 目录中,核心升级时无需重新适配
  • 易于分发:Module 可以打包分享,其他服主只需复制到 modules 目录即可使用
  • 热插拔:通过 CMake 配置即可启用或禁用,无需手动合并代码

二、Module 架构解析

2.1 目录结构

一个标准的 AzerothCore Module 遵循以下目录结构:

modules/
└── mod-custom-name/
    ├── CMakeLists.txt          # 构建配置
    ├── README.md               # 文档说明
    ├── include/
    │   └── mod_custom_name/
    │       └── CustomName.h    # 头文件
    ├── src/
    │   ├── CustomName.cpp      # 核心实现
    │   └── CustomNameScripts.cpp # 脚本注册
    └── sql/
        └── world/
            └── 2026_01_01_00_custom_name.sql  # 数据库变更

2.2 CMakeLists.txt 模板

每个 Module 的核心是 CMakeLists.txt,它告诉构建系统如何编译 Module:

# 模块名称(必须唯一)
set(MOD_NAME mod-custom-name)

# 源文件列表
set(MOD_SOURCES
    ${MOD_NAME}/src/CustomName.cpp
    ${MOD_NAME}/src/CustomNameScripts.cpp
)

# 头文件目录
set(MOD_HEADERS
    ${MOD_NAME}/include/
)

# 注册到 AzerothCore 构建系统
ac_add_module(${MOD_NAME} "${MOD_SOURCES}" "${MOD_HEADERS}")

# 链接依赖库(如有需要)
target_link_libraries(${MOD_NAME} PRIVATE
    ${AC_LIBRARY_SCRIPTS}
)

三、实战:编写一个玩家欢迎 Module

下面我们从一个完整的实战案例开始,逐步构建一个功能完整的 Module。

3.1 功能需求

  • 玩家首次登录时发送欢迎消息
  • 记录玩家在线时长
  • 提供 .online 命令查询在线时长
  • 玩家上线时公告通知公会成员

3.2 头文件定义

// include/mod_welcome/WelcomeMod.h
#ifndef WELCOME_MOD_H
#define WELCOME_MOD_H

#include "ScriptMgr.h"
#include "Player.h"
#include "Chat.h"
#include "WorldSession.h"

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

    // 玩家登录时调用
    void OnLogin(Player* player) override;

    // 玩家首次登录
    void OnFirstLogin(Player* player) override;

    // 玩家登出时调用
    void OnLogout(Player* player) override;

private:
    // 存储玩家在线时长的 Map
    static std::map<uint32, uint32> m_onlineTime;
    static std::map<uint32, uint32> m_sessionStart;
};

#endif

3.3 核心实现

// src/WelcomeMod.cpp
#include "WelcomeMod.h"
#include "Chat.h"
#include "Language.h"
#include "World.h"

std::map<uint32, uint32> WelcomeMod::m_onlineTime;
std::map<uint32, uint32> WelcomeMod::m_sessionStart;

void WelcomeMod::OnLogin(Player* player)
{
    // 记录本次会话开始时间
    m_sessionStart[player->GetGUID().GetCounter()] = time(nullptr);

    // 发送欢迎消息
    std::string welcomeMsg = "|cff00ccff欢迎回到艾泽拉斯,";
    welcomeMsg += player->GetName();
    welcomeMsg += "!|r";
    ChatHandler(player->GetSession()).SendSysMessage(welcomeMsg);

    // 获取总在线时长
    uint32 totalMinutes = m_onlineTime[player->GetGUID().GetCounter()] / 60;
    if (totalMinutes > 0)
    {
        std::ostringstream ss;
        ss << "|cff00ff00你已累计在线 " << totalMinutes << " 分钟|r";
        ChatHandler(player->GetSession()).SendSysMessage(ss.str());
    }

    // 通知公会成员
    if (Guild* guild = player->GetGuild())
    {
        std::ostringstream ss;
        ss << "|cff00ccff[公会] " << player->GetName() << " 上线了!|r";
        guild->BroadcastToMembers(player->GetSession(), false, ss.str());
    }
}

void WelcomeMod::OnFirstLogin(Player* player)
{
    // 首次登录特殊欢迎
    std::string welcomeMsg = "|cffffcc00欢迎来到艾泽拉斯,";
    welcomeMsg += player->GetName();
    welcomeMsg += "!愿你在这片土地上书写传奇!|r";

    // 发送系统公告
    sWorld.SendServerMessage(SERVER_MSG_STRING, welcomeMsg);
}

void WelcomeMod::OnLogout(Player* player)
{
    uint32 guid = player->GetGUID().GetCounter();
    uint32 sessionStart = m_sessionStart[guid];

    if (sessionStart > 0)
    {
        uint32 elapsed = time(nullptr) - sessionStart;
        m_onlineTime[guid] += elapsed;
        m_sessionStart.erase(guid);
    }
}

3.4 命令注册

// src/WelcomeModScripts.cpp
#include "WelcomeMod.h"
#include "Chat.h"

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

    std::vector<ChatCommand> GetCommands() const override
    {
        static std::vector<ChatCommand> welcomeCommandTable =
        {
            { "online", SEC_PLAYER, true, &HandleOnlineCommand, "" }
        };

        static std::vector<ChatCommand> commandTable =
        {
            { "welcome", SEC_PLAYER, true, nullptr, "", welcomeCommandTable }
        };

        return commandTable;
    }

    static bool HandleOnlineCommand(ChatHandler* handler, const char* /*args*/)
    {
        Player* player = handler->GetSession()->GetPlayer();
        uint32 guid = player->GetGUID().GetCounter();
        uint32 totalSeconds = WelcomeMod::m_onlineTime[guid];

        uint32 hours = totalSeconds / 3600;
        uint32 minutes = (totalSeconds % 3600) / 60;
        uint32 seconds = totalSeconds % 60;

        std::ostringstream ss;
        ss << "|cff00ff00你的累计在线时间:"
           << hours << " 小时 " << minutes << " 分钟 " << seconds << " 秒|r";
        handler->SendSysMessage(ss.str());

        return true;
    }
};

void AddSC_welcome_mod_scripts()
{
    new welcome_commandscript();
}

四、数据库集成

如果 Module 需要持久化数据,需要创建对应的数据库表:

-- sql/world/2026_01_01_00_welcome_mod.sql
DROP TABLE IF EXISTS `mod_welcome_player_stats`;
CREATE TABLE `mod_welcome_player_stats` (
    `guid` INT UNSIGNED NOT NULL,
    `total_online_seconds` INT UNSIGNED DEFAULT 0,
    `last_login` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `login_count` INT UNSIGNED DEFAULT 0,
    PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

然后在 OnLoginOnLogout 中读写数据库:

// 从数据库加载
QueryResult result = CharacterDatabase.PQuery(
    "SELECT total_online_seconds, login_count FROM mod_welcome_player_stats WHERE guid = %u",
    player->GetGUID().GetCounter());

if (result)
{
    Field* fields = result->Fetch();
    m_onlineTime[guid] = fields[0].Get<uint32>();
}

// 保存到数据库
CharacterDatabase.PExecute(
    "INSERT INTO mod_welcome_player_stats (guid, total_online_seconds, login_count) "
    "VALUES (%u, %u, 1) ON DUPLICATE KEY UPDATE "
    "total_online_seconds = %u, login_count = login_count + 1, last_login = NOW()",
    guid, m_onlineTime[guid], m_onlineTime[guid]);

五、编译与部署

5.1 启用 Module

# 克隆 Module 到 modules 目录
cd azerothcore-wotlk/modules
git clone https://github.com/your/mod-custom-name.git

# 重新运行 CMake
cd ..
mkdir build && cd build
cmake .. -DMODULES=mod-custom-name

# 编译
make -j$(nproc)

5.2 应用 SQL 变更

# 导入数据库变更
mysql -u root -p world < modules/mod-custom-name/sql/world/2026_01_01_00_welcome_mod.sql
mysql -u root -p characters < modules/mod-custom-name/sql/characters/2026_01_01_00_welcome_mod.sql

六、调试技巧

6.1 日志输出

// 使用 AzerothCore 日志系统
TC_LOG_INFO("module.welcome", "Player %s logged in", player->GetName().c_str());
TC_LOG_DEBUG("module.welcome", "Session start: %u", sessionStart);
TC_LOG_ERROR("module.welcome", "Failed to load stats for player %u", guid);

6.2 热重载

AzerothCore 支持部分 Module 的热重载:

# 在游戏内使用 .reload 命令
.reload config    # 重新加载配置文件
.reload all       # 重新加载所有脚本

七、总结

通过本文的实战演练,我们从零构建了一个完整的 AzerothCore Module,涵盖了:

  • Module 架构:目录结构、CMake 配置、构建集成
  • 脚本系统:PlayerScript、CommandScript 的使用
  • 数据库集成:数据持久化与查询
  • 编译部署:完整的构建和部署流程

AzerothCore 的 Module 系统为私服开发者提供了强大的扩展能力。掌握 Module 开发,意味着你可以为服务器添加任意自定义功能,而无需担心核心升级带来的兼容性问题。