找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 578|回复: 5

[教程] 实战示例4:GMS083 收发自定义封包

[复制链接]

45

主题

45

回帖

379

积分

管理员

积分
379
发表于 2025-2-18 16:44:48 | 显示全部楼层 |阅读模式

自定义封包很常用的场景就是跳出客户端原有设计,进行数据通讯。

比如 全队的 Boss 伤害统计,你可以用自定义封包把其他人的伤害发送给玩家,在客户端上实时显示。
比如 你把一个专属的签到网址发送给玩家的客户端,让客户端跳转到签到网站。
比如 。。。 充分发挥你的想象力

本教程以北斗服务端 + 北斗 Ijl15 插件为基础进行制作,如果你使用其他端或插件请自行适当调整里面的调用方法。


现在教大家如何制作自定义的封包

请记住以下概念:每个封包都有封包 ID 这是所有封包的头

首先是服务端部分

我们要先找到定义封包 ID 的 Enum,在服务端中分别是 SendOpcode.java 和 RecvOpcode.java,只需要在 idea 中双击 shift 就很容易找到这两个文件了。

我们先以发封包为例

打开 SendOpcode.java

拉到最下面

添加新的 ENUM,格式为 名称(id), 其中id保持格式统一用十六进制,并且不要和前面的重复,为了防止客户端存在这个封包 ID 但是服务端没有收录的情况,建议把 id 值设置高一点,但是不要超过 short 类型的上限。你可以像我一样从 0x1000 开始

如图我们以 UPDATE_HPMPAALERT 为例(1. 这是北斗添加的自定义封包;2. 记得要在分号前面添加)
[attach]33[/attach]

添加完封包 ID 以后,我们要添加一个用于创建封包的方法,通常这类方法保存在 PacketCreator.java 中(实际上这个文件太长了,anyway 你可以自行安排放在哪,记得住即可)。
[attach]34[/attach]

首先是初始化一个封包对象,初始化时给的参数 SendOpcode.UPDATE_HPMPAALERT 就是前面新添加的封包 ID
  1. OutPacket p = OutPacket.create(SendOpcode.UPDATE_HPMPAALERT);
复制代码

然后就是开始插入封包数据了。在这里例子里,我们插入了两个 byte 值,分别表示 hp 和 mp 的百分比。

实际上这个封包的内容就是:一个ID(两个byte) + 一个 byte + 一个 byte,总共 4 个 byte 而已。在我们读取封包的时候需要自己根据这里的结构赋予这些 byte 意义。

除了 writeByte 方法外,OutPacket 还提供了如下方法供我们使用
writeByte(byte value);
writeByte(int value);
writeBytes(byte[] value);
writeShort(int value);
writeInt(int value);
writeLong(long value);
writeBool(boolean value);
writeString(String value);
writeFixedString(String value);
writePos(Point value);
skip(int numberOfBytes);

写好创建封包的函数以后,我们要发送封包的话只需要调用发送函数和这个创建函数即可。

由于发送封包的方法有很多,不同的方法有不同的使用场景,有给全地图的玩家发送,有给全服务器的玩家发送,也有只给当前客户端发送的,我就不一一举例了,这里仅以给当前客户端发送为例。

调用方法如下,其中 client 为 Client 对象
  1. client.sendPacket(封包)
复制代码


合起来就是
  1. client.sendPacket(PacketCreator.updateHpMpAlert(hpMpAlertService.getHpAlert(player.getId()),hpMpAlertService.getMpAlert(player.getId())));
复制代码


执行这个方法就可以把这个封包发送给客户端了。


现在讲讲服务端是如何收取封包的

先注册封包 ID,打开 RecvOpcode.java,添加封包 ID,添加方法和 SendOpcode 一样,但是这里的 ID 是客户端那边发送过来的,和前面 SendOpcode 中注册的不一定相同。当然,自定义封包嘛,客户端我们说了算,我们也可以设置成相同的。

然后创建一个处理接收到的封包的方法,我们先在 net/server/channel/handlers 下新建一个类

比如 SetHpMpAlertHandler.java
[attach]35[/attach]

这个类继承自 AbstractPacketHandler

我们重载 handlePacket 方法,参数是固定的 InPacket p, Client c,然后读取这个封包
由于封包的 ID 已经在 handler 之前被解析掉了,所以这里直接读取 ID 之后的数据就可以了
读取数据的方法有这些
  1. byte readByte();
  2. short readUnsignedByte();
  3. short readShort();
  4. int readInt();
  5. long readLong();
  6. Point readPos();
  7. String readString();
  8. byte[] readBytes(int numberOfBytes);
  9. void skip(int numberOfBytes);
  10. int available();
  11. void seek(int byteOffset);
  12. int getPosition();
复制代码


我们执行 byte data = p.readByte(); 就可以读取一个字节存放到 data 变量中了

