// See port_login_flow_server.h. Each step names the 40250 server function whose behaviour it plays. #include "port_login_flow_server.h" #include "../src/net/classic/classic_cipher.h" #include "../src/net/classic/sequence_table.h" #include "../src/net/classic/wire_classic.h" #include #include #include #include #include #include #include #include using namespace mtnet::classic; namespace { constexpr std::uint32_t kHandshake = 0x2468ace0; constexpr int kHandshakeRetryLimit = 32; // game/src/desc.h HANDSHAKE_RETRY_LIMIT constexpr int kIdleMs = 20000; // get_dword_time(): milliseconds on the server's clock. std::uint32_t now_ms() { using namespace std::chrono; static const auto start = steady_clock::now(); return static_cast(duration_cast(steady_clock::now() - start).count()); } int listen_loopback(int* port) { int fd = socket(AF_INET, SOCK_STREAM, 0); int on = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); sockaddr_in addr = {}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); if (bind(fd, reinterpret_cast(&addr), sizeof(addr)) != 0 || listen(fd, 4) != 0) { close(fd); return -1; } socklen_t len = sizeof(addr); getsockname(fd, reinterpret_cast(&addr), &len); *port = ntohs(addr.sin_port); return fd; } template T zeroed() { T value; std::memset(&value, 0, sizeof(value)); return value; } } // namespace // One DESC: the socket, its cipher and the sequence position of the next client packet. struct FakeLoginServer::Connection { int fd = -1; ClassicCipher cipher; size_t sequence = 0; const std::atomic* stop = nullptr; ~Connection() { if (fd >= 0) close(fd); } // DESC::Packet: encrypted once the key agreement finished. bool Send(const void* data, size_t size) { std::vector bytes(static_cast(data), static_cast(data) + size); if (cipher.activated()) cipher.encrypt(bytes.data(), bytes.size()); size_t sent = 0; while (sent < bytes.size()) { ssize_t n = send(fd, bytes.data() + sent, bytes.size() - sent, 0); if (n <= 0) return false; sent += static_cast(n); } return true; } template bool Send(const T& packet) { return Send(&packet, sizeof(packet)); } // DESC::SetPhase sends the phase packet. bool Phase(std::uint8_t phase) { Phase_ p = {HDR_GC_PHASE, phase}; return Send(p); } // Reads exactly `size` bytes, or returns 0 on EOF / -1 on timeout, stop or error. int Read(void* data, size_t size) { auto* p = static_cast(data); size_t got = 0; for (int waited = 0; got < size;) { if (stop->load()) return -1; pollfd pfd = {fd, POLLIN, 0}; int ready = poll(&pfd, 1, 50); if (ready == 0) { if ((waited += 50) >= kIdleMs) return -1; continue; } ssize_t n = recv(fd, p + got, size - got, 0); if (n == 0) return got == 0 ? 0 : -1; if (n < 0) return -1; got += static_cast(n); } if (cipher.activated()) cipher.decrypt(data, size); return 1; } }; FakeLoginServer::~FakeLoginServer() { Stop(); } bool FakeLoginServer::Start() { m_auth_listener = listen_loopback(&m_auth_port); m_game_listener = listen_loopback(&m_game_port); m_state_listener = listen_loopback(&m_state_port); if (m_auth_listener < 0 || m_game_listener < 0 || m_state_listener < 0) return false; m_thread = std::thread([this] { Run(); }); m_state_thread = std::thread([this] { ServeState(); }); return true; } void FakeLoginServer::Stop() { m_stop = true; if (m_thread.joinable()) m_thread.join(); if (m_state_thread.joinable()) m_state_thread.join(); for (int* fd : {&m_auth_listener, &m_game_listener, &m_state_listener}) if (*fd >= 0) { close(*fd); *fd = -1; } } std::vector FakeLoginServer::Events() { std::lock_guard lock(m_mutex); return m_events; } std::string FakeLoginServer::Error() { std::lock_guard lock(m_mutex); return m_error; } bool FakeLoginServer::Has(const std::string& event) { std::lock_guard lock(m_mutex); for (const auto& e : m_events) if (e == event) return true; return false; } void FakeLoginServer::Event(const std::string& event) { std::lock_guard lock(m_mutex); m_events.push_back(event); } bool FakeLoginServer::Fail(const std::string& error) { std::lock_guard lock(m_mutex); if (m_error.empty()) m_error = error; return false; } bool FakeLoginServer::Accept(int listener, Connection& c) { c.stop = &m_stop; for (int waited = 0; !m_stop; waited += 50) { pollfd pfd = {listener, POLLIN, 0}; if (poll(&pfd, 1, 50) > 0) { c.fd = accept(listener, nullptr, nullptr); return c.fd >= 0; } if (waited >= kIdleMs) return Fail("no client connected"); } return false; } void FakeLoginServer::Run() { { Connection auth; if (!Accept(m_auth_listener, auth) || !ServeAuth(auth)) return; } // The first game connection ends at the character list; selecting a character reconnects // (net.DirectEnter → CPythonNetworkStream::ConnectGameServer) and that one goes on to loading. for (int index = 1; !m_stop; ++index) { Connection game; if (!Accept(m_game_listener, game) || !ServeGame(game, index)) return; } } // DESC::Setup → SetPhase(PHASE_HANDSHAKE) + StartHandshake; CInputProcessor::Handshake / // DESC::HandshakeProcess; SendKeyAgreement; CInputHandshake::Analyze(HEADER_CG_KEY_AGREEMENT). bool FakeLoginServer::Handshake(Connection& c, std::uint8_t phase_after) { if (!c.Phase(PHASE_HANDSHAKE)) return Fail("send PHASE_HANDSHAKE"); mtnet::classic::Handshake out = {HDR_HANDSHAKE, kHandshake, now_ms(), 0}; std::uint32_t sent_time = out.time; if (!c.Send(out)) return Fail("send GC_HANDSHAKE"); for (int retry = 0;; ++retry) { auto in = zeroed(); if (c.Read(&in, sizeof(in)) != 1 || in.header != HDR_HANDSHAKE) return Fail("expected CG_HANDSHAKE"); if (in.handshake != kHandshake) return Fail("CG_HANDSHAKE carries the wrong handshake id"); const std::uint32_t now = now_ms(); const int bias = static_cast(now - (in.time + in.delta)); if (in.delta >= 0 && bias >= 0 && bias <= 50) break; if (retry + 1 > kHandshakeRetryLimit) return Fail("handshake retry limit reached"); std::int32_t delta = static_cast(now - in.time) / 2; if (delta < 0) delta = static_cast(now - sent_time) / 2; out = {HDR_HANDSHAKE, kHandshake, now, delta}; sent_time = now; if (!c.Send(out)) return Fail("send GC_HANDSHAKE retry"); } auto agreement = zeroed(); size_t data_length = KEY_AGREEMENT_MAX_DATA_LEN; const size_t agreed_length = c.cipher.prepare(agreement.data, &data_length); if (agreed_length == 0) return Fail("cipher prepare"); agreement.header = HDR_KEY_AGREEMENT; agreement.agreed_length = static_cast(agreed_length); agreement.data_length = static_cast(data_length); if (!c.Send(agreement)) return Fail("send GC_KEY_AGREEMENT"); auto reply = zeroed(); if (c.Read(&reply, sizeof(reply)) != 1 || reply.header != HDR_KEY_AGREEMENT) return Fail("expected CG_KEY_AGREEMENT"); // "Send out the key agreement completion packet first to help client to enter encryption mode". KeyAgreementCompleted completed = {HDR_GC_KEY_AGREEMENT_COMPLETED, {}}; if (!c.Send(completed)) return Fail("send GC_KEY_AGREEMENT_COMPLETED"); if (!c.cipher.activate(false, reply.agreed_length, reply.data, reply.data_length)) return Fail("key agreement failed"); c.cipher.set_activated(true); return c.Phase(phase_after) || Fail("send phase after handshake"); } // CServerStateChecker::Request connects to the first channel and sends the bare header. The game server // has already started the handshake (DESC::Setup), so the client reads past those bytes one per Update; // CInputHandshake::Analyze(HEADER_CG_STATE_CHECKER) asks the db, and CInputDB::RespondChannelStatus writes // the header, the count, the TChannelStatus {short nPort; BYTE bStatus} array and a success byte. void FakeLoginServer::ServeState() { Connection c; c.stop = &m_stop; for (;;) { if (m_stop) return; pollfd pfd = {m_state_listener, POLLIN, 0}; if (poll(&pfd, 1, 50) > 0) break; } c.fd = accept(m_state_listener, nullptr, nullptr); if (c.fd < 0) return; mtnet::classic::Handshake hs = {HDR_HANDSHAKE, kHandshake, now_ms(), 0}; if (!c.Phase(PHASE_HANDSHAKE) || !c.Send(hs)) return; std::uint8_t header = 0; if (c.Read(&header, 1) != 1 || header != 206) { Fail("state checker: expected CG_STATE_CHECKER"); return; } Event("state:checker"); std::vector out = {210}; const std::int32_t count = 2; out.insert(out.end(), reinterpret_cast(&count), reinterpret_cast(&count) + 4); for (int port : {m_state_port, kOtherChannelPort}) { const std::int16_t short_port = static_cast(port); out.insert(out.end(), reinterpret_cast(&short_port), reinterpret_cast(&short_port) + 2); out.push_back(static_cast(kOtherChannelStatus)); } out.push_back(1); c.Send(out.data(), out.size()); // Keep the socket open until the client disconnects (CServerStateChecker::Initialize). std::uint8_t rest; while (c.Read(&rest, 1) == 1) ; } // Reads one client packet of a known header: the static size from packet_info.cpp plus the sequence // byte CInputProcessor::Process checks against gc_abSequence. static int read_packet(FakeLoginServer::Connection& c, std::uint8_t expect, void* packet, size_t size, std::string* error) { auto* bytes = static_cast(packet); int got = c.Read(bytes, 1); if (got != 1) return got; if (bytes[0] != expect) { char text[64]; std::snprintf(text, sizeof(text), "expected CG header %u, got %u", expect, bytes[0]); *error = text; return -1; } if (static_cast(packet_size_cg(expect)) != size) { *error = "size table disagrees with the packet struct"; return -1; } if (size > 1 && c.Read(bytes + 1, size - 1) != 1) return -1; if (is_sequence_cg(expect)) { std::uint8_t sequence = 0; if (c.Read(&sequence, 1) != 1) return -1; const std::uint8_t want = SEQUENCE_TABLE[c.sequence]; if (sequence != want) { char text[80]; std::snprintf(text, sizeof(text), "SEQUENCE mismatch 0x%x != 0x%x header %u", want, sequence, expect); *error = text; return -1; } c.sequence = (c.sequence + 1) % SEQUENCE_TABLE_SIZE; } return 1; } // g_bAuthServer: CInputAuth::Login → GC_AUTH_SUCCESS with the login key the game server will accept. bool FakeLoginServer::ServeAuth(Connection& c) { if (!Handshake(c, PHASE_AUTH)) return false; Event("auth:handshake"); auto login = zeroed(); std::string error; if (read_packet(c, HDR_CG_LOGIN3, &login, sizeof(login), &error) != 1) return Fail("auth: CG_LOGIN3: " + error); if (std::strcmp(login.login, kLogin) != 0 || std::strcmp(login.passwd, kPassword) != 0) return Fail("auth: CG_LOGIN3 carries the wrong account"); Event("auth:login3"); GCAuthSuccess success = {HDR_GC_AUTH_SUCCESS, kLoginKey, 1}; if (!c.Send(success)) return Fail("auth: send GC_AUTH_SUCCESS"); // CAccountConnector connects to the game server and then disconnects from us. std::uint8_t byte = 0; if (c.Read(&byte, 1) != 0) return Fail("auth: expected the client to hang up"); Event("auth:closed"); return true; } bool FakeLoginServer::ServeGame(Connection& c, int index) { const std::string name = "game" + std::to_string(index); if (!Handshake(c, PHASE_LOGIN)) return false; Event(name + ":handshake"); // CInputLogin::LoginByKey → (DB) CInputDB::LoginSuccess: GC_EMPIRE, SetPhase(PHASE_SELECT), // SendLoginSuccessPacket. auto login = zeroed(); std::string error; if (read_packet(c, HDR_CG_LOGIN2, &login, sizeof(login), &error) != 1) return Fail(name + ": CG_LOGIN2: " + error); if (std::strcmp(login.login, kLogin) != 0 || login.login_key != kLoginKey) return Fail(name + ": CG_LOGIN2 carries the wrong account or login key"); Event(name + ":login2"); GCEmpire empire = {HDR_GC_EMPIRE, 1}; auto success = zeroed(); success.header = HDR_GC_LOGIN_SUCCESS_NEWSLOT; SimplePlayer& player = success.players[0]; player.id = 1; std::strncpy(player.name, kCharacterName, sizeof(player.name) - 1); player.job = 0; player.level = 10; player.st = player.ht = player.dx = player.iq = 5; player.x = kX; player.y = kY; player.addr = static_cast(htonl(INADDR_LOOPBACK)); player.port = static_cast(m_game_port); success.handle = static_cast(index); success.random_key = 0x5a5a5a5a; if (!c.Send(empire) || !c.Phase(PHASE_SELECT) || !c.Send(success)) return Fail(name + ": send login success"); // The character list screen: the client either reconnects (DirectEnter) or selects here. auto select = zeroed(); const int got = read_packet(c, HDR_CG_CHARACTER_SELECT, &select, sizeof(select), &error); if (got == 0) { Event(name + ":closed"); return true; } if (got != 1) return Fail(name + ": CG_CHARACTER_SELECT: " + error); if (select.player_index != 0) return Fail(name + ": CG_CHARACTER_SELECT picks an empty slot"); Event(name + ":select"); // CInputDB::PlayerLoad: SetPhase(PHASE_LOADING), CHARACTER::MainCharacterPacket, PointsPacket, // SkillLevelPacket (1531 bytes; guards the client's 32-bit time_t TPlayerSkill layout). auto main = zeroed(); main.header = HDR_GC_MAIN_CHARACTER; main.vid = kMainVID; main.race = 0; std::strncpy(main.name, kCharacterName, sizeof(main.name) - 1); main.x = kX; main.y = kY; main.empire = 1; auto points = zeroed(); points.header = HDR_GC_CHARACTER_POINTS; points.points[1] = 10; // POINT_LEVEL points.points[11] = kStartGold; // POINT_GOLD auto skill = zeroed(); skill.header = HDR_GC_SKILL_LEVEL; if (!c.Phase(PHASE_LOADING) || !c.Send(main) || !c.Send(points) || !c.Send(skill)) return Fail(name + ": send loading packets"); // The client answers the main character with its version (CInputProcessor::Version). std::uint8_t header = 0; if (c.Read(&header, 1) != 1) return Fail(name + ": expected CG_CLIENT_VERSION"); const size_t size = header == HDR_CG_CLIENT_VERSION2 ? sizeof(CGClientVersion2) : header == HDR_CG_CLIENT_VERSION ? sizeof(CGClientVersion) : 0; if (!size) return Fail(name + ": expected CG_CLIENT_VERSION, got header " + std::to_string(header)); std::vector rest(size - 1 + 1); if (c.Read(rest.data(), rest.size()) != 1 || rest.back() != SEQUENCE_TABLE[c.sequence]) return Fail(name + ": CG_CLIENT_VERSION body or sequence"); c.sequence = (c.sequence + 1) % SEQUENCE_TABLE_SIZE; Event(name + ":client_version"); // game.GameWindow.Open (after the loading steps' net.StartGame()) sends CG_ENTERGAME. auto enter = zeroed(); if (read_packet(c, HDR_CG_ENTERGAME, &enter, sizeof(enter), &error) != 1) return Fail(name + ": CG_ENTERGAME: " + error); Event(name + ":entergame"); // CInputLogin::Entergame order: ch->Show inserts the main actor while the client is still in // the loading phase (CHARACTER::EncodeInsertPacket = ADD + ADDITIONAL_INFO, then UpdatePacket), // then SendNPCPosition, PHASE_GAME, SendLandList, TIME, CHANNEL and the greet notice. GCCharacterAdd actor = zeroed(); actor.header = HDR_GC_CHARACTER_ADD; actor.vid = kMainVID; actor.x = kX; actor.y = kY; actor.type = 6; // CHAR_TYPE_PC (common/length.h ECharType) actor.race = 0; // warrior male actor.moving_speed = 100; actor.attack_speed = 100; GCCharAddInfo info = zeroed(); info.header = HDR_GC_CHAR_ADDITIONAL_INFO; info.vid = kMainVID; std::strncpy(info.name, kCharacterName, sizeof(info.name) - 1); info.empire = 1; info.level = 10; GCCharacterUpdate update = zeroed(); update.header = HDR_GC_CHARACTER_UPDATE; update.vid = kMainVID; update.moving_speed = 100; update.attack_speed = 100; if (!c.Send(actor) || !c.Send(info) || !c.Send(update)) return Fail(name + ": send main actor"); // packet_npc_position = DynHead + count, then TNPCPosition entries. NPCPosition34 npc = zeroed(); npc.type = 1; // CHAR_TYPE_NPC std::strncpy(npc.name, "Npc", sizeof(npc.name) - 1); npc.x = kX + 500; npc.y = kY + 500; DynHead npc_head = {HDR_GC_NPC_POSITION, static_cast(sizeof(DynHead) + sizeof(GCNPCPositionHead) + sizeof(npc))}; GCNPCPositionHead npc_count = {1}; if (!c.Send(npc_head) || !c.Send(npc_count) || !c.Send(npc)) return Fail(name + ": send NPC positions"); if (!c.Phase(PHASE_GAME)) return Fail(name + ": send PHASE_GAME"); LandPacketElement24 land = zeroed(); land.id = 1; land.x = kX + 1000; land.y = kY + 1000; land.width = 2000; land.height = 2000; DynHead land_head = {HDR_GC_LAND_LIST, static_cast(sizeof(DynHead) + sizeof(land))}; GCTime game_time = {HDR_GC_TIME, static_cast(now_ms())}; GCChannel channel = {HDR_GC_CHANNEL, 1}; if (!c.Send(land_head) || !c.Send(land) || !c.Send(game_time) || !c.Send(channel)) return Fail(name + ": send land/time/channel"); // SendGreetMessage: a CHAT_TYPE_NOTICE line with no speaker. const char greet[] = "Welcome"; GCChatHead chat = {HDR_GC_CHAT, static_cast(sizeof(GCChatHead) + sizeof(greet)), 2 /* CHAT_TYPE_NOTICE */, 0, 1}; if (!c.Send(chat) || !c.Send(greet, sizeof(greet))) return Fail(name + ": send greet notice"); // CItem::EncodeInsertPacket for an owned item in view: GC_ITEM_GROUND_ADD, then GC_ITEM_OWNERSHIP. GCItemGroundAdd ground = zeroed(); ground.header = HDR_GC_ITEM_GROUND_ADD; ground.x = kGroundItemX; ground.y = kGroundItemY; ground.vid = kGroundItemVID; ground.vnum = kGroundItemVnum; GCItemOwnership ownership = zeroed(); ownership.header = HDR_GC_ITEM_OWNERSHIP; ownership.vid = kGroundItemVID; std::strncpy(ownership.owner, kCharacterName, sizeof(ownership.owner) - 1); if (!c.Send(ground) || !c.Send(ownership)) return Fail(name + ": send ground item"); // A monster in view: CHARACTER::EncodeInsertPacket sends ADDITIONAL_INFO only for PCs and NPCs, so a // CHAR_TYPE_MONSTER is ADD + UPDATE (the client names it from mob_proto). GCCharacterAdd mob = zeroed(); mob.header = HDR_GC_CHARACTER_ADD; mob.vid = kMobVID; mob.x = kMobX; mob.y = kMobY; mob.type = 0; // CHAR_TYPE_MONSTER mob.race = kMobRace; mob.moving_speed = 100; mob.attack_speed = 100; GCCharacterUpdate mob_update = zeroed(); mob_update.header = HDR_GC_CHARACTER_UPDATE; mob_update.vid = kMobVID; mob_update.moving_speed = 100; mob_update.attack_speed = 100; if (!c.Send(mob) || !c.Send(mob_update)) return Fail(name + ": send monster"); // An NPC is ADD + ADDITIONAL_INFO + UPDATE like a PC (the name is the mob_proto locale name). GCCharacterAdd keeper = zeroed(); keeper.header = HDR_GC_CHARACTER_ADD; keeper.vid = kShopVID; keeper.x = kShopX; keeper.y = kShopY; keeper.type = 1; // CHAR_TYPE_NPC keeper.race = kShopRace; keeper.moving_speed = 100; keeper.attack_speed = 100; GCCharAddInfo keeper_info = zeroed(); keeper_info.header = HDR_GC_CHAR_ADDITIONAL_INFO; keeper_info.vid = kShopVID; std::strncpy(keeper_info.name, "General Store", sizeof(keeper_info.name) - 1); GCCharacterUpdate keeper_update = zeroed(); keeper_update.header = HDR_GC_CHARACTER_UPDATE; keeper_update.vid = kShopVID; keeper_update.moving_speed = 100; keeper_update.attack_speed = 100; if (!c.Send(keeper) || !c.Send(keeper_info) || !c.Send(keeper_update)) return Fail(name + ": send shopkeeper"); GCCharacterAdd elder = keeper; elder.vid = kQuestVID; elder.x = kQuestX; elder.y = kQuestY; elder.race = kQuestRace; GCCharAddInfo elder_info = zeroed(); elder_info.header = HDR_GC_CHAR_ADDITIONAL_INFO; elder_info.vid = kQuestVID; std::strncpy(elder_info.name, "Oldster", sizeof(elder_info.name) - 1); GCCharacterUpdate elder_update = keeper_update; elder_update.vid = kQuestVID; if (!c.Send(elder) || !c.Send(elder_info) || !c.Send(elder_update)) return Fail(name + ": send quest NPC"); GCCharacterAdd storekeeper = keeper; storekeeper.vid = kSafeboxVID; storekeeper.x = kSafeboxX; storekeeper.y = kSafeboxY; storekeeper.race = kSafeboxRace; GCCharAddInfo storekeeper_info = zeroed(); storekeeper_info.header = HDR_GC_CHAR_ADDITIONAL_INFO; storekeeper_info.vid = kSafeboxVID; std::strncpy(storekeeper_info.name, "Storekeeper", sizeof(storekeeper_info.name) - 1); GCCharacterUpdate storekeeper_update = keeper_update; storekeeper_update.vid = kSafeboxVID; if (!c.Send(storekeeper) || !c.Send(storekeeper_info) || !c.Send(storekeeper_update)) return Fail(name + ": send storekeeper NPC"); // Messenger (批次 4-f): Send initial friend list struct { GCMessengerHead head; MessengerListEntry entry; char name[10]; } __attribute__((packed)) messenger_pkt = { {HDR_GC_MESSENGER, static_cast(sizeof(messenger_pkt)), MESSENGER_GC_LIST}, {1 /* MESSENGER_CONNECTED_STATE_ONLINE */, static_cast(std::strlen(kFriendName))}, {} }; std::memcpy(messenger_pkt.name, kFriendName, sizeof(messenger_pkt.name)); if (!c.Send(messenger_pkt)) return Fail(name + ": send messenger list"); // Guild (批次 4-f): Send guild name struct { GCMessengerHead head; GuildName16 item; } __attribute__((packed)) guild_name_pkt = { {HDR_GC_GUILD, static_cast(sizeof(guild_name_pkt)), 16 /* GUILD_GC_NAME */}, {kGuildID, {}} }; std::strncpy(guild_name_pkt.item.name, kGuildName, sizeof(guild_name_pkt.item.name)); if (!c.Send(guild_name_pkt)) return Fail(name + ": send guild name"); // Guild (批次 4-f): Send guild info (enables guild for player) struct { GCMessengerHead head; GuildInfo35 info; } __attribute__((packed)) guild_info_pkt = { {HDR_GC_GUILD, static_cast(sizeof(guild_info_pkt)), 8 /* GUILD_GC_INFO */}, zeroed() }; guild_info_pkt.info.guild_id = kGuildID; guild_info_pkt.info.master_pid = 1; guild_info_pkt.info.level = 20; guild_info_pkt.info.exp = 50000; guild_info_pkt.info.member_count = 1; guild_info_pkt.info.max_member_count = 50; guild_info_pkt.info.gold = 123456; guild_info_pkt.info.has_land = 1; std::strncpy(guild_info_pkt.info.name, kGuildName, sizeof(guild_info_pkt.info.name) - 1); if (!c.Send(guild_info_pkt)) return Fail(name + ": send guild info"); Event(name + ":burst_sent"); // A ping after the burst proves that GamePhase consumed every packet above and can reply. Blank ping = {HDR_GC_PING}; Blank pong = {}; if (!c.Send(ping) || read_packet(c, HDR_CG_PONG, &pong, sizeof(pong), &error) != 1) return Fail(name + ": CG_PONG after the entergame burst: " + error); Event(name + ":burst_pong"); // Then hold the connection until the test stops us, reading what the game phase sends the way // CInputProcessor::Process frames it (static size or TDynamicSizePacketHeader, then the sequence // byte). A CG_MOVE (input_main.cpp CInputMain::Move) becomes ":move ". int mob_hp = kMobHits; bool shopping = false; std::int32_t gold = kStartGold; std::uint32_t bag0 = 0; // vnum in inventory cell 0 (the fake character starts with an empty bag) int quest_page = 0; // the quest dialogue's last page sent (1 question, 2 next, 3 done) bool safebox_open = false; std::uint32_t safebox0 = kSafeboxItemVnum; std::int32_t safebox_gold = kSafeboxMoney; // CQuestManager::SendScript: packet_script {header, size, skin, src_size} and the script text. auto send_script = [&](const std::string& script) { struct { DynHead head; std::uint8_t skin; std::uint16_t src_size; } __attribute__((packed)) head = {{HDR_GC_SCRIPT, static_cast(sizeof(head) + script.size())}, 1 /* QUEST_SKIN_NORMAL */, static_cast(script.size())}; std::string bytes(reinterpret_cast(&head), sizeof(head)); bytes += script; return c.Send(bytes.data(), bytes.size()); }; for (;;) { std::uint8_t packet[1024] = {}; if (c.Read(packet, 1) != 1) return true; const std::uint8_t header = packet[0]; size_t size = static_cast(packet_size_cg(header)); if (is_dynamic_cg(header) || header == HDR_CG_SYNC_POSITION) { if (c.Read(packet + 1, sizeof(DynHead) - 1) != 1) return true; DynHead head; std::memcpy(&head, packet, sizeof(head)); size = head.size; if (size < sizeof(DynHead) || size > sizeof(packet) || (size > sizeof(DynHead) && c.Read(packet + sizeof(DynHead), size - sizeof(DynHead)) != 1)) return Fail(name + ": dynamic CG packet " + std::to_string(header) + " in the game phase"); } else if (size == 0 || size > sizeof(packet)) return Fail(name + ": unknown CG header " + std::to_string(header) + " in the game phase"); else if (size > 1 && c.Read(packet + 1, size - 1) != 1) return true; // CInputMain::Shop returns the bytes each subheader carries past TPacketCGShop; the sequence byte follows them. if (header == HDR_CG_SHOP) { const size_t extra = packet[1] == SHOP_CG_BUY || packet[1] == SHOP_CG_SELL2 ? 2 : packet[1] == SHOP_CG_SELL ? 1 : 0; if (extra && c.Read(packet + size, extra) != 1) return true; } if (is_sequence_cg(header)) { std::uint8_t sequence = 0; if (c.Read(&sequence, 1) != 1) return true; if (sequence != SEQUENCE_TABLE[c.sequence]) return Fail(name + ": SEQUENCE mismatch on CG header " + std::to_string(header)); c.sequence = (c.sequence + 1) % SEQUENCE_TABLE_SIZE; } // CInputMain::ItemPickup → CHARACTER::PickupItem → CItem::RemoveFromGround → EncodeRemovePacket. if (header == HDR_CG_ITEM_PICKUP) { CGItemPickup pickup; std::memcpy(&pickup, packet, sizeof(pickup)); Event(name + ":pickup " + std::to_string(pickup.vid)); GCItemGroundDel del = {HDR_GC_ITEM_GROUND_DEL, pickup.vid}; if (pickup.vid == kGroundItemVID && !c.Send(del)) return Fail(name + ": send GC_ITEM_GROUND_DEL"); } // CInputMain::Exchange (input_main.cpp) → CHARACTER::ExchangeStart / CExchange::Cancel: the partner is // played by the server itself — START names the requested vid, then the partner's item (vnum 19, cell 0) // and gold arrive the way CExchange::AddItem/AddGold send them with is_me=false; CANCEL answers END. if (header == HDR_CG_EXCHANGE) { CGExchange ex; std::memcpy(&ex, packet, sizeof(ex)); // arg1 only means something for START (40250 SendExchangeExitPacket leaves it uninitialised). Event(name + ":exchange " + std::to_string(ex.subheader) + (ex.subheader == EXCHANGE_CG_START ? " " + std::to_string(ex.arg1) : std::string())); GCExchange out = zeroed(); out.header = HDR_GC_EXCHANGE; out.arg2 = {0xff, 0xffff}; // NPOS bool ok = true; if (ex.subheader == EXCHANGE_CG_START) { out.subheader = EXCHANGE_GC_START; out.arg1 = ex.arg1; ok = c.Send(out); out.subheader = EXCHANGE_GC_ITEM_ADD; out.arg1 = kExchangeItemVnum; out.arg2 = {1, 0}; // INVENTORY, cell 0 out.arg3 = 1; ok = ok && c.Send(out); out = zeroed(); out.header = HDR_GC_EXCHANGE; out.subheader = EXCHANGE_GC_GOLD_ADD; out.arg2 = {0xff, 0xffff}; out.arg1 = kExchangeGold; ok = ok && c.Send(out); } else if (ex.subheader == EXCHANGE_CG_CANCEL) { out.subheader = EXCHANGE_GC_END; ok = c.Send(out); } if (!ok) return Fail(name + ": send GC_EXCHANGE"); } // CInputMain::Chat (input_main.cpp): the line comes back to the speaker (PacketAround includes them) // as a CHAT_TYPE_TALKING GC_CHAT " : " carrying their vid and empire. if (header == HDR_CG_CHAT) { CGChatHead head; std::memcpy(&head, packet, sizeof(head)); const size_t text_len = size > sizeof(head) ? size - sizeof(head) : 0; const std::string text(reinterpret_cast(packet + sizeof(head)), strnlen(reinterpret_cast(packet + sizeof(head)), text_len)); Event(name + ":chat " + std::to_string(head.type) + " " + text); if (text.rfind("/safebox_password ", 0) == 0) { const std::string pass = text.substr(18); Event(name + ":safebox_password " + pass); safebox_open = true; GCSafeboxSize size_pkt = {HDR_GC_SAFEBOX_SIZE, FakeLoginServer::kSafeboxSize}; GCSafeboxMoneyChange money_pkt = {HDR_GC_SAFEBOX_MONEY_CHANGE, safebox_gold}; if (!c.Send(size_pkt) || !c.Send(money_pkt)) return Fail(name + ": send GC_SAFEBOX_SIZE / MONEY"); if (safebox0 != 0) { GCItemSet set = zeroed(); set.header = HDR_GC_SAFEBOX_SET; set.cell = {3 /* SAFEBOX */, 0}; set.vnum = safebox0; set.count = 1; if (!c.Send(set)) return Fail(name + ": send GC_SAFEBOX_SET"); } } else if (text == "/safebox_close") { Event(name + ":safebox_close"); safebox_open = false; const std::string cmd = "CloseSafebox"; GCChatHead chat = {HDR_GC_CHAT, static_cast(sizeof(GCChatHead) + cmd.size() + 1), 5 /* CHAT_TYPE_COMMAND */, 0, 1}; if (!c.Send(chat) || !c.Send(cmd.c_str(), cmd.size() + 1)) return Fail(name + ": send CloseSafebox"); } else { const std::string line = std::string(kCharacterName) + " : " + text; GCChatHead chat = {HDR_GC_CHAT, static_cast(sizeof(GCChatHead) + line.size() + 1), 0 /* CHAT_TYPE_TALKING */, kMainVID, 1}; if (!c.Send(chat) || !c.Send(line.c_str(), line.size() + 1)) return Fail(name + ": send GC_CHAT"); } } // CInputMain::Target → CHARACTER::SetTarget → BroadcastTargetPacket: GC_TARGET with the monster's HP percent. if (header == HDR_CG_TARGET) { CGTarget target; std::memcpy(&target, packet, sizeof(target)); Event(name + ":target " + std::to_string(target.vid)); GCTarget out = {HDR_GC_TARGET, target.vid, static_cast(target.vid == kMobVID ? mob_hp * 100 / kMobHits : 0)}; if (!c.Send(out)) return Fail(name + ": send GC_TARGET"); } // CInputMain::Attack → battle_hit → CHARACTER::Damage: GC_DAMAGE_INFO to the attacker, the target HP // update, and at 0 HP CHARACTER::Dead's GC_DEAD. if (header == HDR_CG_ATTACK) { CGAttack attack; std::memcpy(&attack, packet, sizeof(attack)); Event(name + ":attack " + std::to_string(attack.type) + " " + std::to_string(attack.victim_vid)); if (attack.victim_vid == kMobVID && mob_hp > 0) { --mob_hp; GCDamageInfo damage = {HDR_GC_DAMAGE_INFO, kMobVID, 1 /* DAMAGE_NORMAL */, 100}; GCTarget target = {HDR_GC_TARGET, kMobVID, static_cast(mob_hp * 100 / kMobHits)}; if (!c.Send(damage) || !c.Send(target)) return Fail(name + ": send GC_DAMAGE_INFO"); GCDead dead = {HDR_GC_DEAD, kMobVID}; if (mob_hp == 0 && !c.Send(dead)) return Fail(name + ": send GC_DEAD"); } } // CInputMain::OnClick → CHARACTER::OnClick → CShopManager::StartShopping → CShop::AddGuest: GC_SHOP START with // the owner's vid and the 40 shelf slots. if (header == HDR_CG_ON_CLICK) { CGOnClick click; std::memcpy(&click, packet, sizeof(click)); Event(name + ":click " + std::to_string(click.vid)); if (click.vid == kSafeboxVID) { const std::string cmd = "ShowMeSafeboxPassword"; GCChatHead chat = {HDR_GC_CHAT, static_cast(sizeof(GCChatHead) + cmd.size() + 1), 5 /* CHAT_TYPE_COMMAND */, 0, 1}; if (!c.Send(chat) || !c.Send(cmd.c_str(), cmd.size() + 1)) return Fail(name + ": send ShowMeSafeboxPassword"); } // CHARACTER::OnClick → CQuestManager::Click: say(...) then select("Yes", "No") → GotoSelectState. if (click.vid == kQuestVID && !send_script("Greetings, young warrior.[ENTER]Shall I tell you a story?[ENTER][QUESTION 1;Yes|2;No]")) return Fail(name + ": send GC_SCRIPT question"); if (click.vid == kQuestVID) quest_page = 1; if (click.vid == kShopVID && !shopping) { shopping = true; struct { DynHead head; std::uint8_t subheader; std::uint32_t owner_vid; ShopItem43 items[SHOP_HOST_ITEM_MAX_NUM]; } __attribute__((packed)) start = zeroed(); static_assert(sizeof(start) == 4 + 4 + 40 * 43); start.head = {HDR_GC_SHOP, static_cast(sizeof(start))}; start.subheader = SHOP_SUB_START; start.owner_vid = kShopVID; start.items[0].vnum = kShopItemVnum; start.items[0].price = kShopItemPrice; start.items[0].count = 1; if (!c.Send(start)) return Fail(name + ": send GC_SHOP START"); } } // CInputMain::ScriptAnswer: an answer above 250 is the [NEXT] button (CQuestManager::Resume), the rest pick // a [QUESTION] choice (CQuestManager::Select, 0-based). if (header == HDR_CG_SCRIPT_ANSWER) { const std::uint8_t answer = packet[1]; Event(name + ":answer " + std::to_string(answer)); if (quest_page == 1 && answer == 0) { quest_page = 2; if (!send_script("Long ago a dragon slept under this village.[ENTER][NEXT]")) return Fail(name + ": send GC_SCRIPT next"); } else if (quest_page == 2 && answer > 250) { quest_page = 3; if (!send_script("It sleeps there still.[ENTER][DONE]")) return Fail(name + ": send GC_SCRIPT done"); } } // CInputMain::Shop: END → CShop::RemoveGuest (GC_SHOP END); BUY → CShop::Buy (PointChange(POINT_GOLD), then // CItem::AddToCharacter's GC_ITEM_SET into the first empty cell); SELL2 → CShopManager::Sell (the cell empties // with the 42-byte header-20 packet, then PointChange(POINT_GOLD)). if (header == HDR_CG_SHOP) { const std::uint8_t sub = packet[1]; Event(name + ":shop " + std::to_string(sub) + (sub == SHOP_CG_BUY || sub == SHOP_CG_SELL2 ? " " + std::to_string(packet[2]) + " " + std::to_string(packet[3]) : std::string())); auto gold_change = [&](std::int32_t amount) { gold += amount; GCPointChange change = zeroed(); change.header = HDR_GC_CHARACTER_POINT_CHANGE; change.vid = kMainVID; change.type = 11; // POINT_GOLD change.amount = amount; change.value = gold; return c.Send(change); }; bool ok = true; if (sub == SHOP_CG_END && shopping) { shopping = false; struct { DynHead head; std::uint8_t subheader; } __attribute__((packed)) end = {{HDR_GC_SHOP, 4}, SHOP_SUB_END}; ok = c.Send(end); } else if (sub == SHOP_CG_BUY && shopping && packet[3] == 0 && bag0 == 0) { bag0 = kShopItemVnum; GCItemSet set = zeroed(); set.header = HDR_GC_ITEM_SET; set.cell = {1, 0}; // INVENTORY, cell 0 set.vnum = kShopItemVnum; set.count = 1; ok = gold_change(-static_cast(kShopItemPrice)) && c.Send(set); } else if (sub == SHOP_CG_SELL2 && shopping && packet[2] == 0 && bag0 != 0) { bag0 = 0; GCItemDelDeprecated del = zeroed(); del.header = HDR_GC_ITEM_DEL; del.cell = {1, 0}; ok = c.Send(del) && gold_change(kShopSellPrice); } if (!ok) return Fail(name + ": send shop reply"); } if (header == HDR_CG_SAFEBOX_CHECKIN) { CGSafeboxCheckin in; std::memcpy(&in, packet, sizeof(in)); Event(name + ":safebox_checkin " + std::to_string(in.item_pos.cell) + " -> " + std::to_string(in.safe_pos)); if (in.item_pos.cell == 0 && in.safe_pos == 0 && bag0 != 0) { safebox0 = bag0; bag0 = 0; GCItemDelDeprecated del = zeroed(); del.header = HDR_GC_ITEM_DEL; del.cell = {1, 0}; GCItemSet set = zeroed(); set.header = HDR_GC_SAFEBOX_SET; set.cell = {3 /* SAFEBOX */, 0}; set.vnum = safebox0; set.count = 1; if (!c.Send(del) || !c.Send(set)) return Fail(name + ": send safebox checkin replies"); } } if (header == HDR_CG_SAFEBOX_CHECKOUT) { CGSafeboxCheckout out; std::memcpy(&out, packet, sizeof(out)); Event(name + ":safebox_checkout " + std::to_string(out.safe_pos) + " -> " + std::to_string(out.item_pos.cell)); if (out.safe_pos == 0 && out.item_pos.cell == 0 && safebox0 != 0) { bag0 = safebox0; safebox0 = 0; GCItemDel del = {HDR_GC_SAFEBOX_DEL, 0}; GCItemSet set = zeroed(); set.header = HDR_GC_ITEM_SET; set.cell = {1, 0}; set.vnum = bag0; set.count = 1; if (!c.Send(del) || !c.Send(set)) return Fail(name + ": send safebox checkout replies"); } } if (header == HDR_CG_MOVE) { CGMove move; std::memcpy(&move, packet, sizeof(move)); Event(name + ":move " + std::to_string(move.func) + " " + std::to_string(move.arg) + " " + std::to_string(move.rot) + " " + std::to_string(move.x) + " " + std::to_string(move.y)); } } }