当前位置: 首页 > backend >正文

项目实战4:TrinityCore框架学习

写在前面

运行这个项目是最简单的,项目基于TrinityCore游戏框架,下载TrinityCore源码后,需要搭建编译环境(较麻烦)、编译、准备数据库、获取游戏客户端数据、配置.conf文件后,即可运行服务器,随后在window下安装对应版本的客户端即可游玩。

本文主要学习TrinityCore这个游戏框架对各个功能实现的代码方法,并且尝试修改查看不同游戏效果。TrinityCore的目标就是模拟官方的行为,让客户端认为自己就是在连接一个官方服务器。

项目线程模型

模块需求

第一块:技能模块

网络部分

加载技能:客户端在登录成功,拿到session key后,向world server请求角色技能信息,world server调用character handler业务处理函数,通过数据库连接池+pipeline模式 从数据库中取出技能存档信息,存储到内存,然后发送给客户端。

技能生命周期部分

使用技能《暴风雪》:客户端发起对应释放技能的请求,world server调用spell handler处理业务,spell handler具体流程是:

new创建技能对象->prepare对目标对象施法->开启定时器,创建spell_event->计算前摇时间 casttime,在定时器中更新->开始施法,首先根据效果ID执行技能效果,1:范围确定,计算区域半径,update检测区域内的target目标,不产生伤害。2:每间隔1秒,对target产生伤害,技能持续8s->施法完成FINISHED状态返回true删除事件,删除new的技能对象

配置部分

客户端、服务器、数据库都有一份相同的配置,以技能配置为例,在数据库中修改数值后,服务器从数据库拿到数据就也会更新自己的配置,客户端再连上服务器后,也会更新配置。

第二块:AI模块

本质:感知事件(输入),做出反馈(输出)

感知:等待游戏核心引擎在发生特定事件时,会调用处理接口,不同的AI就通过虚函数复写AI,实现各自的处理。

事件类型有:生命周期事件(创建或死亡),战斗事件(仇恨单位,受到伤害),移动事件,对话事件,状态变化事件等

实现方式

(1)状态机:AI行为在有限的几个状态内切换,满足一定的条件才能切换。

(2)决策树:感知到事件后,从根节点出发,根据非叶(逻辑)结点遍历达到叶子结点,执行对应事件的实际指令。

AI层次设计

(1)稳定(写死):即怪物固定的行为,写成cpp形式,在编译worldserver时统一编译

(2)变化(动态):如ScriptAI和SmartAI,通过脚本或配置的形式来定义怪物的行为而不是c++代码,修改脚本或数据库就能直接修改怪物行为,然后重新编译单独的动态库。

野怪AI设计

当一个野怪在世界中生成时,服务器会按以下逻辑为其分配一个AI实例:

最高优先级:ScriptName (creature_template.ScriptName)​​
数据库表 creature_template 中每个怪物条目都有一个 ScriptName 字段。如果该字段非空,通过脚本系统去找到一个与之匹配的AI类。

中级优先级:AIName
同样在 creature_template 表中,如果 ScriptName 为空,则检查 AIName 字段。它的行为完全由数据库表 smart_scripts 来定义,无需修改C++代码,非常灵活。

默认优先级:VanillaAI

如果前两项都未配置,怪物就没有任何“智能”可言。此时,核心会回退到使用一个非常基础的AI,这种AI可能只会做一些最简单的事情,比如被攻击后反击(通过核心机制,而非AI决策),但不会主动索敌或使用技能。

第三块:GM指令

定义

GM 指令,又称控制台命令或作弊命令,是专门提供给服务器管理员使用的一系列特殊命令。这些命令允许执行者超越正常游戏的限制,用于管理服务器、测试游戏内容、协助玩家或进行调试。GM模式下AI不会开启。

所有可用的 GM 指令都存储在 TrinityCore 的世界数据库的 command 表

具体执行流程

​客户端发送消息​: 你的客户端将字符串 .additem 12345 发送给服务器。
​服务器接收并解析​: 服务器的 WorldSession::HandleChatMessage 等函数会处理这个消息。
​识别GM指令​: 系统发现消息以 ​**.**​(点号)开头,识别出这是一个GM指令。
​权限检查​: 系统查询 command 表,找到 additem 指令,并检查账户权限
​参数解析​: 将字符串分割成命令部分(additem)和参数部分(12345)。
​查找并调用处理函数​: 在 chatCommandTable 中找到 additem 对应的函数指针 HandleAddItemCommand,并调用它,同时将参数 12345 传递过去。
​执行逻辑​: HandleAddItemCommand 函数开始执行:
获取你当前选中的玩家目标。
将参数 12345 转换为整数(物品ID)。
在数据库中查找这个ID对应的物品模板是否存在。
调用 targetPlayer->AddItem() 方法,将物品添加到目标玩家的背包中。
通过 handler->PSendSysMessage 给你发送一个操作成功的反馈消息。