在这个例子中,我在客户端构造的封包和服务端构造的封包是一模一样的,所以直接拿前面构造的用于发送给客户端的封包结构为参考。
由于封包的ID已经被解析过了,所以封包内容只剩下两个字节,第一个字节是hp百分比,第二个字节是mp百分比,所以第一次p.readByte() 读出来的数据要用于设置hp,第二次读出来的数据要用于mp。这个顺序很重要不能乱。
由于这个方法的逻辑很简单,所以不需要创建变量来存放封包的数据,而是直接调用方法获取数据使用了。

光有这个类还不够,我们还要把这个类注册到拦截器中

打开 PacketProcessor.java

如果你的封包是玩家登录以后使用的,就在 registerChannelHandlers 方法下面添加
  1. registerHandler(RecvOpcode.SET_HPMPALERT, new SetHpMpAlertHandler());
复制代码


注册以后,拦截器就会把接收到的 ID 为 RecvOpcode.SET_HPMPALERT 的封包转发给 SetHpMpAlertHandler 的 handlePacket 方法。

这样服务端就创建了一组收发封包的方法,接下来讲讲
客户端是如何收发封包的

购买主题 已有 3 人购买  本主题需向作者支付 100 蘑菇币 才能浏览
有问题欢迎跟帖提问。

4

主题

16

回帖

13

积分

新手上路

积分
13
发表于 2025-2-22 09:33:01 | 显示全部楼层
Ling總的服務端收發流程寫的好詳細,耐心看就懂了!

4

主题

20

回帖

54

积分

注册会员

积分
54
发表于 2025-8-16 20:27:29 | 显示全部楼层
越来越觉得老冒险岛的客户端设计得太万能了。

有些功能本应服务端做的,最后因为功能实现在客户端,不得不从客户端去设计自定义包,绕一圈再在服务端完成。

4

主题

18

回帖

60

积分

注册会员

积分
60
发表于 2025-9-29 21:50:32 | 显示全部楼层
本帖最后由 江奈Mizuki 于 2025-9-29 21:51 编辑 [/i]

ling总,我今晚跟着这篇帖子走了一遍流程,有两部分的问题想要请教。

第一部分是关于对这篇帖子里代码的copy问题
1.[笔记]帖子中针对“血蓝报警”的收发包做了一个示范。
2.[笔记]对“血蓝报警”的示范内容,在github上的BeiDou项目(6月份git的V1.9版本)已经有了,跟着帖子做的话,都是针对ijl15的项目进行操作。
3.[提问]但是跟着帖子做了之后,不知道做完“应该要看到一个什么现象”。
4.[提问]或许能顺利将ijl15.dll成功打包也是胜利,但打包并不成功,报错集中在Packet.cpp,如CGuiConfig::canInitial、CConfig::SetServerAlert(in)处报“后面有::的一定是类名或命名空间名”,以及SetHpAlert等标识符未定义的问题(是要自己补充吗?还是说我漏了什么、做错了什么...)
  1. #include "stdafx.h"
  2. #include "Packet.h"
  3. #include "CInPacket.h"

  4. typedef int(__fastcall* InPack_Type)(void* pthis, int dummy, CInPacket* in_pack);
  5. static auto _InPacket = reinterpret_cast<InPack_Type>(0x004965F1);

  6. InPack_Type InPackHook = [](void* pThis, int dummy, CInPacket* in_pack) -> int
  7. {
  8.     //ling总的教学:关于自定义血线报警收包
  9.     /*
  10.     CInPacket in;
  11.     in = *in_pack;
  12.     short pId = in.decode<short>();
  13.     switch (pId)
  14.     {
  15.     case 0x7D: // 进入游戏,获取角色信息
  16.         CGuiConfig::canInitial = true;
  17.         break;
  18.         // 自定义
  19.     case 0x1000:
  20.         CConfig::SetServerAlert(in);
  21.         return 0;
  22.     default:
  23.         break;
  24.     }
  25.     return _InPacket(pThis, 0, in_pack);
  26.     */
  27.     return _InPacket(pThis, 0, in_pack);;//必须返回一个值所以加了
  28. };


  29. void Packet::Hook() {
  30.     Memory::SetHook(true, reinterpret_cast<void**>(&_InPacket), InPackHook);
  31. }

  32. //ling总的教学:关于自定义血线报警收包
  33. /*
  34. void SetServerAlert(CInPacket in)
  35. {
  36.     SetHpAlert(in.decode<char>());
  37.     SetMpAlert(in.decode<char>());
  38. }
  39. void update_hpmpalert(const int* hpPer, const int* mpPer)
  40. {
  41.     COutPacket p(0x1000);
  42.     p.EncodeChar(*hpPer);
  43.     p.EncodeChar(*mpPer);
  44.     CClientSocketNew::pins()->SendPacket(&p);
  45. }
  46. */
复制代码

[i]↑我的Packet.cpp文件,中间两段/**/注释取消以后就会报错[/i]


