#include "classic_parser.h" #include #include namespace mtnet::classic { void ClassicParser::parse_login_success(const SimplePlayer *players, const uint32_t *guild_id, const char (*guild_name)[GUILD_NAME_MAX_LEN + 1], int n, uint32_t handle, uint32_t rkey) { m_slots.clear(); m_slot_count = n; for (int i = 0; i < n; ++i) { const SimplePlayer &sp = players[i]; CharSlot s; s.id = sp.id; s.name.assign(sp.name, strnlen(sp.name, sizeof(sp.name))); s.job = sp.job; s.level = sp.level; s.st = sp.st; s.ht = sp.ht; s.dx = sp.dx; s.iq = sp.iq; s.play_minutes = sp.play_minutes; s.x = sp.x; s.y = sp.y; s.main_part = sp.main_part; s.hair_part = sp.hair_part; s.skill_group = sp.skill_group; s.change_name = sp.change_name != 0; s.guild_id = guild_id[i]; s.guild_name.assign(guild_name[i], strnlen(guild_name[i], GUILD_NAME_MAX_LEN + 1)); m_slots.push_back(std::move(s)); } m_handle = handle; m_random_key = rkey; m_char_list_ready = true; } bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) { switch (header) { case HDR_GC_LOGIN_SUCCESS_NEWSLOT: { // 32 — what 40250 actually sends GCLoginSuccess p; if (!fill(p, header, body, len)) { return false; } parse_login_success(p.players, p.guild_id, p.guild_name, PLAYER_PER_ACCOUNT, p.handle, p.random_key); return true; } case HDR_GC_LOGIN_SUCCESS: { // 6 — legacy 3-slot GCLoginSuccess3 p; if (!fill(p, header, body, len)) { return false; } parse_login_success(p.players, p.guild_id, p.guild_name, PLAYER_PER_ACCOUNT3, p.handle, p.random_key); return true; } case HDR_GC_LOGIN_FAILURE: { GCLoginFailure p; if (!fill(p, header, body, len)) { return false; } m_login_failure.assign(p.status, strnlen(p.status, sizeof(p.status))); return true; } case HDR_GC_AUTH_SUCCESS: case HDR_GC_AUTH_SUCCESS_OPENID: case HDR_GC_MATRIX_CARD: case HDR_GC_RUNUP_MATRIX_QUIZ: case HDR_GC_REQUEST_PASSPOD: case HDR_GC_REQUEST_PASSPOD_FAILED: case HDR_GC_HS_REQUEST: case HDR_GC_XTRAP_CS1_REQUEST: case HDR_GC_PANAMA_PACK: // These packets belong to optional account/anti-cheat integrations. // Their static sizes are still registered in wire_classic.h so they // cannot desynchronise the game stream; this client has no matching // Godot UI or vendor runtime to act on them. return true; case HDR_GC_CHARACTER_CREATE_SUCCESS: { GCPlayerCreateSuccess p; if (!fill(p, header, body, len)) { return false; } if (p.account_slot < m_slots.size()) { CharSlot &s = m_slots[p.account_slot]; s.id = p.player.id; s.name.assign(p.player.name, strnlen(p.player.name, sizeof(p.player.name))); s.job = p.player.job; s.level = p.player.level; s.st = p.player.st; s.ht = p.player.ht; s.dx = p.player.dx; s.iq = p.player.iq; s.play_minutes = p.player.play_minutes; s.x = p.player.x; s.y = p.player.y; s.main_part = p.player.main_part; s.hair_part = p.player.hair_part; s.skill_group = p.player.skill_group; s.change_name = p.player.change_name != 0; } m_char_events.push_back({CharEvent::CreateOk, p.account_slot, 0}); return true; } case HDR_GC_CREATE_FAILURE: { GCPlayerCreateFailure p; if (!fill(p, header, body, len)) { return false; } m_char_events.push_back({CharEvent::CreateFail, -1, p.type}); return true; } case HDR_GC_DELETE_SUCCESS: { GCPlayerDeleteSuccess p; if (!fill(p, header, body, len)) { return false; } if (p.account_index < m_slots.size()) { m_slots[p.account_index] = CharSlot{}; } m_char_events.push_back({CharEvent::DeleteOk, p.account_index, 0}); return true; } case HDR_GC_DELETE_WRONG_SOCIAL_ID: m_char_events.push_back({CharEvent::DeleteFail, -1, 0}); return true; case HDR_GC_CHANGE_NAME: { GCChangeNameNotice p; if (!fill(p, header, body, len)) { return false; } std::string name(p.name, strnlen(p.name, sizeof(p.name))); for (auto &s : m_slots) { if (s.id == p.pid) { s.name = name; // GC_CHANGE_NAME is the successful completion of the forced // rename flow; do not prompt again on the next Start click. s.change_name = false; } } m_name_events.push_back({p.pid, std::move(name)}); return true; } case HDR_GC_LOGIN_KEY: { GCLoginKey p; if (!fill(p, header, body, len)) { return false; } m_login_key = p.login_key; return true; } case HDR_GC_EMPIRE: { GCEmpire p; if (!fill(p, header, body, len)) { return false; } m_empire = p.empire; return true; } case HDR_GC_MAIN_CHARACTER: { // 113 GCMainCharacter p; if (!fill(p, header, body, len)) { return false; } m_world.mut_spawn_main(p.vid, p.race, std::string(p.name, strnlen(p.name, sizeof(p.name))), (float)p.x, (float)p.y, (float)p.z); return true; } case HDR_GC_MAIN_CHARACTER3_BGM: { GCMainCharacter3BGM p; if (!fill(p, header, body, len)) { return false; } m_world.mut_spawn_main(p.vid, p.race, std::string(p.name, strnlen(p.name, sizeof(p.name))), (float)p.x, (float)p.y, (float)p.z); return true; } case HDR_GC_MAIN_CHARACTER4_BGM_VOL: { GCMainCharacter4BGM p; if (!fill(p, header, body, len)) { return false; } m_world.mut_spawn_main(p.vid, p.race, std::string(p.name, strnlen(p.name, sizeof(p.name))), (float)p.x, (float)p.y, (float)p.z); return true; } case HDR_GC_CHARACTER_ADD: { // 1 GCCharacterAdd p; if (!fill(p, header, body, len)) { return false; } m_world.mut_spawn(p.vid, p.race, p.type, /*name*/ std::string(), (float)p.x, (float)p.y, (float)p.z, p.angle, p.moving_speed, p.attack_speed); return true; } case HDR_GC_CHARACTER_ADD2: { GCCharacterAdd2 p; if (!fill(p, header, body, len)) { return false; } m_world.mut_spawn(p.vid, p.race, p.type, std::string(p.name, strnlen(p.name, sizeof(p.name))), (float)p.x, (float)p.y, (float)p.z, p.angle, p.moving_speed, p.attack_speed); m_world.mut_char_info(p.vid, {}, p.parts, p.empire, (int32_t)p.guild, 0, p.alignment, p.pk_mode, p.mount_vnum); return true; } case HDR_GC_CHARACTER_DEL: { // 2 GCCharacterDel p; if (!fill(p, header, body, len)) { return false; } m_world.mut_despawn(p.vid); return true; } case HDR_GC_MOVE: { // 3 GCMove p; if (!fill(p, header, body, len)) { return false; } // classic wire heading: deg = bRot * 5 (client NetStream Send/Recv Move) m_world.mut_move(p.vid, (float)p.rot * 5.0f, p.func, (float)p.x, (float)p.y, p.duration); return true; } case HDR_GC_CHARACTER_POINTS: { // 16 GCPoints p; if (!fill(p, header, body, len)) { return false; } m_world.mut_set_points(p.points, POINT_MAX_NUM); return true; } case HDR_GC_CHARACTER_POINT_CHANGE: { // 17 — struct has int32_t header GCPointChange p; if (!fill(p, header, body, len)) { return false; } m_world.mut_set_point(p.type, p.value, p.vid); return true; } case HDR_GC_ITEM_SET: { // 21 GCItemSet p; if (!fill(p, header, body, len)) { return false; } mtnet::ItemAttr a[ITEM_ATTRIBUTE_MAX_NUM]; for (int i = 0; i < ITEM_ATTRIBUTE_MAX_NUM; ++i) { a[i] = {p.attrs[i].type, p.attrs[i].value}; } m_world.mut_item_set(p.cell.window_type, p.cell.cell, p.vnum, p.count, p.flags, p.anti_flags, p.sockets, a); return true; } case HDR_GC_ITEM_DEL: { // 20 — inventory pos only GCItemDel p; if (!fill(p, header, body, len)) { return false; } m_world.mut_item_del(/*WINDOW_INVENTORY*/ 1, p.pos); return true; } case HDR_GC_ITEM_UPDATE: { // 25 GCItemUpdate p; if (!fill(p, header, body, len)) { return false; } mtnet::ItemAttr a[ITEM_ATTRIBUTE_MAX_NUM]; for (int i = 0; i < ITEM_ATTRIBUTE_MAX_NUM; ++i) { a[i] = {p.attrs[i].type, p.attrs[i].value}; } m_world.mut_item_update(p.cell.window_type, p.cell.cell, p.count, p.sockets, a); return true; } case HDR_GC_ITEM_GROUND_ADD: { // 26 GCItemGroundAdd p; if (!fill(p, header, body, len)) { return false; } m_world.mut_ground_add(p.vid, p.vnum, (float)p.x, (float)p.y, (float)p.z); return true; } case HDR_GC_ITEM_GROUND_DEL: { // 27 GCItemGroundDel p; if (!fill(p, header, body, len)) { return false; } m_world.mut_ground_del(p.vid); return true; } case HDR_GC_CHARACTER_UPDATE: case HDR_GC_CHARACTER_UPDATE2: case HDR_GC_CHARACTER_UPDATE_NEW: { // 19 / 24 GCCharacterUpdate p; if (!fill(p, header, body, len)) { return false; } m_world.mut_char_update(p.vid, p.parts, p.moving_speed, p.attack_speed, (int32_t)p.guild_id, p.alignment, p.pk_mode, p.mount_vnum); return true; } case HDR_GC_CHARACTER_POSITION: { // 43 GCCharacterPosition p; if (!fill(p, header, body, len)) { return false; } m_world.mut_set_position(p.vid, p.position); return true; } case HDR_GC_CHANGE_SPEED: { // 18 GCChangeSpeed p; if (!fill(p, header, body, len)) { return false; } m_world.mut_change_speed(p.vid, p.moving_speed); return true; } case HDR_GC_WALK_MODE: { // 111 GCWalkMode p; if (!fill(p, header, body, len)) { return false; } m_world.mut_walk_mode(p.vid, p.mode); return true; } case HDR_GC_STUN: { // 13 GCStun p; if (!fill(p, header, body, len)) { return false; } m_world.mut_stun(p.vid); return true; } case HDR_GC_DEAD: { // 14 GCDead p; if (!fill(p, header, body, len)) { return false; } m_world.mut_dead(p.vid); return true; } case HDR_GC_MOTION: { // 36 GCMotion p; if (!fill(p, header, body, len)) { return false; } m_world.mut_motion(p.vid, p.victim_vid, p.motion); return true; } case HDR_GC_TARGET: { // 63 GCTarget p; if (!fill(p, header, body, len)) { return false; } m_world.mut_target(p.vid, p.hp_percent); return true; } case HDR_GC_DAMAGE_INFO: { // 135 GCDamageInfo p; if (!fill(p, header, body, len)) { return false; } m_world.mut_damage(p.vid, p.flag, p.damage); return true; } case HDR_GC_FISHING: { GCFishing packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_fishing(packet.subheader, packet.info, packet.dir); return true; } case HDR_GC_CHAR_ADDITIONAL_INFO: { // 136 GCCharAddInfo p; if (!fill(p, header, body, len)) { return false; } m_world.mut_char_info(p.vid, std::string(p.name, strnlen(p.name, sizeof(p.name))), p.parts, p.empire, (int32_t)p.guild_id, (int32_t)p.level, p.alignment, p.pk_mode, p.mount_vnum); return true; } case HDR_GC_SKILL_LEVEL: { // 76 GCSkillLevel p; if (!fill(p, header, body, len)) { return false; } uint8_t level[SKILL_MAX_NUM]; uint8_t master[SKILL_MAX_NUM]; for (int i = 0; i < SKILL_MAX_NUM; ++i) { level[i] = p.skills[i].level; master[i] = p.skills[i].master_type; } m_world.mut_skill_levels(level, master, SKILL_MAX_NUM); return true; } case HDR_GC_SKILL_COOLTIME_END: { GCSkillCooltimeEnd p; if (!fill(p, header, body, len)) { return false; } m_world.mut_skill_cooldown_end(p.skill); return true; } case HDR_GC_QUICKSLOT_ADD: { // 28 GCQuickSlotAdd p; if (!fill(p, header, body, len)) { return false; } m_world.mut_quickslot_set(p.pos, p.slot.type, p.slot.position); return true; } case HDR_GC_QUICKSLOT_DEL: { // 29 GCQuickSlotDel p; if (!fill(p, header, body, len)) { return false; } m_world.mut_quickslot_del(p.pos); return true; } case HDR_GC_QUICKSLOT_SWAP: { // 30 GCQuickSlotSwap p; if (!fill(p, header, body, len)) { return false; } m_world.mut_quickslot_swap(p.pos, p.pos_to); return true; } case HDR_GC_AFFECT_ADD: { // 126 GCAffectAdd p; if (!fill(p, header, body, len)) { return false; } m_world.mut_affect_add(p.elem.type, p.elem.point_idx, p.elem.apply_value, p.elem.flag, p.elem.duration); return true; } case HDR_GC_AFFECT_REMOVE: { // 127 GCAffectRemove p; if (!fill(p, header, body, len)) { return false; } m_world.mut_affect_remove(p.type); return true; } case HDR_GC_WHISPER: { // 34 (dynamic) — body = type, name_from[25], text const size_t name_max = CHARACTER_NAME_MAX_LEN + 1; if (len < 1u + name_max) { return true; } uint8_t wtype = body[0]; const char *name = reinterpret_cast(body + 1); const char *text = reinterpret_cast(body + 1 + name_max); size_t text_len = len - 1 - name_max; m_world.mut_whisper(wtype, std::string(name, strnlen(name, name_max)), std::string(text, strnlen(text, text_len))); return true; } case HDR_GC_SYNC_POSITION: { // 5 (dynamic) — body = N x SyncPosElement size_t n = len / sizeof(SyncPosElement); const auto *els = reinterpret_cast(body); for (size_t i = 0; i < n; ++i) { m_world.mut_snap_position(els[i].vid, (float)els[i].x, (float)els[i].y); } return true; } case HDR_GC_PARTY_INVITE: { // 77 GCPartyInvite p; if (!fill(p, header, body, len)) { return false; } m_world.mut_party_invite(p.leader_vid); return true; } case HDR_GC_PARTY_ADD: { // 78 GCPartyAdd p; if (!fill(p, header, body, len)) { return false; } m_world.mut_party_add(p.pid, std::string(p.name, strnlen(p.name, sizeof(p.name)))); return true; } case HDR_GC_PARTY_UPDATE: { // 79 GCPartyUpdate p; if (!fill(p, header, body, len)) { return false; } m_world.mut_party_update(p.pid, p.role, p.percent_hp, p.affects); return true; } case HDR_GC_PARTY_REMOVE: { // 80 GCPartyRemove p; if (!fill(p, header, body, len)) { return false; } m_world.mut_party_remove(p.pid); return true; } case HDR_GC_PARTY_LINK: { // 91 GCPartyLink p; if (!fill(p, header, body, len)) { return false; } m_world.mut_party_link(p.pid, p.vid); return true; } case HDR_GC_PARTY_UNLINK: { // 92 GCPartyUnlink p; if (!fill(p, header, body, len)) { return false; } m_world.mut_party_unlink(p.pid); return true; } case HDR_GC_PARTY_PARAMETER: { // 83 GCPartyParameter p; if (!fill(p, header, body, len)) { return false; } m_world.mut_party_parameter(p.distribute_mode); return true; } case HDR_GC_MESSENGER: { // 74 (dynamic) — body = subheader + records if (len < 1) { return true; } const uint8_t sub = body[0]; const uint8_t *cur = body + 1; const uint8_t *end = body + len; auto read_name = [&](uint8_t n) -> std::string { if (cur + n > end) { return {}; } std::string name(reinterpret_cast(cur), n); cur += n; return name; }; switch (sub) { case MESSENGER_GC_LIST: // LIST is a complete snapshot, so remove friends from a previous // character/server before applying the records that follow. m_world.mut_friend_clear(); while (cur + sizeof(MessengerListEntry) <= end) { const uint8_t connected = cur[0]; const uint8_t n = cur[1]; cur += sizeof(MessengerListEntry); std::string name = read_name(n); if (name.size() != n) { break; } m_world.mut_friend_set(name, (connected & 1u) != 0); m_world.mut_friend_mobile(name, (connected & 2u) != 0); } return true; case MESSENGER_GC_LOGIN: case MESSENGER_GC_LOGOUT: if (cur < end) { const uint8_t n = *cur++; std::string name = read_name(n); if (name.size() == n) { m_world.mut_friend_set(name, sub == MESSENGER_GC_LOGIN); } } return true; case MESSENGER_GC_MOBILE: // 40250 sends [state][name_length][name]. The state is // the mobile-presence flag, not an online/offline transition. if (cur + 2 <= end) { const uint8_t state = *cur++; const uint8_t n = *cur++; std::string name = read_name(n); if (name.size() == n) { m_world.mut_friend_mobile(name, state != 0); } } return true; default: // INVITE has no local representation; it is handled by the // command channel in this 40250 client. return true; } } case HDR_GC_EXCHANGE: { // 42 (static) GCExchange p; if (!fill(p, header, body, len)) { return false; } const bool self = p.is_me != 0; switch (p.subheader) { case EXCHANGE_GC_START: m_world.mut_exchange_start(p.arg1); break; case EXCHANGE_GC_ITEM_ADD: m_world.mut_exchange_item(self, static_cast(p.arg2.cell), p.arg1, static_cast(p.arg3)); break; case EXCHANGE_GC_ITEM_DEL: m_world.mut_exchange_item_del(self, static_cast(p.arg1)); break; case EXCHANGE_GC_GOLD_ADD: m_world.mut_exchange_gold(self, static_cast(p.arg1)); break; case EXCHANGE_GC_ACCEPT: m_world.mut_exchange_accept(self, p.arg1 != 0); break; case EXCHANGE_GC_END: m_world.mut_exchange_end(); break; case EXCHANGE_GC_ALREADY: case EXCHANGE_GC_LESS_GOLD: m_world.mut_exchange_notice(); break; default: break; } return true; } case HDR_GC_SAFEBOX_SIZE: { // 88 GCSafeboxSize p; if (!fill(p, header, body, len)) { return false; } m_world.mut_safebox_open(p.size); return true; } case HDR_GC_SAFEBOX_MONEY_CHANGE: { // 84 GCSafeboxMoneyChange p; if (!fill(p, header, body, len)) { return false; } m_world.mut_safebox_money(p.money); return true; } case HDR_GC_SAFEBOX_SET: { // 85 GCItemSet p; if (!fill(p, header, body, len)) { return false; } mtnet::ItemAttr attrs[ITEM_ATTRIBUTE_MAX_NUM] = {}; for (int i = 0; i < ITEM_ATTRIBUTE_MAX_NUM; ++i) { attrs[i] = {p.attrs[i].type, p.attrs[i].value}; } m_world.mut_safebox_set(p.cell.cell, p.vnum, p.count, p.flags, p.anti_flags, p.sockets, attrs); return true; } case HDR_GC_SAFEBOX_DEL: { // 86 GCItemDel p; if (!fill(p, header, body, len)) { return false; } m_world.mut_safebox_del(p.pos); return true; } case HDR_GC_SAFEBOX_WRONG_PASSWORD: // 87 m_world.mut_safebox_wrong_password(); return true; case HDR_GC_MALL_OPEN: { // 122 GCMallOpen p; if (!fill(p, header, body, len)) { return false; } m_world.mut_mall_open(p.size); return true; } case HDR_GC_MALL_SET: { // 128 GCItemSet p; if (!fill(p, header, body, len)) { return false; } mtnet::ItemAttr attrs[ITEM_ATTRIBUTE_MAX_NUM] = {}; for (int i = 0; i < ITEM_ATTRIBUTE_MAX_NUM; ++i) { attrs[i] = {p.attrs[i].type, p.attrs[i].value}; } m_world.mut_mall_set(p.cell.cell, p.vnum, p.count, p.flags, p.anti_flags, p.sockets, attrs); return true; } case HDR_GC_MALL_DEL: { // 129 GCItemDel p; if (!fill(p, header, body, len)) { return false; } m_world.mut_mall_del(p.pos); return true; } case HDR_GC_SCRIPT: { // 45 (dynamic) — body = [skin][u16 src_size][text] if (len < 3) { return true; } uint8_t skin = body[0]; const char *txt = reinterpret_cast(body + 3); size_t txt_len = len - 3; m_world.mut_script(skin, std::string(txt, strnlen(txt, txt_len))); return true; } case HDR_GC_QUEST_CONFIRM: { // 46 (static) GCQuestConfirm p; if (!fill(p, header, body, len)) { return false; } m_world.mut_quest_confirm(std::string(p.msg, strnlen(p.msg, sizeof(p.msg))), p.timeout, p.request_pid); return true; } case HDR_GC_QUEST_INFO: { // 81 (dynamic) — body = [u16 index][u8 flag][flag-driven...] if (len < 3) { return true; } uint16_t index; std::memcpy(&index, body, 2); uint8_t flag = body[2]; const uint8_t *cur = body + 3; const uint8_t *end = body + len; auto take_str = [&](size_t cap) -> std::string { size_t n = 0; while (cur + n < end && cur[n] != 0 && n < cap) { ++n; } std::string s(reinterpret_cast(cur), n); cur += (cur + n < end) ? n + 1 : n; // skip NUL if present return s; }; auto take_i32 = [&]() -> int32_t { int32_t v = 0; if (cur + 4 <= end) { std::memcpy(&v, cur, 4); cur += 4; } return v; }; std::string title, clock_name, counter_name, icon; int32_t clock_value = 0, counter_value = 0; if (flag & QUEST_SEND_TITLE) { title = take_str(28); } if (flag & QUEST_SEND_CLOCK_NAME) { clock_name = take_str(16); } if (flag & QUEST_SEND_CLOCK_VALUE) { clock_value = take_i32(); } if (flag & QUEST_SEND_COUNTER_NAME) { counter_name = take_str(16); } if (flag & QUEST_SEND_COUNTER_VALUE) { counter_value = take_i32(); } if (flag & QUEST_SEND_ICON_FILE) { icon = take_str(24); } m_world.mut_quest_info(index, flag, (flag & QUEST_SEND_IS_BEGIN) != 0, title, clock_name, clock_value, counter_name, counter_value, icon); return true; } case HDR_GC_SHOP: { // 38 (dynamic) — body = [subheader][sub-specific...] if (len < 1) { return true; } uint8_t sub = body[0]; const uint8_t *cur = body + 1; const uint8_t *end = body + len; auto read_tab = [&](ShopTab &tab) { for (int j = 0; j < SHOP_HOST_ITEM_MAX_NUM && cur + (int)sizeof(ShopItem43) <= end; ++j) { ShopItem43 si; std::memcpy(&si, cur, sizeof(si)); cur += sizeof(si); if (si.vnum != 0) { tab.items.push_back({si.vnum, si.price, si.count, si.display_pos}); } } }; switch (sub) { case SHOP_SUB_START: { if (cur + 4 > end) { return true; } uint32_t vid; std::memcpy(&vid, cur, 4); cur += 4; ShopTab tab; // unnamed -> flat single shelf read_tab(tab); m_world.mut_shop_open(vid, {std::move(tab)}); return true; } case SHOP_SUB_START_EX: { if (cur + 5 > end) { return true; } uint32_t vid; std::memcpy(&vid, cur, 4); cur += 4; uint8_t tab_count = *cur++; std::vector tabs; for (uint8_t t = 0; t < tab_count && t < SHOP_TAB_COUNT_MAX; ++t) { if (cur + (int)sizeof(ShopTabHead32) > end) { break; } ShopTabHead32 th; std::memcpy(&th, cur, sizeof(th)); cur += sizeof(th); ShopTab tab; tab.name.assign(th.name, strnlen(th.name, SHOP_TAB_NAME_MAX)); tab.coin_type = th.coin_type; read_tab(tab); tabs.push_back(std::move(tab)); } m_world.mut_shop_open(vid, tabs); return true; } case SHOP_SUB_END: m_world.mut_shop_close(); return true; case SHOP_SUB_NOT_ENOUGH_MONEY: case SHOP_SUB_NOT_ENOUGH_MONEY_EX: m_world.mut_shop_error("NOT_ENOUGH_MONEY"); return true; case SHOP_SUB_SOLDOUT: case SHOP_SUB_SOLD_OUT: m_world.mut_shop_error("SOLDOUT"); return true; case SHOP_SUB_INVENTORY_FULL: m_world.mut_shop_error("INVENTORY_FULL"); return true; case SHOP_SUB_INVALID_POS: m_world.mut_shop_error("INVALID_POS"); return true; case SHOP_SUB_UPDATE_ITEM: { if (cur + (int)sizeof(ShopUpdateItem) > end) { return true; } ShopUpdateItem update; std::memcpy(&update, cur, sizeof(update)); m_world.mut_shop_update_item(update.pos, update.item.vnum, update.item.price, update.item.count); return true; } case SHOP_SUB_UPDATE_PRICE: { if (cur + (int)sizeof(ShopUpdatePrice) > end) { return true; } ShopUpdatePrice update; std::memcpy(&update, cur, sizeof(update)); m_world.mut_shop_update_price(update.price); return true; } default: return true; // OK has no state payload } } case HDR_GC_SHOP_SIGN: { GCShopSign packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_shop_sign(packet.vid, std::string(packet.sign, strnlen(packet.sign, sizeof(packet.sign)))); return true; } case HDR_GC_GUILD: { // dynamic — body = [subheader][sub-specific...] if (len < 1) { return true; } const uint8_t sub = body[0]; const uint8_t *cur = body + 1; const uint8_t *end = body + len; switch (sub) { case GUILD_GC_LOGIN: case GUILD_GC_LOGOUT: if (cur + 4 <= end) { uint32_t pid; std::memcpy(&pid, cur, sizeof(pid)); m_world.mut_guild_member_online(pid, sub == GUILD_GC_LOGIN); } return true; case GUILD_GC_INFO: { if (cur + (int)sizeof(GuildInfo35) > end) { return true; } GuildInfo35 info; std::memcpy(&info, cur, sizeof(info)); m_world.mut_guild_info(info.member_count, info.max_member_count, info.guild_id, info.master_pid, info.exp, info.level, std::string(info.name, strnlen(info.name, sizeof(info.name))), info.gold, info.has_land != 0); return true; } case GUILD_GC_LIST: { if ((end - cur) % (int)sizeof(GuildMember38) != 0) { return true; } m_world.mut_guild_clear_members(); while (cur + (int)sizeof(GuildMember38) <= end) { GuildMember38 member; std::memcpy(&member, cur, sizeof(member)); cur += sizeof(member); m_world.mut_guild_member(member.pid, member.grade, member.is_general != 0, member.job, member.level, member.offer, std::string(member.name, strnlen(member.name, sizeof(member.name)))); } return true; } case GUILD_GC_GRADE: { if (cur >= end) { return true; } const uint8_t count = *cur++; for (uint8_t i = 0; i < count; ++i) { if (cur + 1 + (int)sizeof(GuildGrade10) > end) { return true; } const uint8_t grade = *cur++; GuildGrade10 data; std::memcpy(&data, cur, sizeof(data)); cur += sizeof(data); m_world.mut_guild_grade(grade, std::string(data.name, strnlen(data.name, sizeof(data.name))), data.auth); } return true; } case GUILD_GC_ADD: { if (cur + (int)sizeof(GuildMember38) > end) { return true; } GuildMember38 member; std::memcpy(&member, cur, sizeof(member)); m_world.mut_guild_member(member.pid, member.grade, member.is_general != 0, member.job, member.level, member.offer, std::string(member.name, strnlen(member.name, sizeof(member.name)))); return true; } case GUILD_GC_REMOVE: if (cur + 4 <= end) { uint32_t pid; std::memcpy(&pid, cur, sizeof(pid)); m_world.mut_guild_remove_member(pid); } return true; case GUILD_GC_GRADE_NAME: if (cur + 1 + GUILD_GRADE_NAME_MAX_LEN + 1 <= end) { const uint8_t grade = *cur++; m_world.mut_guild_grade_name(grade, std::string(reinterpret_cast(cur), strnlen(reinterpret_cast(cur), GUILD_GRADE_NAME_MAX_LEN + 1))); } return true; case GUILD_GC_GRADE_AUTH: if (cur + 2 <= end) { m_world.mut_guild_grade_auth(cur[0], cur[1]); } return true; case GUILD_GC_CHANGE_EXP: if (cur + 5 <= end) { const uint8_t level = *cur++; uint32_t exp; std::memcpy(&exp, cur, sizeof(exp)); m_world.mut_guild_exp(level, exp); } return true; case GUILD_GC_CHANGE_MEMBER_GRADE: if (cur + 5 <= end) { uint32_t pid; std::memcpy(&pid, cur, sizeof(pid)); m_world.mut_guild_member_grade(pid, cur[4]); } return true; case GUILD_GC_SKILL_INFO: { if (cur + (int)sizeof(GuildSkill17) > end) { return true; } GuildSkill17 skill; std::memcpy(&skill, cur, sizeof(skill)); m_world.mut_guild_skill(skill.skill_point, skill.levels, skill.guild_point, skill.max_guild_point); return true; } case GUILD_GC_CHANGE_MEMBER_GENERAL: if (cur + 5 <= end) { uint32_t pid; std::memcpy(&pid, cur, sizeof(pid)); m_world.mut_guild_member_general(pid, cur[4] != 0); } return true; case GUILD_GC_WAR: if (cur + (int)sizeof(GuildWar10) <= end) { GuildWar10 war; std::memcpy(&war, cur, sizeof(war)); m_world.mut_guild_war(war.self_id, war.opponent_id, war.type, war.state); } return true; case GUILD_GC_GUILD_WAR_LIST: case GUILD_GC_GUILD_WAR_END_LIST: { if ((end - cur) % (int)sizeof(GuildWarPair8) != 0) { return true; } std::vector pairs; while (cur + (int)sizeof(GuildWarPair8) <= end) { GuildWarPair8 pair; std::memcpy(&pair, cur, sizeof(pair)); cur += sizeof(pair); pairs.push_back({pair.src, pair.dst}); } m_world.mut_guild_war_pairs(pairs, sub == GUILD_GC_GUILD_WAR_END_LIST); return true; } case GUILD_GC_WAR_POINT: if (cur + (int)sizeof(GuildWarPoint12) <= end) { GuildWarPoint12 point; std::memcpy(&point, cur, sizeof(point)); m_world.mut_guild_war_score(point.gain_guild_id, point.opponent_guild_id, point.point); } return true; case GUILD_GC_COMMENTS: { if (cur >= end) { m_world.mut_guild_comments({}); return true; } const uint8_t count = *cur++; std::vector comments; comments.reserve(count); for (uint8_t i = 0; i < count; ++i) { if (cur + (int)sizeof(GuildComment80) > end) { break; } GuildComment80 packet; std::memcpy(&packet, cur, sizeof(packet)); cur += sizeof(packet); comments.push_back({packet.id, std::string(packet.name, strnlen(packet.name, sizeof(packet.name))), std::string(packet.content, strnlen(packet.content, sizeof(packet.content)))}); } m_world.mut_guild_comments(comments); return true; } case GUILD_GC_GUILD_INVITE: { GuildInvite17 packet; if (cur + (int)sizeof(packet) <= end) { std::memcpy(&packet, cur, sizeof(packet)); m_world.mut_guild_invite(packet.guild_id, std::string(packet.guild_name, strnlen(packet.guild_name, sizeof(packet.guild_name)))); } return true; } case GUILD_GC_MONEY_CHANGE: if (cur + 4 <= end) { uint32_t gold; std::memcpy(&gold, cur, sizeof(gold)); m_world.mut_guild_money(gold); } return true; case GUILD_GC_NAME: if ((end - cur) % (int)sizeof(GuildName16) != 0) { return true; } while (cur + (int)sizeof(GuildName16) <= end) { GuildName16 name; std::memcpy(&name, cur, sizeof(name)); cur += sizeof(name); m_world.mut_guild_name(name.id, std::string(name.name, strnlen(name.name, sizeof(name.name)))); } return true; default: return true; } } case HDR_GC_ITEM_USE: { GCItemUse packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_item_used(packet.vnum); return true; } case HDR_GC_ITEM_OWNERSHIP: { GCItemOwnership packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_ground_owner(packet.vid, std::string(packet.owner, strnlen(packet.owner, sizeof(packet.owner)))); return true; } case HDR_GC_OWNERSHIP: { GCOwnership packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_ground_owner(packet.victim_vid, std::to_string(packet.owner_vid)); return true; } case HDR_GC_MOUNT: { GCMount packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_mount(packet.vid, packet.mount_vnum); return true; } case HDR_GC_CREATE_FLY: { GCCreateFly packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_fly(packet.type, packet.start_vid, packet.end_vid); return true; } case HDR_GC_FLY_TARGETING: case HDR_GC_ADD_FLY_TARGETING: { GCFlyTargeting packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_fly_target(packet.shooter_vid, packet.target_vid, packet.x, packet.y, header == HDR_GC_ADD_FLY_TARGETING); return true; } case HDR_GC_PVP: { GCPvp packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_pvp(packet.src_vid, packet.dst_vid, packet.mode); return true; } case HDR_GC_DUEL_START: { // TPacketGCDuelStart is [header][u16 whole_size] followed by the // DWORD VIDs of every opponent in the duel. ClassicStream has // already consumed the three-byte dynamic prefix, so `body` is the // packed VID list here. if (len % sizeof(uint32_t) != 0) { return false; } std::vector opponents; opponents.reserve(len / sizeof(uint32_t)); for (uint32_t i = 0; i < len; i += sizeof(uint32_t)) { uint32_t vid = 0; std::memcpy(&vid, body + i, sizeof(vid)); opponents.push_back(vid); } m_world.mut_duel_start(opponents.data(), (int)opponents.size()); return true; } case HDR_GC_SPECIAL_EFFECT: { GCSpecialEffect packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_special_effect(packet.vid, packet.type); return true; } case HDR_GC_SPECIFIC_EFFECT: { GCSpecificEffect packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_specific_effect(packet.vid, std::string(packet.effect_file, strnlen(packet.effect_file, sizeof(packet.effect_file)))); return true; } case HDR_GC_SKILL_GROUP: { GCSkillGroup packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_skill_group(packet.skill_group); return true; } case HDR_GC_SKILL_LEVEL_OLD: { GCSkillLevelOld packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_skill_levels(packet.levels, nullptr, SKILL_MAX_NUM); return true; } case HDR_GC_OBSERVER_ADD: case HDR_GC_OBSERVER_MOVE: { GCObserverMove packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_observer(header == HDR_GC_OBSERVER_ADD ? ObserverEvent::Add : ObserverEvent::Move, packet.vid, (int32_t)packet.x * 100, (int32_t)packet.y * 100); return true; } case HDR_GC_OBSERVER_REMOVE: { GCObserverRemove packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_observer(ObserverEvent::Remove, packet.vid, 0, 0); return true; } case HDR_GC_REFINE_INFORMATION_OLD: case HDR_GC_REFINE_INFORMATION: { GCRefineInfo packet; if (!fill(packet, header, body, len)) { return false; } RefineCue::Mat materials[5] = {}; for (int i = 0; i < 5; ++i) { materials[i].vnum = packet.materials[i].vnum; materials[i].count = packet.materials[i].count; } m_world.mut_refine(packet.type, packet.pos, packet.src_vnum, packet.result_vnum, packet.material_count, packet.cost, packet.prob, materials); return true; } case HDR_GC_DRAGON_SOUL_REFINE: { GCDragonSoulRefine packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_dragon_soul_refine(packet.sub_type, packet.pos.window_type, packet.pos.cell); return true; } case HDR_GC_WARP: { GCWarpClassic packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_warp(packet.x, packet.y, packet.addr, packet.port); return true; } case HDR_GC_TIME: { GCTime packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_server_time(packet.time); return true; } case HDR_GC_CHANNEL: { GCChannel packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_channel(packet.channel); return true; } case HDR_GC_DUNGEON: { if (len < sizeof(GCDungeonHead)) { return true; } GCDungeonHead packet; std::memcpy(&packet, body, sizeof(packet)); if (packet.subheader == 1 && len >= sizeof(packet) + sizeof(GCDungeonDestination)) { GCDungeonDestination destination; std::memcpy(&destination, body + sizeof(packet), sizeof(destination)); m_world.mut_dungeon(packet.subheader, destination.x, destination.y, true); } else { m_world.mut_dungeon(packet.subheader, 0, 0, false); } return true; } case HDR_GC_NPC_POSITION: { if (len < sizeof(GCNPCPositionHead)) { return true; } GCNPCPositionHead head; std::memcpy(&head, body, sizeof(head)); const uint8_t *cur = body + sizeof(head); const uint8_t *end = body + len; std::vector marks; for (uint16_t i = 0; i < head.count && cur + (int)sizeof(NPCPosition34) <= end; ++i) { NPCPosition34 entry; std::memcpy(&entry, cur, sizeof(entry)); cur += sizeof(entry); marks.push_back({entry.type, 0, std::string(entry.name, strnlen(entry.name, sizeof(entry.name))), entry.x, entry.y}); } m_world.mut_npc_marks(marks); return true; } case HDR_GC_LAND_LIST: { const uint8_t *cur = body; const uint8_t *end = body + len; if (len % sizeof(LandPacketElement24) != 0) { return true; } std::vector areas; while (cur + (int)sizeof(LandPacketElement24) <= end) { LandPacketElement24 entry; std::memcpy(&entry, cur, sizeof(entry)); cur += sizeof(entry); areas.push_back({entry.id, entry.x, entry.y, entry.width, entry.height, entry.guild_id}); } m_world.mut_land_areas(areas); return true; } case HDR_GC_TARGET_CREATE: { GCTargetCreate43 packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_marker_create(packet.id, std::string(packet.name, strnlen(packet.name, sizeof(packet.name))), packet.vid, packet.type); return true; } case HDR_GC_TARGET_UPDATE: { GCTargetUpdate13 packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_marker_update(packet.id, packet.x, packet.y); return true; } case HDR_GC_TARGET_DELETE: { GCTargetDelete5 packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_marker_delete(packet.id); return true; } case HDR_GC_LOVER_INFO: { GCLoverInfo packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_lover(std::string(packet.name, strnlen(packet.name, sizeof(packet.name))), packet.love_point); return true; } case HDR_GC_LOVE_POINT_UPDATE: { GCLovePointUpdate packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_love_point(packet.love_point); return true; } case HDR_GC_DIG_MOTION: { GCDigMotion packet; if (!fill(packet, header, body, len)) { return false; } m_world.mut_dig(packet.vid, packet.target_vid, packet.count); return true; } case HDR_GC_REQUEST_MAKE_GUILD: ++m_guild_make_requests; return true; case HDR_GC_VIEW_EQUIP: { GCViewEquip packet; if (!fill(packet, header, body, len)) { return false; } Item items[mtnet::VIEW_EQUIP_WEAR_MAX_NUM] = {}; for (int i = 0; i < mtnet::VIEW_EQUIP_WEAR_MAX_NUM && i < CLASSIC_WEAR_MAX_NUM; ++i) { items[i].vnum = packet.equips[i].vnum; items[i].count = packet.equips[i].count; std::memcpy(items[i].sockets, packet.equips[i].sockets, sizeof(items[i].sockets)); for (int j = 0; j < ITEM_ATTRIBUTE_MAX_NUM; ++j) { items[i].attrs[j] = {packet.equips[i].attrs[j].type, packet.equips[i].attrs[j].value}; } } m_world.mut_view_equipment(packet.vid, items, mtnet::VIEW_EQUIP_WEAR_MAX_NUM); return true; } case HDR_GC_CHAT: { // 4 (dynamic) — body = type,vid,empire,text (after [hdr][u16 size]) if (len < 6) { return true; // malformed, ignore } uint8_t type = body[0]; uint32_t vid; std::memcpy(&vid, body + 1, 4); // body[5] = empire; text follows const char *txt = reinterpret_cast(body + 6); m_world.mut_chat(type, vid, std::string(txt, strnlen(txt, len - 6))); return true; } default: // not handled yet — harmless, keep going. classic_stream already // rejected unknown *static* headers via the size table. return true; } } } // namespace mtnet::classic