fix(net): 修复 40250 进入游戏时的收包错位

- GC_ITEM_DEL(20) 按 40250 旧结构为 42 字节,GC_REFINE_INFORMATION(95) 为 59 字节
- 公会标记连接接受 GC_MARK_DIFF_DATA(101),按 1 字节消费
- GUILD_SUBHEADER_GC_SKILL_INFO 服务器声明 22 字节但实际只写 21 字节,
  按实际长度分帧,修复 "unknown GC header 200 (last: 75,20)" 断线
- 会话测试的 quest info 包改为 40250 按 flag 定长的格式,新增对应分帧测试

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
This commit is contained in:
shenlei
2026-09-12 21:29:21 +09:00
co-authored by Claude Opus 5
parent 474a709f69
commit 85ce75e707
7 changed files with 239 additions and 23 deletions
@@ -168,6 +168,10 @@ private:
if (header == HDR_GC_MARK_BLOCK) {
return receive_block(packet, available);
}
if (header == HDR_GC_MARK_DIFF_DATA) {
// GuildMarkDownloader.cpp: sizeof(BYTE), dispatch returns true.
return raw(ClassicStream::RawPacketStatus::Consumed, 1);
}
return raw(ClassicStream::RawPacketStatus::NotHandled);
}
+25 -4
View File
@@ -326,12 +326,20 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
p.anti_flags, p.sockets, a);
return true;
}
case HDR_GC_ITEM_DEL: { // 20 — inventory pos only
GCItemDel p;
case HDR_GC_ITEM_DEL: { // 20 — TPacketGCItemDelDeprecated, read as ITEM_SET by 40250
GCItemDelDeprecated p;
if (!fill(p, header, body, len)) {
return false;
}
m_world.mut_item_del(/*WINDOW_INVENTORY*/ 1, p.pos);
if (p.vnum == 0) {
m_world.mut_item_del(p.cell.window_type, p.cell.cell);
return true;
}
mtnet::ItemAttr a[ITEM_ATTRIBUTE_MAX_NUM];
for (int i = 0; i < ITEM_ATTRIBUTE_MAX_NUM; ++i) {
a[i] = {p.attrs[i].type, p.attrs[i].value};
}
m_world.mut_item_set(p.cell.window_type, p.cell.cell, p.vnum, p.count, 0, 0, p.sockets, a);
return true;
}
case HDR_GC_ITEM_UPDATE: { // 25
@@ -1331,7 +1339,20 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
m_world.mut_observer(ObserverEvent::Remove, packet.vid, 0, 0);
return true;
}
case HDR_GC_REFINE_INFORMATION_OLD:
case HDR_GC_REFINE_INFORMATION_OLD: { // 95 — RecvRefineInformationPacket, no type
GCRefineInfoOld packet;
if (!fill(packet, header, body, len)) {
return false;
}
RefineCue::Mat materials[5] = {};
for (int i = 0; i < 5; ++i) {
materials[i].vnum = packet.materials[i].vnum;
materials[i].count = packet.materials[i].count;
}
m_world.mut_refine(0, packet.pos, packet.src_vnum, packet.result_vnum,
packet.material_count, packet.cost, packet.prob, materials);
return true;
}
case HDR_GC_REFINE_INFORMATION: {
GCRefineInfo packet;
if (!fill(packet, header, body, len)) {
@@ -518,6 +518,16 @@ void ClassicStream::dispatch() {
}
framed = static_cast<uint32_t>(
quest_info_packet_size(head[QUEST_INFO_HEAD_SIZE - 1]));
} else if (header == HDR_GC_GUILD) {
// GUILD_SUBHEADER_GC_SKILL_INFO reports one byte more than the
// server writes (see GUILD_SKILL_INFO_PACKET_SIZE).
uint8_t head[sizeof(DynHead) + 1];
if (!m_recv.peek(head, sizeof(head))) {
return;
}
if (head[sizeof(DynHead)] == GUILD_SUBHEADER_GC_SKILL_INFO) {
framed = GUILD_SKILL_INFO_PACKET_SIZE;
}
}
if (m_recv.readable() < framed) {
return; // whole packet not here yet
+38 -3
View File
@@ -282,6 +282,7 @@ enum : uint8_t {
HDR_GC_OBSERVER_MOVE = 98,
HDR_GC_VIEW_EQUIP = 99,
HDR_GC_MARK_BLOCK = 100,
HDR_GC_MARK_DIFF_DATA = 101, // mark connection only: bare header, ignored
HDR_GC_MARK_IDXLIST = 102,
HDR_GC_TIME = 106,
HDR_GC_CHANGE_NAME = 107,
@@ -944,6 +945,19 @@ struct GCViewEquip {
};
struct GCChangeName { uint8_t header; uint32_t pid; char name[CHARACTER_NAME_MAX_LEN + 1]; };
struct GCRefineMaterial { uint32_t vnum; int32_t count; };
// Header 95: client TPacketGCRefineInformation = [hdr][pos][TRefineTable], no type.
struct GCRefineInfoOld {
uint8_t header;
uint8_t pos;
uint32_t src_vnum;
uint32_t result_vnum;
uint8_t material_count;
int32_t cost;
int32_t prob;
GCRefineMaterial materials[5];
};
static_assert(sizeof(GCRefineInfoOld) == 59);
// Header 119: server TPacketGCRefineInformation == client TPacketGCRefineInformationNew.
struct GCRefineInfo {
uint8_t header;
uint8_t type;
@@ -1148,9 +1162,22 @@ struct GCItemSet { // packet_item_set (server packet.h:1134, header 21)
};
static_assert(sizeof(GCItemSet) == 1 + 3 + 4 + 1 + 4 + 4 + 1 + 12 + 21); // 51
struct GCItemDel { uint8_t header; uint8_t pos; }; // header 20 — inventory pos only
struct GCItemDel { uint8_t header; uint8_t pos; }; // TPacketGCItemDel — SAFEBOX_DEL / MALL_DEL
static_assert(sizeof(GCItemDel) == 2);
// Header 20: the 40250 server sends TPacketGCItemDelDeprecated (packet.h:1124,
// char_item.cpp:424) when an inventory cell empties; the 40250 client reads the
// same 42 bytes as its HEADER_GC_ITEM_SET (no flags/anti_flags/highlight).
struct GCItemDelDeprecated {
uint8_t header;
ItemPos cell;
uint32_t vnum;
uint8_t count;
int32_t sockets[ITEM_SOCKET_MAX_NUM];
ItemAttr3 attrs[ITEM_ATTRIBUTE_MAX_NUM];
};
static_assert(sizeof(GCItemDelDeprecated) == 1 + 3 + 4 + 1 + 12 + 21); // 42
// GC_MESSENGER is dynamic: after [header][u16 size], body starts with the
// subheader and then the subheader-specific records below.
struct GCMessengerHead { uint8_t header; uint16_t size; uint8_t subheader; };
@@ -1249,6 +1276,14 @@ static_assert(sizeof(GuildMember38) == 38 && sizeof(GuildInfo35) == 35 &&
sizeof(GuildName16) == 16 && sizeof(GuildSkill17) == 17 &&
sizeof(GuildComment80) == 80 && sizeof(GuildInvite17) == 17);
// CGuild::SendSkillInfoPacket() (guild.cpp) announces
// `size = sizeof(pack) + 6 + GUILD_SKILL_COUNT` (22) but writes only
// skill_point, abySkill[12], power(2) and max_power(2) after the head — 21
// bytes. The original client's RecvGuild() reads those fields one by one, so
// framing off the size field would swallow the next packet's header byte.
inline constexpr uint8_t GUILD_SUBHEADER_GC_SKILL_INFO = 12;
inline constexpr int GUILD_SKILL_INFO_PACKET_SIZE = 4 + sizeof(GuildSkill17); // 21
// --- chat (dynamic) ---
struct CGChatHead { uint8_t header; uint16_t length; uint8_t type; }; // + char szChat[]
static_assert(sizeof(CGChatHead) == 4);
@@ -1382,7 +1417,7 @@ constexpr int packet_size_gc(uint8_t h) {
case HDR_GC_CHARACTER_POINTS: return sizeof(GCPoints);
case HDR_GC_CHARACTER_POINT_CHANGE: return sizeof(GCPointChange);
case HDR_GC_ITEM_SET: return sizeof(GCItemSet);
case HDR_GC_ITEM_DEL: return sizeof(GCItemDel);
case HDR_GC_ITEM_DEL: return sizeof(GCItemDelDeprecated);
case HDR_GC_ITEM_USE: return sizeof(GCItemUse);
case HDR_GC_ITEM_UPDATE: return sizeof(GCItemUpdate);
case HDR_GC_ITEM_GROUND_ADD: return sizeof(GCItemGroundAdd);
@@ -1447,7 +1482,7 @@ constexpr int packet_size_gc(uint8_t h) {
case HDR_GC_LOVE_POINT_UPDATE: return sizeof(GCLovePointUpdate);
case HDR_GC_DIG_MOTION: return sizeof(GCDigMotion);
case HDR_GC_VIEW_EQUIP: return sizeof(GCViewEquip);
case HDR_GC_REFINE_INFORMATION_OLD:
case HDR_GC_REFINE_INFORMATION_OLD: return sizeof(GCRefineInfoOld);
case HDR_GC_REFINE_INFORMATION: return sizeof(GCRefineInfo);
case HDR_GC_DRAGON_SOUL_REFINE: return sizeof(GCDragonSoulRefine);
case HDR_GC_QUEST_CONFIRM: return sizeof(GCQuestConfirm);