技术点

第一点:定时器在TrinityCore的应用


定时器的设计:定时器由两个部分构成,容器和驱动方式。容器决定所有定时器由一个什么样的数据结构统一管理,驱动方式的设计决定了如何感知时间的流逝并且触发到期的定时器。(在定时器的10s时间里,该线程不可能阻塞等待,那就希望线程能去执行其他任务,在10s后有一种方式能通知该线程回来处理10s后的事件)

EventProcess模型

对象专属定时器,专注对象行为延时处理。

EventProcess中定时器的容器:multimap(底层是红黑树),因为可能会在同一时间有多个定时器结束,即可能有多个相同的key值。在EventProcess里,对应的键值是:<时间uint64,触发的事件BasicEvent*>

EventProcess的定时器驱动方式:执行函数update。传入当前时间,遍历红黑树最左边的最小结点,如果有事件的时间<=当前时间,从红黑树删除改结点并且Execute执行,但是否删除该任务还要看Execute的返回值,如果为false,表明这个任务需要反复执行,不delete。

EventProcess定时器的封装的实现:采用了两种方法,一个是lambda表达式(将参数传递给BasicEvent类),一个是类对象(继承自BasicEvent)两种形式。

TaskScheduler模型

怪物AI专属定时器,专注AI行为处理,可在定时器中死亡。因为AI的定时器间有相关性,所以独立于EventProcess定时器。

TaskScheduler中的定时器容器:multiset(底层也是红黑树)。对应存储的是​以 TaskContainer 对象作为元素的有序集合,而TaskContainer是一个指向Task任务信息函数对象的智能指针,通过比较Task的到期时间_end来排序。

TaskScheduler的定时器驱动方式:执行函数update。传入与上次执行update的时间间隔,在update函数内还是计算当前时间,执行已经到了时间的任务的回调函数。

TaskScheduler定时器的封装的实现:采用了shared_ptr和weak_ptr两种指针实现的,具体来说,定时器由shared_ptr来管理,但是同时任务对象内部还有一个不析构对象的shared_ptr,weak_ptr会弱引用这个ptr,作为TaskContext类的指针,可以调用TaskScheduler类的所有函数接口。原因在于这是提供给AI用的定时器模型,可能出现野怪死亡,依赖的对象销毁,TaskScheduler对象也销毁,直接回调导致指针悬空,所以需要TaskContext的弱指针来检查调度器是否存活。

第二点:数据库连接池在TrinityCore的应用

TrinityCore需要向数据库建立连接请求数据的是地图线程池中的map线程(由main主线程调用),在数据库连接池的底层封装参见之前的连接池文章,可以通过建立一个连接池来等待数据库的连接而不阻塞发起请求的map线程,随后通过future+promise机制将线程池从数据库获得的返回值返回给map线程作为参数执行回调函数。

TrinityCore应用连接池的特殊应用模式:

pineline模式的封装,即将多条不相关的sql一起处理,将本来的多个回调融合到一个回调中减少开销,只需要将参数都传进来即可。

事务模式的封装,将多条相关的sql一起处理,但是会按照传给一个连接的sql的顺序进行执行,不会乱序,但是多条连接的sql是可以并行执行的。

TrinityCore通过不同的接口调用,使用同步连接池或者异步连接池,同步连接池只在服务器开机的时候使用,加快开机进程,因为保证顺序所以无法异步。同步连接池的实现过程就是申请一个连接就给一个连接加上一把锁,用户请求通过轮询获取锁的方式,来获得连接,连接使用完毕后释放锁。异步连接池的实现是一个连接对应一个线程,而我们会开一个线程池与数据库建立连接,用户请求线程只需要往SQL执行队列抛出任务,线程池中线程会从队列取出任务并且通过建立的连接执行对应的用户请求,并且通过future+promise返回结果值给用户线程。

 第三点:注册、登录模块

1、保障客户数据的安全,客户端在与游戏服务器传输过程中不传输明文密码。

2、连服务器的数据库存储的用户密码都是加密后的,明文密码只存在于客户端本地。

模块实现:

TrinityCore结合SRP6理论过程:

注册流程 web server

1.客户端发送username、password。服务端先验证username,若无重名,随机s(盐值salt),生成密码验证器v(verifier,通过username:password + 盐值混淆后的结合哈希获得的值),并且将这个v值写入数据库

登录Authserver流程

