Files
mtgodot-poc/extension/src/net/classic/classic_session.cpp
T

948 lines
29 KiB
C++

#include "classic_session.h"
#include <algorithm>
#include <cstring>
#include <vector>
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<uint8_t>(slot);
std::memcpy(p.name, name.data(), name.size());
p.job = static_cast<uint16_t>(job);
p.shape = static_cast<uint8_t>(shape);
p.con = static_cast<uint8_t>(con);
p.intel = static_cast<uint8_t>(intel);
p.str = static_cast<uint8_t>(str);
p.dex = static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint16_t>(sizeof(CGChatHead) + text.size() + 1);
std::vector<uint8_t> 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<SyncPosElement> &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<uint8_t> buf(total, 0);
DynHead head{HDR_CG_SYNC_POSITION, static_cast<uint16_t>(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<uint8_t> buf(total, 0);
CGWhisper p{};
p.header = HDR_CG_WHISPER;
p.size = static_cast<uint16_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<CGMyShopItem> &items) {
if (m_stage != Stage::InGame || sign.size() > SHOP_TAB_NAME_MAX) return false;
const size_t n = std::min(items.size(), static_cast<size_t>(39));
const size_t total = sizeof(CGMyShopHead) + n * sizeof(CGMyShopItem);
std::vector<uint8_t> buf(total, 0);
CGMyShopHead head{};
head.header = HDR_CG_MYSHOP;
std::memcpy(head.sign, sign.data(), sign.size());
head.count = static_cast<uint8_t>(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<uint8_t> 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<uint8_t>(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<uint8_t> 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<uint8_t> 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