#include "classic_session.h" #include #include #include namespace mtnet::classic { ClassicSession::ClassicSession() : m_parser(m_world) { m_stream.on_phase = [this](uint8_t p) { on_phase(p); }; m_stream.on_packet = [this](uint8_t h, const uint8_t *b, uint32_t l) { return on_packet(h, b, l); }; m_stream.on_error = [this](const std::string &e) { m_last_error = e; set_stage(Stage::Failed); if (on_error) { on_error(e); } }; m_stream.on_state_change = [this](ClassicStream::State s) { if (s == ClassicStream::State::Offline && m_stage != Stage::Failed && m_stage != Stage::Offline) { m_last_error = m_stream.last_error().empty() ? "disconnected" : m_stream.last_error(); set_stage(Stage::Failed); } else if (s == ClassicStream::State::Online && m_stage == Stage::Connecting) { // handshake packets flow next; stay in Connecting until GC_PHASE(LOGIN) } }; } void ClassicSession::set_stage(Stage s) { if (m_stage == s) { return; } m_stage = s; if (on_stage_change) { on_stage_change(s); } } void ClassicSession::start_offline(const std::string &id, const std::string &pw) { m_id = id; m_pw = pw; m_login_sent = false; m_entergame_sent = false; m_version_sent = false; m_last_error.clear(); // During PHASE_HANDSHAKE even retry handshakes must be answered with // 0xFF. The normal game client switches to CG_TIME_SYNC only after the // server has advanced into the login/select side of the connection. m_stream.set_time_sync_mode(false); set_stage(Stage::Connecting); } bool ClassicSession::connect(const std::string &game_host, uint16_t game_port, const std::string &id, const std::string &pw) { start_offline(id, pw); if (!m_stream.connect(game_host, game_port)) { m_last_error = m_stream.last_error(); set_stage(Stage::Failed); return false; } return true; } void ClassicSession::disconnect() { m_stream.disconnect(); set_stage(Stage::Offline); } void ClassicSession::pump() { m_stream.process(); // auto CG_ENTERGAME a beat into PHASE_LOADING (the m2dev client waits ~1.5s; // sending it too early makes the server drop us mid spawn-burst). if (m_stage == Stage::Loading && !m_entergame_sent && m_entergame_delay > 0 && m_now - m_loading_since >= m_entergame_delay) { enter_game(); } m_world.tick(); } void ClassicSession::send_login() { if (m_login_sent) { return; } // docs ยง2.2: the client turns sequence mode on around here; CG_LOGIN is a // bSeq packet and the server expects the trailing byte from packet #1. m_stream.set_sequence_mode(true); CGLogin p{}; p.header = HDR_CG_LOGIN; std::strncpy(p.login, m_id.c_str(), sizeof(p.login) - 1); std::strncpy(p.passwd, m_pw.c_str(), sizeof(p.passwd) - 1); m_stream.send_fixed(&p, sizeof(p)); m_login_sent = true; } bool ClassicSession::create_character(int slot, const std::string &name, int job, int shape, int con, int intel, int str, int dex) { if (m_stage != Stage::CharSelect || slot < 0 || slot >= m_parser.slot_count() || name.empty() || name.size() > CHARACTER_NAME_MAX_LEN) { return false; } CGPlayerCreate p{}; p.header = HDR_CG_CHARACTER_CREATE; p.index = static_cast(slot); std::memcpy(p.name, name.data(), name.size()); p.job = static_cast(job); p.shape = static_cast(shape); p.con = static_cast(con); p.intel = static_cast(intel); p.str = static_cast(str); p.dex = static_cast(dex); return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::delete_character(int slot, const std::string &private_code) { if (m_stage != Stage::CharSelect || slot < 0 || slot >= m_parser.slot_count() || private_code.size() > PRIVATE_CODE_LENGTH) { return false; } CGPlayerDelete p{}; p.header = HDR_CG_CHARACTER_DELETE; p.index = static_cast(slot); std::memcpy(p.private_code, private_code.data(), private_code.size()); return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::change_name(int slot, const std::string &name) { if (m_stage != Stage::CharSelect || slot < 0 || slot >= m_parser.slot_count() || name.empty() || name.size() > CHARACTER_NAME_MAX_LEN) { return false; } CGChangeName p{}; p.header = HDR_CG_CHANGE_NAME; p.index = static_cast(slot); std::memcpy(p.name, name.data(), name.size()); return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_empire(uint8_t empire) { if (m_stage != Stage::CharSelect) { return false; } CGEmpire p{HDR_CG_EMPIRE, empire}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_client_version() { if (m_version_sent) { return true; } CGClientVersion p{}; // The stock 40250 game client sends the version report immediately after // receiving the main-character packet and advances the normal CG sequence. p.header = HDR_CG_CLIENT_VERSION; std::strncpy(p.filename, "metin2.bin", sizeof(p.filename) - 1); std::strncpy(p.timestamp, "1215955205", sizeof(p.timestamp) - 1); if (!m_stream.send_fixed(&p, sizeof(p))) { return false; } m_version_sent = true; return true; } void ClassicSession::on_phase(uint8_t phase) { if (phase == PHASE_LOGIN || phase == PHASE_SELECT || phase == PHASE_LOADING || phase == PHASE_GAME || phase == PHASE_DEAD) { m_stream.set_time_sync_mode(true); } switch (phase) { case PHASE_LOGIN: set_stage(Stage::LoggingIn); send_login(); break; case PHASE_SELECT: set_stage(Stage::CharSelect); if (m_parser.char_list_ready() && on_char_list) { on_char_list(m_parser.char_slots()); } break; case PHASE_LOADING: set_stage(Stage::Loading); m_loading_since = m_now; m_entergame_sent = false; break; case PHASE_GAME: set_stage(Stage::InGame); if (on_entered_game) { on_entered_game(); } break; case PHASE_CLOSE: m_last_error = m_last_error.empty() ? "server closed phase" : m_last_error; set_stage(Stage::Failed); break; default: break; } } bool ClassicSession::on_packet(uint8_t header, const uint8_t *body, uint32_t len) { if (!m_parser.on_gc(header, body, len)) { m_last_error = "parse error on GC header " + std::to_string(header); return false; } if ((header == HDR_GC_MAIN_CHARACTER || header == HDR_GC_MAIN_CHARACTER3_BGM || header == HDR_GC_MAIN_CHARACTER4_BGM_VOL) && !send_client_version()) { m_last_error = "failed to send client version"; return false; } if (!m_parser.login_failure().empty() && m_stage == Stage::LoggingIn) { m_last_error = "login failed: " + m_parser.login_failure(); set_stage(Stage::Failed); return false; } return true; } bool ClassicSession::select_char(int slot) { // Caller-side misuse: return false but don't poison m_last_error, which is // reserved for transport/protocol failures ("is the connection healthy"). if (m_stage != Stage::CharSelect) { return false; } if (slot < 0 || slot >= m_parser.slot_count()) { return false; } CGPlayerSelect p{}; p.header = HDR_CG_CHARACTER_SELECT; p.player_index = static_cast(slot); m_stream.send_fixed(&p, sizeof(p)); return true; } bool ClassicSession::enter_game() { if (m_entergame_sent) { return true; } CGEnterGame p{}; p.header = HDR_CG_ENTERGAME; m_stream.send_fixed(&p, sizeof(p)); m_entergame_sent = true; return true; } // --- in-game intents --------------------------------------------------------- bool ClassicSession::send_move(uint8_t func, uint8_t arg, float rot_deg, int32_t x, int32_t y, uint32_t time_ms) { if (m_stage != Stage::InGame) { return false; } CGMove p{}; p.header = HDR_CG_MOVE; p.func = func; p.arg = arg; p.rot = static_cast(rot_deg / 5.0f); // classic wire: bRot = deg/5 p.x = x; p.y = y; p.time = time_ms ? time_ms : m_now; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_attack(uint8_t type, uint32_t victim_vid) { if (m_stage != Stage::InGame) { return false; } CGAttack p{}; p.header = HDR_CG_ATTACK; p.type = type; p.victim_vid = victim_vid; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_chat(uint8_t type, const std::string &text) { if (m_stage != Stage::InGame || text.empty() || text.size() >= 512) { return false; } // wire: [header][WORD length][BYTE type][text\0]; length = 4 + strlen + 1 const uint16_t len = static_cast(sizeof(CGChatHead) + text.size() + 1); std::vector buf(len, 0); CGChatHead h{}; h.header = HDR_CG_CHAT; h.length = len; h.type = type; std::memcpy(buf.data(), &h, sizeof(h)); std::memcpy(buf.data() + sizeof(h), text.data(), text.size()); // buf[len-1] stays 0 (the trailing NUL) return m_stream.send_dynamic(buf.data(), buf.size()); } bool ClassicSession::send_target(uint32_t vid) { if (m_stage != Stage::InGame) { return false; } CGTarget p{HDR_CG_TARGET, vid}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_character_position(uint8_t position) { if (m_stage != Stage::InGame) { return false; } CGPosition p{HDR_CG_CHARACTER_POSITION, position}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_click_npc(uint32_t vid) { if (m_stage != Stage::InGame) { return false; } CGOnClick p{HDR_CG_ON_CLICK, vid}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_script_answer(uint8_t answer) { if (m_stage != Stage::InGame) { return false; } CGScriptAnswer p{HDR_CG_SCRIPT_ANSWER, answer}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_item_use(uint8_t window, uint16_t cell) { if (m_stage != Stage::InGame) { return false; } CGItemUse p{}; p.header = HDR_CG_ITEM_USE; p.pos = {window, cell}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_item_move(uint8_t window, uint16_t cell, uint8_t to_window, uint16_t to_cell, uint8_t count) { if (m_stage != Stage::InGame) { return false; } CGItemMove p{}; p.header = HDR_CG_ITEM_MOVE; p.pos = {window, cell}; p.change_pos = {to_window, to_cell}; p.num = count; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_item_drop(uint8_t window, uint16_t cell, uint32_t gold, uint8_t count) { if (m_stage != Stage::InGame) { return false; } if (count > 0) { CGItemDrop2 p{}; p.header = HDR_CG_ITEM_DROP2; p.pos = {window, cell}; p.gold = gold; p.count = count; return m_stream.send_fixed(&p, sizeof(p)); } CGItemDrop p{}; p.header = HDR_CG_ITEM_DROP; p.pos = {window, cell}; p.gold = gold; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_item_pickup(uint32_t ground_vid) { if (m_stage != Stage::InGame) { return false; } CGItemPickup p{HDR_CG_ITEM_PICKUP, ground_vid}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_use_skill(uint32_t skill_vnum, uint32_t target_vid) { if (m_stage != Stage::InGame) { return false; } if (target_vid) { CGFlyTargeting ft{}; ft.header = HDR_CG_FLY_TARGETING; ft.target_vid = target_vid; m_stream.send_fixed(&ft, sizeof(ft)); } CGUseSkill p{}; p.header = HDR_CG_USE_SKILL; p.vnum = skill_vnum; p.target_vid = target_vid; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_item_use_to_item(uint8_t window, uint16_t cell, uint8_t target_window, uint16_t target_cell) { if (m_stage != Stage::InGame) { return false; } CGItemUseToItem p{}; p.header = HDR_CG_ITEM_USE_TO_ITEM; p.pos = {window, cell}; p.target_pos = {target_window, target_cell}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_item_give(uint32_t target_vid, uint8_t window, uint16_t cell, uint8_t count) { if (m_stage != Stage::InGame || target_vid == 0 || count == 0) { return false; } CGGiveItem p{}; p.header = HDR_CG_ITEM_GIVE; p.target_vid = target_vid; p.pos = {window, cell}; p.count = count; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_request_warp() { if (m_stage != Stage::InGame) { return false; } CGWarp p{HDR_CG_WARP}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_fishing(uint8_t rot) { if (m_stage != Stage::InGame) { return false; } CGFishing p{HDR_CG_FISHING, rot}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_dungeon() { if (m_stage != Stage::InGame) { return false; } CGWarp p{HDR_CG_DUNGEON}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_sync_positions(const std::vector &positions) { if (m_stage != Stage::InGame || positions.empty() || positions.size() > 16) { return false; } const size_t total = sizeof(DynHead) + positions.size() * sizeof(SyncPosElement); if (total > 0xffffu) { return false; } std::vector buf(total, 0); DynHead head{HDR_CG_SYNC_POSITION, static_cast(total)}; std::memcpy(buf.data(), &head, sizeof(head)); std::memcpy(buf.data() + sizeof(head), positions.data(), positions.size() * sizeof(SyncPosElement)); return m_stream.send_fixed(buf.data(), buf.size()); } bool ClassicSession::send_whisper(const std::string &to, const std::string &text) { if (m_stage != Stage::InGame || to.empty() || to.size() > CHARACTER_NAME_MAX_LEN || text.empty() || text.size() >= 512) { return false; } const size_t total = sizeof(CGWhisper) + text.size() + 1; if (total > 0xffffu) { return false; } std::vector buf(total, 0); CGWhisper p{}; p.header = HDR_CG_WHISPER; p.size = static_cast(total); std::memcpy(p.name_to, to.data(), to.size()); std::memcpy(buf.data(), &p, sizeof(p)); std::memcpy(buf.data() + sizeof(p), text.data(), text.size()); return m_stream.send_dynamic(buf.data(), buf.size()); } bool ClassicSession::send_shoot(uint8_t type) { if (m_stage != Stage::InGame) { return false; } CGShoot p{HDR_CG_SHOOT, type}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_add_fly_targeting(uint32_t target_vid, int32_t x, int32_t y) { if (m_stage != Stage::InGame) { return false; } CGFlyTargeting p{HDR_CG_ADD_FLY_TARGETING, target_vid, x, y}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_quickslot_add(uint8_t pos, uint8_t type, uint8_t ref) { if (m_stage != Stage::InGame || pos >= QUICKSLOT_MAX_NUM) { return false; } CGQuickSlotAdd p{HDR_CG_QUICKSLOT_ADD, pos, type, ref}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_quickslot_del(uint8_t pos) { if (m_stage != Stage::InGame || pos >= QUICKSLOT_MAX_NUM) { return false; } CGQuickSlotDel p{HDR_CG_QUICKSLOT_DEL, pos}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_quickslot_swap(uint8_t pos, uint8_t pos_to) { if (m_stage != Stage::InGame || pos >= QUICKSLOT_MAX_NUM || pos_to >= QUICKSLOT_MAX_NUM) { return false; } CGQuickSlotSwap p{HDR_CG_QUICKSLOT_SWAP, pos, pos_to}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_script_button(uint32_t index) { if (m_stage != Stage::InGame) { return false; } CGScriptButton p{HDR_CG_SCRIPT_BUTTON, index}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_script_select_item(uint32_t selection) { if (m_stage != Stage::InGame) { return false; } CGScriptSelectItem p{HDR_CG_SCRIPT_SELECT_ITEM, selection}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_quest_input(const std::string &text) { if (m_stage != Stage::InGame || text.size() > 64) { return false; } CGQuestInput p{}; p.header = HDR_CG_QUEST_INPUT_STRING; std::memcpy(p.msg, text.data(), text.size()); return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_quest_confirm(bool yes, uint32_t request_pid) { if (m_stage != Stage::InGame) { return false; } CGQuestConfirm p{HDR_CG_QUEST_CONFIRM, static_cast(yes ? 1 : 0), request_pid}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_quest_cancel() { // The classic protocol has no separate CG_QUEST_CANCEL header. The stock // client closes a script by answering 255 (the same sentinel exposed by // script_answer()) and the server routes it through ScriptAnswer. return send_script_answer(255); } bool ClassicSession::send_party_invite(uint32_t vid) { if (m_stage != Stage::InGame || vid == 0) return false; CGPartyInvite p{HDR_CG_PARTY_INVITE, vid}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_party_answer(uint32_t leader_vid, bool accept) { if (m_stage != Stage::InGame || leader_vid == 0) return false; CGPartyInviteAnswer p{HDR_CG_PARTY_INVITE_ANSWER, leader_vid, static_cast(accept ? 1 : 0)}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_party_remove(uint32_t pid) { if (m_stage != Stage::InGame || pid == 0) return false; CGPartyRemove p{HDR_CG_PARTY_REMOVE, pid}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_party_set_state(uint32_t pid, uint8_t role, bool on) { if (m_stage != Stage::InGame || pid == 0) return false; CGPartySetState p{HDR_CG_PARTY_SET_STATE, pid, role, static_cast(on ? 1 : 0)}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_party_use_skill(uint8_t skill_index, uint32_t vid) { if (m_stage != Stage::InGame) return false; CGPartyUseSkill p{HDR_CG_PARTY_USE_SKILL, skill_index, vid}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_party_parameter(uint8_t mode) { if (m_stage != Stage::InGame) return false; CGPartyParameter p{HDR_CG_PARTY_PARAMETER, mode}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_shop_buy(uint8_t pos, uint8_t count) { if (m_stage != Stage::InGame) return false; uint8_t buf[sizeof(CGShop) + sizeof(CGShopBuy)] = {}; CGShop head{HDR_CG_SHOP, SHOP_CG_BUY}; CGShopBuy body{count, pos}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &body, sizeof(body)); return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_shop_sell(uint8_t pos, uint8_t count) { if (m_stage != Stage::InGame) return false; if (count == 0) { uint8_t buf[sizeof(CGShop) + sizeof(CGShopSell)] = {}; CGShop head{HDR_CG_SHOP, SHOP_CG_SELL}; CGShopSell body{pos}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &body, sizeof(body)); return m_stream.send_fixed(buf, sizeof(buf)); } uint8_t buf[sizeof(CGShop) + sizeof(CGShopSell2)] = {}; CGShop head{HDR_CG_SHOP, SHOP_CG_SELL2}; CGShopSell2 body{pos, count}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &body, sizeof(body)); return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_shop_close() { if (m_stage != Stage::InGame) return false; CGShop p{HDR_CG_SHOP, SHOP_CG_END}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_private_shop(const std::string &sign, const std::vector &items) { if (m_stage != Stage::InGame || sign.size() > SHOP_TAB_NAME_MAX) return false; const size_t n = std::min(items.size(), static_cast(39)); const size_t total = sizeof(CGMyShopHead) + n * sizeof(CGMyShopItem); std::vector buf(total, 0); CGMyShopHead head{}; head.header = HDR_CG_MYSHOP; std::memcpy(head.sign, sign.data(), sign.size()); head.count = static_cast(n); std::memcpy(buf.data(), &head, sizeof(head)); if (n > 0) { std::memcpy(buf.data() + sizeof(head), items.data(), n * sizeof(CGMyShopItem)); } return m_stream.send_fixed(buf.data(), buf.size()); } bool ClassicSession::send_cube_make(int result_index) { // 40250's command handler ignores a recipe index: an argument means // "make all" while no argument makes one recipe from the currently loaded // cube slots. Never send the index through as it would unexpectedly craft // repeatedly and consume every matching material. (void)result_index; return send_chat(0, "/cube make"); } bool ClassicSession::send_cube_result_list(int npc_vnum) { (void)npc_vnum; // 40250 derives the active NPC from the selected cube context. return send_chat(0, "/cube r_info"); } bool ClassicSession::send_cube_materials(int start_index, int count) { if (start_index < 0 || count < 1 || start_index > 255 || count > 255) return false; return send_chat(0, "/cube r_info " + std::to_string(start_index) + " " + std::to_string(count)); } bool ClassicSession::send_cube_open() { return send_chat(0, "/cube open"); } bool ClassicSession::send_cube_close() { return send_chat(0, "/cube close"); } bool ClassicSession::send_cube_list() { return send_chat(0, "/cube list"); } bool ClassicSession::send_cube_add_item(int cube_index, int inventory_index) { if (cube_index < 0 || cube_index >= 24 || inventory_index < 0 || inventory_index > 255) { return false; } return send_chat(0, "/cube add " + std::to_string(cube_index) + " " + std::to_string(inventory_index)); } bool ClassicSession::send_cube_delete_item(int cube_index) { if (cube_index < 0 || cube_index >= 24) return false; return send_chat(0, "/cube delete " + std::to_string(cube_index)); } bool ClassicSession::send_guild_sub(uint8_t subheader) { if (m_stage != Stage::InGame) return false; CGGuild p{HDR_CG_GUILD, subheader}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_guild_u32(uint8_t subheader, uint32_t value) { if (m_stage != Stage::InGame) return false; uint8_t buf[sizeof(CGGuild) + sizeof(value)] = {}; CGGuild head{HDR_CG_GUILD, subheader}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &value, sizeof(value)); return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_guild_i32(uint8_t subheader, int32_t value) { if (m_stage != Stage::InGame) return false; uint8_t buf[sizeof(CGGuild) + sizeof(value)] = {}; CGGuild head{HDR_CG_GUILD, subheader}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &value, sizeof(value)); return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_guild_grade_name(uint8_t grade, const std::string &name) { if (m_stage != Stage::InGame || grade == 0 || grade >= 16 || name.empty() || name.size() > GUILD_GRADE_NAME_MAX_LEN) return false; uint8_t buf[sizeof(CGGuild) + 1 + GUILD_GRADE_NAME_MAX_LEN + 1] = {}; CGGuild head{HDR_CG_GUILD, GUILD_CG_CHANGE_GRADE_NAME}; std::memcpy(buf, &head, sizeof(head)); buf[sizeof(head)] = grade; std::memcpy(buf + sizeof(head) + 1, name.data(), name.size()); return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_guild_grade_authority(uint8_t grade, uint8_t authority) { if (m_stage != Stage::InGame || grade == 0 || grade >= 16) return false; uint8_t buf[sizeof(CGGuild) + 2] = {}; CGGuild head{HDR_CG_GUILD, GUILD_CG_CHANGE_GRADE_AUTHORITY}; std::memcpy(buf, &head, sizeof(head)); buf[sizeof(head)] = grade; buf[sizeof(head) + 1] = authority; return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_guild_member_grade(uint32_t pid, uint8_t grade) { if (m_stage != Stage::InGame || pid == 0 || grade == 0 || grade >= 16) return false; uint8_t buf[sizeof(CGGuild) + sizeof(pid) + 1] = {}; CGGuild head{HDR_CG_GUILD, GUILD_CG_CHANGE_MEMBER_GRADE}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &pid, sizeof(pid)); buf[sizeof(head) + sizeof(pid)] = grade; return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_guild_member_general(uint32_t pid, bool enabled) { if (m_stage != Stage::InGame || pid == 0) return false; uint8_t buf[sizeof(CGGuild) + sizeof(pid) + 1] = {}; CGGuild head{HDR_CG_GUILD, GUILD_CG_CHANGE_MEMBER_GENERAL}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &pid, sizeof(pid)); buf[sizeof(head) + sizeof(pid)] = enabled ? 1 : 0; return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_guild_comment(const std::string &text) { if (m_stage != Stage::InGame || text.empty() || text.size() + 1 > GUILD_COMMENT_MAX_LEN) return false; std::vector buf(sizeof(CGGuild) + 1 + text.size() + 1, 0); CGGuild head{HDR_CG_GUILD, GUILD_CG_POST_COMMENT}; std::memcpy(buf.data(), &head, sizeof(head)); buf[sizeof(head)] = static_cast(text.size() + 1); if (!text.empty()) { std::memcpy(buf.data() + sizeof(head) + 1, text.data(), text.size()); } return m_stream.send_fixed(buf.data(), buf.size()); } bool ClassicSession::send_guild_invite_answer(uint32_t guild_id, bool accept) { if (m_stage != Stage::InGame) return false; uint8_t buf[sizeof(CGGuild) + sizeof(guild_id) + 1] = {}; CGGuild head{HDR_CG_GUILD, GUILD_CG_GUILD_INVITE_ANSWER}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &guild_id, sizeof(guild_id)); buf[sizeof(head) + sizeof(guild_id)] = accept ? 1 : 0; return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_guild_answer_make(const std::string &name) { if (m_stage != Stage::InGame || name.empty() || name.size() > GUILD_NAME_MAX_LEN) return false; CGAnswerMakeGuild p{}; p.header = HDR_CG_ANSWER_MAKE_GUILD; std::memcpy(p.guild_name, name.data(), name.size()); return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_guild_skill(uint32_t skill_vnum, uint32_t target_pid) { if (m_stage != Stage::InGame) return false; uint8_t buf[sizeof(CGGuild) + sizeof(CGGuildUseSkill)] = {}; CGGuild head{HDR_CG_GUILD, GUILD_CG_USE_SKILL}; CGGuildUseSkill body{skill_vnum, target_pid}; std::memcpy(buf, &head, sizeof(head)); std::memcpy(buf + sizeof(head), &body, sizeof(body)); return m_stream.send_fixed(buf, sizeof(buf)); } bool ClassicSession::send_refine(uint8_t pos, uint8_t type) { if (m_stage != Stage::InGame) return false; CGRefine p{HDR_CG_REFINE, pos, type}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_dragon_soul_refine(uint8_t subheader, const ItemPos *grid, size_t count) { if (m_stage != Stage::InGame || !grid || count > DS_REFINE_GRID_MAX_NUM) return false; CGDragonSoulRefine p{}; p.header = HDR_CG_DRAGON_SOUL_REFINE; p.subheader = subheader; std::memcpy(p.grid, grid, count * sizeof(ItemPos)); return m_stream.send_fixed(&p, sizeof(p)); } namespace { bool valid_character_name(const std::string &name) { return !name.empty() && name.size() < CHARACTER_NAME_MAX_LEN; } } bool ClassicSession::send_friend_add(const std::string &name) { if (m_stage != Stage::InGame || !valid_character_name(name)) { return false; } // Messenger's extra payload is not length-prefixed: the server consumes a // fixed CHARACTER_NAME_MAX_LEN byte buffer after the two-byte base header. std::vector buf(sizeof(CGMessenger) + CHARACTER_NAME_MAX_LEN, 0); CGMessenger p{HDR_CG_MESSENGER, MESSENGER_CG_ADD_BY_NAME}; std::memcpy(buf.data(), &p, sizeof(p)); std::memcpy(buf.data() + sizeof(p), name.data(), name.size()); return m_stream.send_fixed(buf.data(), buf.size()); } bool ClassicSession::send_friend_remove(const std::string &name) { if (m_stage != Stage::InGame || !valid_character_name(name)) { return false; } std::vector buf(sizeof(CGMessenger) + CHARACTER_NAME_MAX_LEN, 0); CGMessenger p{HDR_CG_MESSENGER, MESSENGER_CG_REMOVE}; std::memcpy(buf.data(), &p, sizeof(p)); std::memcpy(buf.data() + sizeof(p), name.data(), name.size()); return m_stream.send_fixed(buf.data(), buf.size()); } bool ClassicSession::send_exchange_start(uint32_t partner_vid) { if (m_stage != Stage::InGame) { return false; } CGExchange p{}; p.header = HDR_CG_EXCHANGE; p.subheader = EXCHANGE_CG_START; p.arg1 = partner_vid; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_exchange_item_add(uint8_t inv_window, uint16_t inv_cell, uint8_t display_pos) { if (m_stage != Stage::InGame) { return false; } CGExchange p{}; p.header = HDR_CG_EXCHANGE; p.subheader = EXCHANGE_CG_ITEM_ADD; p.arg2 = display_pos; p.pos = {inv_window, inv_cell}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_exchange_gold(uint32_t gold) { if (m_stage != Stage::InGame) { return false; } CGExchange p{}; p.header = HDR_CG_EXCHANGE; p.subheader = EXCHANGE_CG_GOLD_ADD; p.arg1 = gold; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_exchange_accept() { if (m_stage != Stage::InGame) { return false; } CGExchange p{}; p.header = HDR_CG_EXCHANGE; p.subheader = EXCHANGE_CG_ACCEPT; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_exchange_cancel() { if (m_stage != Stage::InGame) { return false; } CGExchange p{}; p.header = HDR_CG_EXCHANGE; p.subheader = EXCHANGE_CG_CANCEL; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_safebox_checkin(uint8_t safe_pos, uint8_t inv_window, uint16_t inv_cell) { if (m_stage != Stage::InGame) { return false; } CGSafeboxCheckin p{}; p.header = HDR_CG_SAFEBOX_CHECKIN; p.safe_pos = safe_pos; p.item_pos = {inv_window, inv_cell}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_safebox_checkout(uint8_t safe_pos, uint8_t inv_window, uint16_t inv_cell) { if (m_stage != Stage::InGame) { return false; } CGSafeboxCheckout p{}; p.header = HDR_CG_SAFEBOX_CHECKOUT; p.safe_pos = safe_pos; p.item_pos = {inv_window, inv_cell}; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_safebox_move(uint16_t from_cell, uint16_t to_cell, uint8_t count) { if (m_stage != Stage::InGame) { return false; } CGItemMove p{}; p.header = HDR_CG_SAFEBOX_ITEM_MOVE; // The stock 40250 client sends inventory-window positions; the active // safebox context tells the server these positions refer to the safebox. p.pos = {WINDOW_INVENTORY, from_cell}; p.change_pos = {WINDOW_INVENTORY, to_cell}; p.num = count; return m_stream.send_fixed(&p, sizeof(p)); } bool ClassicSession::send_mall_checkout(uint8_t mall_pos, uint8_t inv_window, uint16_t inv_cell) { if (m_stage != Stage::InGame) { return false; } CGMallCheckout p{}; p.header = HDR_CG_MALL_CHECKOUT; p.mall_pos = mall_pos; p.item_pos = {inv_window, inv_cell}; return m_stream.send_fixed(&p, sizeof(p)); } } // namespace mtnet::classic