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

1410 lines
39 KiB
C++

#include "classic_parser.h"
#include <cstring>
#include <vector>
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<const char *>(body + 1);
const char *text = reinterpret_cast<const char *>(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<const SyncPosElement *>(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<const char *>(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<uint8_t>(p.arg2.cell), p.arg1,
static_cast<uint8_t>(p.arg3));
break;
case EXCHANGE_GC_ITEM_DEL:
m_world.mut_exchange_item_del(self, static_cast<uint8_t>(p.arg1));
break;
case EXCHANGE_GC_GOLD_ADD:
m_world.mut_exchange_gold(self, static_cast<int64_t>(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<const char *>(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<const char *>(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<ShopTab> 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<const char *>(cur),
strnlen(reinterpret_cast<const char *>(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<GuildWarPair> 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<GuildComment> 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<uint32_t> 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<NPCMark> 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<LandArea> 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<const char *>(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