2.服务端根据用户名获取专属盐值 与 密码验证器值v,以及随机生成的私钥b,计算得出公钥B,并且把盐值发给客户端,客户端根据盐值和输入的用户名+密码哈希计算密码验证器的值。并且也随机生成一个私钥a,且基于a生成A公钥。最后两方都根据v计算S的值,根据A,B,S计算M1的值,比对M1的值判断是否登录成功。图示如下:

问:登录过程单纯根据v值已经能验证密码正确性,为什么还要计算公钥私钥来生成S和M
答:是为了保证连接的活性,如果没有私钥生成,劫持者虽然不知道密码,但会窃取一次登录包在用户连接断开后发送给服务器,
包括单纯的v值。所以v值只是一个理论保证,需要每次连接随机生成的私钥来保证连接的活性,以此来生成S和M才能保证后续连接的真实性

登录游戏服务器world server

3.客户端发送用户名与session key,服务器根据用户名获取专属的session key,比对客户端的session key,验证是否成功。

细致安全保证

1.SRP6算法,保证密码不可见性,采用 SHA1哈希+加盐+慢哈希。如果劫持方通过sql注入获得了数据库中verifier字段的值,也无法通过SHA1哈希回推原密码,这在理论上是不可能的,就像打碎的玻璃无法复原。但是劫持方会采用暴力算法,准备一个巨大(亿级)的密码候选表里面有常用的密码,对每一个密码通过SHA1计算哈希值与数据库中的哈希值比对,以此来获得原文。但是在加盐(增加随机数,比如用户名)处理后,因为盐(随机值)是无穷无尽的,想要通过暴力来破解的数量级又上升了一个维度。再加上SRP6的多轮SHA1计算慢哈希,整个过程比一次简单哈希更复杂,对合法用户来说只是慢几毫秒无法感知,但是对于暴力破解的劫持方,以现在的算力得出原文的总时间至少是几百年的数量级。

2.非对称加密,离散对数运算保证私钥私密性。防劫持者通过公钥破解私钥。

第四点:网络模块boost::io

boost::io中绑定的异步io是ioContext,效果上相当于iocp、proactor、io_uring,与reactor的不同有两点,1.ioContext可以做到事件完成后通知,而reactor是事件就绪后通知,对于一个具体的recv来说的话,一个是用户发来数据并且读完了,通知主线程去处理;一个是用户发来了数据,通知主线程去读。2.ioContext这类异步io需要手动设置事件进入队列,而reactor只需要一次加入epoll监听fd即可。

网络数据流向:

(1)建立连接,提供回调 :由Acceptor线程运行

