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:
@@ -183,6 +183,26 @@ int main() {
|
||||
"on_char_list fired with renamed slot");
|
||||
}
|
||||
|
||||
// --- GC 9 create failure: input_login.cpp:442-465 sends a zeroed 10-byte
|
||||
// TPacketGCLoginFailure under header 9 (blocked creation / bad name). The 40250
|
||||
// client reads TPacketGCCreateFailure (2 bytes) and PythonNetworkStream.cpp:509
|
||||
// skips the trailing zero bytes as blank headers. input_db.cpp:196 sends 2 bytes. ---
|
||||
{
|
||||
s.parser().drain_char_events();
|
||||
std::vector<uint8_t> cf(10, 0);
|
||||
cf[0] = HDR_GC_CREATE_FAILURE;
|
||||
GCPlayerCreateFailure dup{HDR_GC_CREATE_FAILURE, 1};
|
||||
std::vector<uint8_t> db = raw(dup);
|
||||
cf.insert(cf.end(), db.begin(), db.end());
|
||||
feed(s, cf);
|
||||
auto ev = s.parser().drain_char_events();
|
||||
CHECK(ev.size() == 2 && ev[0].kind == ClassicParser::CharEvent::CreateFail &&
|
||||
ev[0].fail_type == 0 && ev[1].kind == ClassicParser::CharEvent::CreateFail &&
|
||||
ev[1].fail_type == 1,
|
||||
"10-byte and 2-byte GC 9 create failures both decode like 40250");
|
||||
CHECK(s.last_error().empty(), "no desync after 10-byte GC 9");
|
||||
}
|
||||
|
||||
// --- select_char(0) -> CG_CHARACTER_SELECT + seq[1] ---
|
||||
{
|
||||
CHECK(s.select_char(0), "select_char(0) ok");
|
||||
@@ -523,9 +543,84 @@ int main() {
|
||||
feed(s, raw(iu));
|
||||
CHECK(s.world().item_slot(1, 4).count == 3, "item_update count -> 3");
|
||||
|
||||
GCItemDel id{HDR_GC_ITEM_DEL, 4};
|
||||
feed(s, raw(id));
|
||||
// GC 20 is TPacketGCItemDelDeprecated on the 40250 server (char_item.cpp:424,
|
||||
// packet.h:1124): header, TItemPos, vnum, count, alSockets[3], aAttr[7] = 42
|
||||
// bytes; the 40250 client reads it as ITEM_SET. Sent on inventory load, so feed
|
||||
// it glued to the next packet: a short frame desyncs the loading stream.
|
||||
std::vector<uint8_t> del(42, 0);
|
||||
del[0] = HDR_GC_ITEM_DEL;
|
||||
del[1] = 1; // WINDOW_INVENTORY
|
||||
del[2] = 4; // cell 4 (u16 LE)
|
||||
GCItemSet next{};
|
||||
next.header = HDR_GC_ITEM_SET;
|
||||
next.cell = {1, 7};
|
||||
next.vnum = 11200;
|
||||
next.count = 2;
|
||||
std::vector<uint8_t> glued = del;
|
||||
std::vector<uint8_t> nb = raw(next);
|
||||
glued.insert(glued.end(), nb.begin(), nb.end());
|
||||
feed(s, glued);
|
||||
CHECK(s.world().item_slot(1, 4).vnum == 0, "item_del cleared inventory cell 4");
|
||||
CHECK(s.world().item_slot(1, 7).vnum == 11200 && s.world().item_slot(1, 7).count == 2,
|
||||
"packet after 42-byte GC 20 framed correctly");
|
||||
CHECK(s.last_error().empty(), "no desync after GC 20");
|
||||
|
||||
// Same wire layout with a vnum: the 40250 client applies it as an item set.
|
||||
std::vector<uint8_t> set20(42, 0);
|
||||
set20[0] = HDR_GC_ITEM_DEL;
|
||||
set20[1] = 1;
|
||||
set20[2] = 9;
|
||||
const uint32_t vnum20 = 27001;
|
||||
std::memcpy(&set20[4], &vnum20, 4);
|
||||
set20[8] = 5; // count
|
||||
const int32_t sock0 = 28030;
|
||||
std::memcpy(&set20[9], &sock0, 4);
|
||||
set20[21] = 7; // aAttr[0].bType
|
||||
const int16_t av = 15;
|
||||
std::memcpy(&set20[22], &av, 2);
|
||||
feed(s, set20);
|
||||
const mtnet::Item &it20 = s.world().item_slot(1, 9);
|
||||
CHECK(it20.vnum == 27001 && it20.count == 5 && it20.sockets[0] == 28030 &&
|
||||
it20.attrs[0].type == 7 && it20.attrs[0].value == 15,
|
||||
"GC 20 with vnum sets the cell like the 40250 client");
|
||||
s.world().mut_item_del(1, 9);
|
||||
}
|
||||
|
||||
// --- GC 95 HEADER_GC_REFINE_INFORMATION: the 40250 client registers it with
|
||||
// TPacketGCRefineInformation = [hdr][pos][TRefineTable] = 59 bytes (no type byte;
|
||||
// only 119 _NEW carries type). Glue a packet after it to prove the frame length. ---
|
||||
{
|
||||
s.world().drain_refine_cues();
|
||||
std::vector<uint8_t> rf(59, 0);
|
||||
rf[0] = 95;
|
||||
rf[1] = 6; // pos
|
||||
const uint32_t src = 11209, dst = 11210, mat_vnum = 30053;
|
||||
const int32_t cost = 5000, prob = 90, mat_count = 2;
|
||||
std::memcpy(&rf[2], &src, 4);
|
||||
std::memcpy(&rf[6], &dst, 4);
|
||||
rf[10] = 1; // material_count
|
||||
std::memcpy(&rf[11], &cost, 4);
|
||||
std::memcpy(&rf[15], &prob, 4);
|
||||
std::memcpy(&rf[19], &mat_vnum, 4);
|
||||
std::memcpy(&rf[23], &mat_count, 4);
|
||||
GCItemSet after{};
|
||||
after.header = HDR_GC_ITEM_SET;
|
||||
after.cell = {1, 11};
|
||||
after.vnum = 11210;
|
||||
after.count = 1;
|
||||
std::vector<uint8_t> ab = raw(after);
|
||||
rf.insert(rf.end(), ab.begin(), ab.end());
|
||||
feed(s, rf);
|
||||
auto cues = s.world().drain_refine_cues();
|
||||
CHECK(cues.size() == 1 && cues[0].type == 0 && cues[0].pos == 6 &&
|
||||
cues[0].src_vnum == 11209 && cues[0].result_vnum == 11210 &&
|
||||
cues[0].material_count == 1 && cues[0].cost == 5000 &&
|
||||
cues[0].prob == 90 && cues[0].materials[0].vnum == 30053 &&
|
||||
cues[0].materials[0].count == 2,
|
||||
"GC 95 refine information uses the 59-byte 40250 layout");
|
||||
CHECK(s.world().item_slot(1, 11).vnum == 11210, "packet after GC 95 framed correctly");
|
||||
CHECK(s.last_error().empty(), "no desync after GC 95");
|
||||
s.world().mut_item_del(1, 11);
|
||||
}
|
||||
|
||||
// --- GC entity state: UPDATE(19) / CHANGE_SPEED(18) / POSITION(43) / MOTION(36) /
|
||||
@@ -743,22 +838,26 @@ int main() {
|
||||
cf[0].request_pid == 555,
|
||||
"quest confirm cue");
|
||||
|
||||
// GC_QUEST_INFO dynamic: [hdr 81][u16 size][u16 index][u8 flag][TITLE\0][COUNTER_NAME\0][i32]
|
||||
std::vector<uint8_t> qb;
|
||||
// GC_QUEST_INFO (questpc.cpp PC::SendQuestInfoPakcet): [hdr 81][u16 size][u16 index]
|
||||
// [u8 flag] then fixed-width optional fields (title 31, counter name 17, i32). The
|
||||
// head is written before size is bumped, so the wire size stays 6.
|
||||
std::vector<uint8_t> qpkt(QUEST_INFO_HEAD_SIZE, 0);
|
||||
qpkt[0] = HDR_GC_QUEST_INFO;
|
||||
uint16_t qsz = QUEST_INFO_HEAD_SIZE;
|
||||
std::memcpy(&qpkt[1], &qsz, 2);
|
||||
uint16_t qidx = 3;
|
||||
qb.insert(qb.end(), (uint8_t *)&qidx, (uint8_t *)&qidx + 2);
|
||||
uint8_t qflag = QUEST_SEND_TITLE | QUEST_SEND_COUNTER_NAME | QUEST_SEND_COUNTER_VALUE;
|
||||
qb.push_back(qflag);
|
||||
const char *qt = "Kill 10 wolves";
|
||||
qb.insert(qb.end(), qt, qt + std::strlen(qt) + 1);
|
||||
const char *cn = "Wolves";
|
||||
qb.insert(qb.end(), cn, cn + std::strlen(cn) + 1);
|
||||
std::memcpy(&qpkt[3], &qidx, 2);
|
||||
qpkt[5] = QUEST_SEND_TITLE | QUEST_SEND_COUNTER_NAME | QUEST_SEND_COUNTER_VALUE;
|
||||
std::vector<uint8_t> qt(QUEST_INFO_TITLE_SIZE, 0);
|
||||
std::memcpy(qt.data(), "Kill 10 wolves", 14);
|
||||
qpkt.insert(qpkt.end(), qt.begin(), qt.end());
|
||||
std::vector<uint8_t> cn(QUEST_INFO_COUNTER_NAME_SIZE, 0);
|
||||
std::memcpy(cn.data(), "Wolves", 6);
|
||||
qpkt.insert(qpkt.end(), cn.begin(), cn.end());
|
||||
int32_t cv = 4;
|
||||
qb.insert(qb.end(), (uint8_t *)&cv, (uint8_t *)&cv + 4);
|
||||
uint16_t qsz = (uint16_t)(3 + qb.size());
|
||||
std::vector<uint8_t> qpkt = {HDR_GC_QUEST_INFO};
|
||||
qpkt.insert(qpkt.end(), (uint8_t *)&qsz, (uint8_t *)&qsz + 2);
|
||||
qpkt.insert(qpkt.end(), qb.begin(), qb.end());
|
||||
qpkt.insert(qpkt.end(), (uint8_t *)&cv, (uint8_t *)&cv + 4);
|
||||
CHECK(qpkt.size() == static_cast<size_t>(quest_info_packet_size(qpkt[5])),
|
||||
"quest info test packet matches 40250 flag-driven length");
|
||||
feed(s, qpkt);
|
||||
const mtnet::QuestInfo *q = s.world().quest(3);
|
||||
CHECK(q && q->title == "Kill 10 wolves" && q->counter_name == "Wolves" &&
|
||||
|
||||
Reference in New Issue
Block a user