第二部分是关于自己使用的问题
1.[场景]之前我已经通过CSDN博客【一份分享 Imgui Hook MapleStoryV83 -  CSDN App】的教学,在dllmain.cpp里画了一个gui窗口:
  1. HRESULT __stdcall hkEndScene(LPDIRECT3DDEVICE9 pDevice)
  2. {
  3.         if (!g_ImGuiInitialized)
  4.         {
  5.                 g_pd3dDevice = pDevice;
  6.                 HookWndProc();

  7.                 ImGui::CreateContext();

  8.                 ImGuiIO& io = ImGui::GetIO();
  9.                 io.Fonts->AddFontFromFileTTF(
  10.                         "C:\\Windows\\Fonts\\msyh.ttc",
  11.                         16.0f, nullptr, io.Fonts->GetGlyphRangesChineseFull());

  12.                 ImGui_ImplWin32_Init(g_hWnd);
  13.                 ImGui_ImplDX9_Init(pDevice);
  14.                 g_ImGuiInitialized = true;
  15.         }

  16.         // 开始新 Frame
  17.         ImGui_ImplDX9_NewFrame();
  18.         ImGui_ImplWin32_NewFrame();
  19.         ImGui::NewFrame();

  20.         // 设置窗口初始大小和折叠状态 (只在第一次生效)
  21.         ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiCond_Once);
  22.         ImGui::SetNextWindowCollapsed(true, ImGuiCond_Once);

  23.         // 主窗口
  24.         ImGui::Begin(u8"测试窗口");

  25.         ImGui::Text(u8"一个text窗口,随便写点什么:");


  26.         ImGui::End();

  27.         ImGui::EndFrame();
  28.         ImGui::Render();
  29.         ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());

  30.         return oEndScene(pDevice);
  31. }
复制代码

2.[目的]我想在imgui上体现一些关于伤害的数据,第一步至少先实现“客户端收到了记录伤害的数据包,然后在dllmain.cpp里处理有关这个包的信息,在imgui绘制的代码里面调用这个数据”
3.[提问]跟着帖子做的操作都是在Packet.cpp里面处理收包,距离我在dllmain.cpp里使用收包的数据还差一小步,对吧。这剩下的一小步是不是:我要在dllmain.cpp里先
  1. #include "Packet.h"
复制代码

然后在Packet.cpp里定义我要定义的处理包的函数,变成函数和变量,最后到dllmain.cpp里使用?
4.[提问]以上也是一个我基于imgui实现伤害观测的思路,我目前觉得应该是效率比较高的一种方案。想确认一下对于实现伤害观测的目的来说,我是否舍近求远了,是否有更方便的方式?

45

主题

45

回帖

379

积分

管理员

积分
379
 楼主| 发表于 2025-9-29 22:18:13 | 显示全部楼层
江奈Mizuki 发表于 2025-9-29 21:50
ling总,我今晚跟着这篇帖子走了一遍流程,有两部分的问题想要请教。

第一部分是关于对这篇帖子里代码的co ...


正常的血蓝报警是有问题,一台机器上所有账号共用一套设置,这个设置是存储在机器本地的,比如你在角色A上设置HP 50% 报警,那你换到角色B上也会是 50%,你改成 10% 然后切回角色A也还是 10%


另外这个百分比配合宠物自动吃药的逻辑是,每次掉血后,当前HP低于设置的百分比时,会发一次封包告诉服务器,然后服务器吃一次药(服务端并不知道你设置的百分比是多少,所以只能通过你发一次封包,服务器就吃一瓶药来实现)。


这又出现另一个问题,比如你设置 50%,你的血量被死光打中只剩下1 HP,因为只受到一次伤害,所以客户端也只进行了一次判断,认为你血量低于 50 %,然后发一次封包给服务端,服务端吃一次药。这样的操作并不能保证你的HP回复到你设置的50%,你很容易就会被下次伤害打死。又或者另外一种,一些情况下你疯狂用技能,当你MP低于设置的百分比后,客户端疯狂发包叫服务端吃MP药(发包速度甚至超过你丢技能的速度),服务端就会疯狂吃药,结果导致吃药过多而浪费。


我这个例子通过这个设置的百分比值发送并保存在服务端,达到不同角色在同一个机器(或不同机器)都能读取到设置的百分比值。并且宠物自动吃药的地方可以在服务端判断,是否已经达到设置的安全线,达到就不再吃药。


这些代码都是我从自己的插件里摘出来的,已经整理过一次了,可能还有些多余的没删干净,你要自己学会看,哪些是多余的就删掉。还有一些方法可能要在`.h`文件里定义啥的,都是基础问题。

具体的你要把错误日志发出来,别人才能分析是什么问题。

还有,我建议你先把C++和JAVA的入门课学完再折腾这些,问题会少很多
有问题欢迎跟帖提问。

4

主题

18

回帖

60

积分

注册会员

积分
60
发表于 2025-9-30 10:54:08 | 显示全部楼层

谢谢,我理解例子的作用了。
也很抱歉,我不应该把如何包含头文件这种问题拿出来问,之后会多做点功课以后再问的。。。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|小黑屋|蘑菇物语

GMT+8, 2025-10-21 21:50 , Processed in 0.058507 second(s), 29 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表