class AsyncAcceptor
{
public:typedef void(*AcceptCallback)(tcp::socket&& newSocket, uint32_t threadIndex);构造函数:绑定io_context异步io,监听ip以及端口AsyncAcceptor(boost::asio::io_context& ioContext, std::string const& bindIp, uint16_t port) :_acceptor(ioContext), _endpoint(boost::asio::ip::address::from_string(bindIp), port),_socket(ioContext), _closed(false), _socketFactory(std::bind(&AsyncAcceptor::DefeaultSocketFactory, this)){}template<class T>void AsyncAccept();template<AcceptCallback acceptCallback>    void AsyncAcceptWithCallback()    异步io完成后,需要回调函数通知主线程{tcp::socket* socket;uint32_t threadIndex;std::tie(socket, threadIndex) = _socketFactory(); 将socket连接与网络线程绑定提交一个异步的accept,第二个参数是提供  asio处理完一个连接接收之后,的一个完成回调函数_acceptor.async_accept(*socket, [this, socket, threadIndex](boost::system::error_code error){if (!error){try{socket->non_blocking(true);acceptCallback(std::move(*socket), threadIndex);}catch (boost::system::system_error const& err){std::cout << "Failed to initialize client's socket" << std::endl;}}if (!_closed)this->AsyncAcceptWithCallback<acceptCallback>();});}bool Bind() 封装bind操作{boost::system::error_code errorCode;_acceptor.open(_endpoint.protocol(), errorCode);创建socketif (errorCode){std::cout << "Failed to open acceptor" << std::endl;return false;}#if defined (_WIN64) || defined( __WIN32__ ) || defined( WIN32 ) || defined( _WIN32 )
#else_acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true), errorCode);if (errorCode){std::cout << "Failed to set reuse_address option on acceptor" << std::endl;return false;}
#endif_acceptor.bind(_endpoint, errorCode); bind绑定if (errorCode){std::cout << "Could not bind" << std::endl;return false;}_acceptor.listen(TRINITY_MAX_LISTEN_CONNECTIONS, errorCode); listen监听if (errorCode){std::cout << "Failed to start listening" << std::endl;return false;}return true;}void Close() 封装断开连接{if (_closed.exchange(true))return;boost::system::error_code err;_acceptor.close(err);}void SetSocketFactory(std::function<std::pair<tcp::socket*, uint32_t>()> func) { _socketFactory = func; }private:std::pair<tcp::socket*, uint32_t> DefeaultSocketFactory() { return std::make_pair(&_socket, 0); }tcp::acceptor _acceptor;tcp::endpoint _endpoint;tcp::socket _socket;std::atomic<bool> _closed;std::function<std::pair<tcp::socket*, uint32_t>()> _socketFactory;
};

(2)将连接与线程绑定,并且update驱动读写

void AddNewSockets(){std::lock_guard<std::mutex> lock(_newSocketsLock);if (_newSockets.empty())    return;for (std::shared_ptr<SocketType> sock : _newSockets){if (!sock->IsOpen()){SocketRemoved(sock);    从newsockets容器拿出--_connections;}else_sockets.push_back(sock);    加入sockets容器}_newSockets.clear();}void Update(){if (_stopped)return;_updateTimer.expires_from_now(boost::posix_time::milliseconds(1));_updateTimer.async_wait([this](boost::system::error_code const&) { Update(); });AddNewSockets();_sockets.erase(std::remove_if(_sockets.begin(), _sockets.end(), [this](std::shared_ptr<SocketType> sock){if (!sock->Update())    调用socket对象的接口{if (sock->IsOpen())sock->CloseSocket();this->SocketRemoved(sock);--this->_connections;return true;}return false;}), _sockets.end());}

(3)收发数据:socket对象实现具体收发

class Socket : public std::enable_shared_from_this<T>
流程:
读数据:
void AsyncRead(){if (!IsOpen())return;_readBuffer.Normalize();_readBuffer.EnsureFreeSpace();_socket.async_read_some(boost::asio::buffer(_readBuffer.GetWritePointer(), _readBuffer.GetRemainingSpace()),std::bind(&Socket<T>::ReadHandlerInternal, this->shared_from_this(), std::placeholders::_1, std::placeholders::_2));}发送数据:
先入队列
void QueuePacket(MessageBuffer&& buffer){_writeQueue.push(std::move(buffer));#ifdef TC_SOCKET_USE_IOCPAsyncProcessQueue();
#endif}
每1ms会Update 监测队列内容进行发送   设置队列是因为逻辑线程和网络线程的交互
virtual bool Update(){if (_closed)return false;#ifndef TC_SOCKET_USE_IOCPif (_isWritingAsync || (_writeQueue.empty() && !_closing))return true;for (; HandleQueue(););
#endifreturn true;}

http://www.xdnf.cn/news/18909.html

相关文章:

  • 科技守护古树魂:古树制茶行业的数字化转型之路
  • 把llamafacoty微调后的模型导出ollama模型文件
  • 【前端教程】JavaScript入门核心:使用方式、执行机制与核心语法全解析
  • Oracle 数据库权限管理的艺术:从入门到精通
  • 目标检测领域基本概念
  • 第6篇:链路追踪系统 - 分布式环境下的请求跟踪
  • JSP程序设计之JSP指令
  • 【Python】QT(PySide2、PyQt5):Qt Designer,VS Code使用designer,可能的报错
  • Java学习笔记之——通过分页查询样例感受JDBC、Mybatis以及MybatisPlus(一)
  • 上海控安:汽车API安全-风险与防护策略解析
  • Java 实现HTML转Word:从HTML文件与字符串到可编辑Word文档
  • Nginx + Certbot配置 HTTPS / SSL 证书(简化版已测试)
  • 机器视觉学习-day07-图像镜像旋转
  • 【Deepseek】Windows MFC/Win32 常用核心 API 汇总
  • 【PyTorch】基于YOLO的多目标检测项目(一)
  • 【Redis】数据分片机制和集群机制
  • 【Java SE】基于多态与接口实现图书管理系统:从设计到编码全解析
  • C/C++---前缀和(Prefix Sum)
  • 微服务的编程测评系统17-判题功能-代码沙箱
  • MQTT broker 安装与基础配置实战指南(一)
  • 题目—移除元素
  • PyTorch中的激活函数
  • AI需求优先级:数据价值密度×算法成熟度
  • HSA35NV001美光固态闪存NQ482NQ470
  • 达可替尼-
  • SpringBoot整合RabbitMQ:从消息队列基础到高可用架构实战指南
  • 浏览器网页路径扫描器(脚本)
  • 改造thinkphp6的命令行工具和分批次导出大量数据
  • MySQL 基础:DDL、DML、DQL、DCL 四大类 SQL 语句全解析
  • K8s 二次开发漫游录