Files
mtgodot-poc/extension/src/net/classic/wire_classic.h
T
shenleiandClaude Code 8092ff44e2 装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排,
  新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS),
  帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案
- 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、
  物品槽失配置灰(服务器回收/换武器后刷新)
- 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、
  被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1)
- 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/
  near/far/divorce 命令事件→在线/在身边状态与离婚隐藏
- 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示;
  仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close;
  仓库金钱经核实 40250 无 wire 协议,UI 定界只读
- app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫
- 相机 look_at 零向量守卫(首帧/传送落点)
- 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为
  HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路)
- docs/CLIENT-GAP.md 同步增量 129

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
2026-09-07 21:43:30 +09:00

1511 lines
56 KiB
C++

#pragma once
// 40250 "classic" Metin2 wire format — the switchable `MT_PROTOCOL=classic`
// backend (see docs/CLIENT-40250-PORT.md). This is NOT the m2dev fork protocol
// (that stays in ../wire.h).
//
// Framing (docs §2.2):
// recv: read 1 byte header -> look up static size (packet_size) -> read body.
// dynamic-size packets carry a `uint16 size` right after the header.
// send: most CG game packets append 1 trailing `uint8 sequence` byte once
// sequence mode is on (is_sequence()).
//
// Values + struct layouts verified against the 40250 sources:
// Server/metin2/src/server/game/src/packet.h (header enum, EPhase, wire structs)
// Server/metin2/src/server/game/src/packet_info.cpp (CG size table + bSeq flags)
// Server/metin2/src/server/common/{tables.h,length.h}
// ClientVS22/source/UserInterface/{Packet.h,PythonNetworkStream.cpp} (GC size/dynamic map)
// ClientVS22/source/EterBase/cipher.{h,cpp}
//
// The 40250 client Packet.h header enum is #if defined(GAIDEN)-gated and the
// shipped client build does NOT define GAIDEN; where client/server names differ
// for the same value we follow the SERVER packet.h name (it is the wire value).
#include <cstddef>
#include <cstdint>
// §1.10: every fixed-length `char name[...]` / sign / comment field below is
// filled at the call site via mtnet::to_wire(src, cap) (byte cap, no split
// multibyte char) and read back via mtnet::from_wire_str().
#include "../text_codec.h"
namespace mtnet::classic {
// ------------------------------------------------------------------ constants
inline constexpr int CHARACTER_NAME_MAX_LEN = 24; // common/length.h -> name[25]
inline constexpr int GUILD_NAME_MAX_LEN = 12; // -> guild_name[13]
inline constexpr int GUILD_GRADE_NAME_MAX_LEN = 8;
inline constexpr int GUILD_COMMENT_MAX_LEN = 50;
inline constexpr int GUILD_SKILL_MAX_NUM = 12;
inline constexpr int PLAYER_PER_ACCOUNT = 4; // common/length.h
inline constexpr int PLAYER_PER_ACCOUNT3 = 3; // legacy GC_LOGIN_SUCCESS (hdr 6)
inline constexpr int ID_MAX_NUM = 30; // Packet.h -> login[31]
inline constexpr int PASS_MAX_NUM = 16; // -> passwd[17]
inline constexpr int PRIVATE_CODE_LENGTH = 8;
inline constexpr int POINT_MAX_NUM = 255; // common/length.h:58
inline constexpr int CHR_EQUIPPART_NUM = 4; // ARMOR/WEAPON/HEAD/HAIR
inline constexpr int LOGIN_STATUS_MAX_LEN = 8; // -> szStatus[9]
inline constexpr int KEY_AGREEMENT_MAX_DATA_LEN = 256;
inline constexpr int ITEM_SOCKET_MAX_NUM = 3; // common/item_length.h
inline constexpr int ITEM_ATTRIBUTE_MAX_NUM = 7;
inline constexpr int SKILL_MAX_NUM = 255; // common/length.h
inline constexpr int QUICKSLOT_MAX_NUM = 36;
inline constexpr int SHOP_HOST_ITEM_MAX_NUM = 40;
inline constexpr int SHOP_TAB_NAME_MAX = 32;
inline constexpr int SHOP_SIGN_MAX_LEN = 32;
inline constexpr int SHOP_TAB_COUNT_MAX = 3;
inline constexpr int GUILD_MARK_WIDTH = 16;
inline constexpr int GUILD_MARK_HEIGHT = 12;
inline constexpr int MARK_BLOCK_TOTAL_COUNT = 80;
// CHRTYPE_* — the domain of Entity::ch_type and packet_add_char.bType
// (client GameType.h / server char.h). Values line up with the Entity comment.
enum : uint8_t {
CHRTYPE_PC = 0,
CHRTYPE_NPC = 1,
CHRTYPE_MONSTER = 2,
CHRTYPE_STONE = 3,
CHRTYPE_WARP = 4,
CHRTYPE_DOOR = 5,
CHRTYPE_BUILDING = 6,
CHRTYPE_PET = 7,
CHRTYPE_HORSE = 8,
CHRTYPE_GOTO = 9,
};
// Races the reference client never spawns — REF/UserInterface/
// PythonNetworkStreamPhaseGameActor.cpp:66 IsInvisibleRace(). GC_CHARACTER_ADD
// for one of these is dropped before any pending/stash step.
inline constexpr bool is_invisible_race(uint16_t race) {
return race == 20025 || race == 20038 || race == 20039;
}
// EPacketShopSubHeaders (client Packet.h L1864)
enum : uint8_t {
SHOP_SUB_START = 0,
SHOP_SUB_END = 1,
SHOP_SUB_UPDATE_ITEM = 2,
SHOP_SUB_UPDATE_PRICE = 3,
SHOP_SUB_OK = 4,
SHOP_SUB_NOT_ENOUGH_MONEY = 5,
SHOP_SUB_SOLDOUT = 6,
SHOP_SUB_INVENTORY_FULL = 7,
SHOP_SUB_INVALID_POS = 8,
SHOP_SUB_SOLD_OUT = 9,
SHOP_SUB_START_EX = 10,
SHOP_SUB_NOT_ENOUGH_MONEY_EX = 11,
};
// ------------------------------------------------------------------ EPhase
// Server enum EPhase (packet.h:850). Only 0..6 reach the client via GC_PHASE.
enum Phase : uint8_t {
PHASE_CLOSE = 0,
PHASE_HANDSHAKE = 1,
PHASE_LOGIN = 2,
PHASE_SELECT = 3,
PHASE_LOADING = 4,
PHASE_GAME = 5,
PHASE_DEAD = 6,
PHASE_CLIENT_CONNECTING = 7,
PHASE_DBCLIENT = 8,
PHASE_P2P = 9,
PHASE_AUTH = 10,
PHASE_TEEN = 11,
PHASE_PASSPOD = 12,
};
// ------------------------------------------------------------------ headers
// Control plane (two-directional). Decimal values shown for the 0xF- block.
enum : uint8_t {
HDR_HANDSHAKE = 0xFF, // GC + CG
HDR_GC_BINDUDP = 0xFE,
HDR_CG_PONG = 0xFE,
HDR_GC_PHASE = 0xFD,
HDR_CG_CLIENT_VERSION = 0xFD,
HDR_GC_TIME_SYNC = 0xFC, // client name: HEADER_GC_HANDSHAKE_OK
HDR_CG_TIME_SYNC = 0xFC,
HDR_KEY_AGREEMENT = 0xFB, // GC + CG
HDR_GC_KEY_AGREEMENT_COMPLETED = 0xFA,
HDR_CG_CLIENT_VERSION2 = 0xF1,
};
// CG (client -> server). Server packet.h.
enum : uint8_t {
HDR_CG_LOGIN = 1,
HDR_CG_ATTACK = 2,
HDR_CG_CHAT = 3,
HDR_CG_CHARACTER_CREATE = 4,
HDR_CG_CHARACTER_DELETE = 5,
HDR_CG_CHARACTER_SELECT = 6,
HDR_CG_MOVE = 7,
HDR_CG_SYNC_POSITION = 8,
HDR_CG_ENTERGAME = 10,
HDR_CG_ITEM_USE = 11,
HDR_CG_ITEM_DROP = 12,
HDR_CG_ITEM_MOVE = 13,
HDR_CG_ITEM_PICKUP = 15,
HDR_CG_QUICKSLOT_ADD = 16,
HDR_CG_QUICKSLOT_DEL = 17,
HDR_CG_QUICKSLOT_SWAP = 18,
HDR_CG_WHISPER = 19,
HDR_CG_ITEM_DROP2 = 20,
HDR_CG_ON_CLICK = 26,
HDR_CG_EXCHANGE = 27,
HDR_CG_CHARACTER_POSITION = 28,
HDR_CG_SCRIPT_ANSWER = 29,
HDR_CG_QUEST_INPUT_STRING = 30,
HDR_CG_QUEST_CONFIRM = 31,
HDR_CG_SHOP = 50,
HDR_CG_FLY_TARGETING = 51,
HDR_CG_USE_SKILL = 52,
HDR_CG_ADD_FLY_TARGETING = 53,
HDR_CG_SHOOT = 54,
HDR_CG_MYSHOP = 55,
HDR_CG_ITEM_USE_TO_ITEM = 60,
HDR_CG_TARGET = 61,
HDR_CG_TEXT = 64,
HDR_CG_WARP = 65,
HDR_CG_SCRIPT_BUTTON = 66,
HDR_CG_MESSENGER = 67,
HDR_CG_MALL_CHECKOUT = 69,
HDR_CG_SAFEBOX_CHECKIN = 70,
HDR_CG_SAFEBOX_CHECKOUT = 71,
HDR_CG_PARTY_INVITE = 72,
HDR_CG_PARTY_INVITE_ANSWER = 73,
HDR_CG_PARTY_REMOVE = 74,
HDR_CG_PARTY_SET_STATE = 75,
HDR_CG_PARTY_USE_SKILL = 76,
HDR_CG_SAFEBOX_ITEM_MOVE = 77,
HDR_CG_PARTY_PARAMETER = 78,
HDR_CG_GUILD = 80,
HDR_CG_ANSWER_MAKE_GUILD = 81,
HDR_CG_FISHING = 82,
HDR_CG_ITEM_GIVE = 83,
HDR_CG_EMPIRE = 90,
HDR_CG_REFINE = 96,
HDR_CG_MARK_LOGIN = 100,
HDR_CG_MARK_CRCLIST = 101,
HDR_CG_MARK_UPLOAD = 102,
HDR_CG_MARK_IDXLIST = 104,
HDR_CG_HACK = 105,
HDR_CG_CHANGE_NAME = 106,
HDR_CG_LOGIN2 = 109,
HDR_CG_DUNGEON = 110,
HDR_CG_LOGIN3 = 111,
HDR_CG_GUILD_SYMBOL_UPLOAD = 112,
HDR_CG_SYMBOL_CRC = 113,
HDR_CG_SCRIPT_SELECT_ITEM = 114,
HDR_CG_LOGIN5_OPENID = 116,
HDR_CG_PASSPOD_ANSWER = 202,
HDR_CG_HS_ACK = 203,
HDR_CG_XTRAP_ACK = 204,
HDR_CG_DRAGON_SOUL_REFINE = 205,
HDR_CG_STATE_CHECKER = 206,
};
// GC (server -> client). Server packet.h (wire authority).
enum : uint8_t {
HDR_GC_CHARACTER_ADD = 1,
HDR_GC_CHARACTER_DEL = 2,
HDR_GC_MOVE = 3,
HDR_GC_CHAT = 4,
HDR_GC_SYNC_POSITION = 5,
HDR_GC_LOGIN_SUCCESS = 6, // client: LOGIN_SUCCESS3 (players[3]); legacy
HDR_GC_LOGIN_FAILURE = 7,
HDR_GC_CHARACTER_CREATE_SUCCESS = 8,
HDR_GC_CREATE_FAILURE = 9,
HDR_GC_DELETE_SUCCESS = 10,
HDR_GC_DELETE_WRONG_SOCIAL_ID = 11,
HDR_GC_ATTACK = 12,
HDR_GC_STUN = 13,
HDR_GC_DEAD = 14,
HDR_GC_MAIN_CHARACTER_OLD = 15,
HDR_GC_CHARACTER_POINTS = 16,
HDR_GC_CHARACTER_POINT_CHANGE = 17,
HDR_GC_CHANGE_SPEED = 18,
HDR_GC_CHARACTER_UPDATE = 19,
HDR_GC_ITEM_DEL = 20,
HDR_GC_ITEM_SET = 21,
HDR_GC_ITEM_USE = 22,
HDR_GC_ITEM_DROP = 23,
HDR_GC_CHARACTER_UPDATE_NEW = 24,
HDR_GC_ITEM_UPDATE = 25,
HDR_GC_ITEM_GROUND_ADD = 26,
HDR_GC_ITEM_GROUND_DEL = 27,
HDR_GC_QUICKSLOT_ADD = 28,
HDR_GC_QUICKSLOT_DEL = 29,
HDR_GC_QUICKSLOT_SWAP = 30,
HDR_GC_ITEM_OWNERSHIP = 31,
HDR_GC_LOGIN_SUCCESS_NEWSLOT = 32, // client: LOGIN_SUCCESS4 (players[4]) — what 40250 sends
HDR_GC_WHISPER = 34,
HDR_GC_MOTION = 36,
HDR_GC_PARTS = 37,
HDR_GC_SHOP = 38, // dynamic
HDR_GC_SHOP_SIGN = 39,
HDR_GC_DUEL_START = 40, // dynamic
HDR_GC_PVP = 41,
HDR_GC_EXCHANGE = 42,
HDR_GC_CHARACTER_POSITION = 43,
HDR_GC_PING = 44,
HDR_GC_SCRIPT = 45, // dynamic
HDR_GC_QUEST_CONFIRM = 46,
HDR_GC_MOUNT = 61,
HDR_GC_OWNERSHIP = 62,
HDR_GC_TARGET = 63,
HDR_GC_WARP = 65,
HDR_GC_ADD_FLY_TARGETING = 69,
HDR_GC_CREATE_FLY = 70,
HDR_GC_FLY_TARGETING = 71,
HDR_GC_SKILL_LEVEL_OLD = 72,
HDR_GC_SKILL_COOLTIME_END = 73,
HDR_GC_MESSENGER = 74, // dynamic
HDR_GC_GUILD = 75, // dynamic
HDR_GC_SKILL_LEVEL = 76,
HDR_GC_PARTY_INVITE = 77,
HDR_GC_PARTY_ADD = 78,
HDR_GC_PARTY_UPDATE = 79,
HDR_GC_PARTY_REMOVE = 80,
HDR_GC_QUEST_INFO = 81, // dynamic
HDR_GC_REQUEST_MAKE_GUILD = 82,
HDR_GC_PARTY_PARAMETER = 83,
HDR_GC_SAFEBOX_MONEY_CHANGE = 84,
HDR_GC_SAFEBOX_SET = 85,
HDR_GC_SAFEBOX_DEL = 86,
HDR_GC_SAFEBOX_WRONG_PASSWORD = 87,
HDR_GC_SAFEBOX_SIZE = 88,
HDR_GC_FISHING = 89,
HDR_GC_EMPIRE = 90,
HDR_GC_PARTY_LINK = 91,
HDR_GC_PARTY_UNLINK = 92,
HDR_GC_REFINE_INFORMATION_OLD = 95,
HDR_GC_OBSERVER_ADD = 96,
HDR_GC_OBSERVER_REMOVE = 97,
HDR_GC_OBSERVER_MOVE = 98,
HDR_GC_VIEW_EQUIP = 99,
HDR_GC_MARK_BLOCK = 100,
HDR_GC_MARK_IDXLIST = 102,
HDR_GC_TIME = 106,
HDR_GC_CHANGE_NAME = 107,
HDR_GC_DUNGEON = 110, // dynamic
HDR_GC_WALK_MODE = 111,
HDR_GC_SKILL_GROUP = 112,
HDR_GC_MAIN_CHARACTER = 113, // client non-GAIDEN: MAIN_CHARACTER2_EMPIRE
HDR_GC_SPECIAL_EFFECT = 114, // sic "SEPCIAL" in source
HDR_GC_NPC_POSITION = 115, // dynamic
HDR_GC_MATRIX_CARD = 116,
HDR_GC_CHARACTER_UPDATE2 = 117,
HDR_GC_LOGIN_KEY = 118,
HDR_GC_REFINE_INFORMATION = 119,
HDR_GC_CHARACTER_ADD2 = 120,
HDR_GC_CHANNEL = 121,
HDR_GC_MALL_OPEN = 122,
HDR_GC_TARGET_UPDATE = 123,
HDR_GC_TARGET_DELETE = 124,
HDR_GC_TARGET_CREATE = 125,
HDR_GC_AFFECT_ADD = 126,
HDR_GC_AFFECT_REMOVE = 127,
HDR_GC_MALL_SET = 128,
HDR_GC_MALL_DEL = 129,
HDR_GC_LAND_LIST = 130, // dynamic
HDR_GC_LOVER_INFO = 131,
HDR_GC_LOVE_POINT_UPDATE = 132,
HDR_GC_SYMBOL_DATA = 133,
HDR_GC_DIG_MOTION = 134,
HDR_GC_DAMAGE_INFO = 135,
HDR_GC_CHAR_ADDITIONAL_INFO = 136,
HDR_GC_MAIN_CHARACTER3_BGM = 137,
HDR_GC_MAIN_CHARACTER4_BGM_VOL = 138,
HDR_GC_AUTH_SUCCESS = 150,
HDR_GC_PANAMA_PACK = 151,
HDR_GC_HYBRIDCRYPT_KEYS = 152, // dynamic — EterPack key delivery, ignore
HDR_GC_HYBRIDCRYPT_SDB = 153, // dynamic
HDR_GC_AUTH_SUCCESS_OPENID = 154,
HDR_GC_RUNUP_MATRIX_QUIZ = 201,
HDR_GC_REQUEST_PASSPOD = 202,
HDR_GC_REQUEST_PASSPOD_FAILED = 203,
HDR_GC_HS_REQUEST = 204,
HDR_GC_XTRAP_CS1_REQUEST = 205,
HDR_GC_SPECIFIC_EFFECT = 208,
HDR_GC_DRAGON_SOUL_REFINE = 209,
HDR_GC_RESPOND_CHANNELSTATUS = 210,
};
// ------------------------------------------------------------------ structs
#pragma pack(push, 1)
struct ItemPos { // TItemPos (GameType.h, pack(1))
uint8_t window_type;
uint16_t cell;
};
static_assert(sizeof(ItemPos) == 3);
struct SimplePlayer { // TSimplePlayer (common/tables.h) == client TSimplePlayerInformation
uint32_t id;
char name[CHARACTER_NAME_MAX_LEN + 1];
uint8_t job; // BYTE (create packet's `job` is WORD; this summary field is BYTE)
uint8_t level;
uint32_t play_minutes;
uint8_t st, ht, dx, iq;
uint16_t main_part;
uint8_t change_name;
uint16_t hair_part;
uint8_t dummy[4];
int32_t x, y;
int32_t addr;
uint16_t port;
uint8_t skill_group;
};
// 4 + 25 + 1 + 1 + 4 + 4 + 2 + 1 + 2 + 4 + 8 + 4 + 2 + 1
static_assert(sizeof(SimplePlayer) == 63);
// --- control / handshake ---
struct Blank { uint8_t header; };
static_assert(sizeof(Blank) == 1);
struct DynHead { uint8_t header; uint16_t size; }; // TDynamicSizePacketHeader
static_assert(sizeof(DynHead) == 3);
struct Handshake { // packet_handshake (GC == CG)
uint8_t header;
uint32_t handshake;
uint32_t time;
int32_t delta;
};
static_assert(sizeof(Handshake) == 13);
struct Phase_ { uint8_t header; uint8_t phase; }; // packet_phase
static_assert(sizeof(Phase_) == 2);
struct BindUDP { uint8_t header; uint32_t addr; uint16_t port; };
static_assert(sizeof(BindUDP) == 7);
// TPacketCGText is the handshake/admin command base. The server's
// CInputHandshake::Analyze() consumes bytes after this one-byte header up to
// and including the first newline, so the size table describes only this
// fixed prefix.
struct CGText { uint8_t header; };
static_assert(sizeof(CGText) == 1);
struct KeyAgreement { // TPacketKeyAgreement — fixed 261B, registered STATIC
uint8_t header;
uint16_t agreed_length;
uint16_t data_length;
uint8_t data[KEY_AGREEMENT_MAX_DATA_LEN];
};
static_assert(sizeof(KeyAgreement) == 261);
struct KeyAgreementCompleted { uint8_t header; uint8_t data[3]; };
static_assert(sizeof(KeyAgreementCompleted) == 4);
// --- login ---
struct CGLogin { uint8_t header; char login[ID_MAX_NUM + 1]; char passwd[PASS_MAX_NUM + 1]; };
static_assert(sizeof(CGLogin) == 1 + 31 + 17);
struct CGLogin2 { uint8_t header; char login[ID_MAX_NUM + 1]; uint32_t login_key; uint32_t client_key[4]; };
static_assert(sizeof(CGLogin2) == 1 + 31 + 4 + 16);
struct CGLogin3 { uint8_t header; char login[ID_MAX_NUM + 1]; char passwd[PASS_MAX_NUM + 1]; uint32_t client_key[4]; };
static_assert(sizeof(CGLogin3) == 1 + 31 + 17 + 16);
struct GCLoginKey { uint8_t header; uint32_t login_key; };
static_assert(sizeof(GCLoginKey) == 5);
template <int N>
struct GCLoginSuccessT { // packet_login_success — 40250 sends the NEWSLOT (N=4) form
uint8_t header;
SimplePlayer players[N];
uint32_t guild_id[N];
char guild_name[N][GUILD_NAME_MAX_LEN + 1];
uint32_t handle;
uint32_t random_key;
};
using GCLoginSuccess = GCLoginSuccessT<PLAYER_PER_ACCOUNT>; // header 32
using GCLoginSuccess3 = GCLoginSuccessT<PLAYER_PER_ACCOUNT3>; // header 6 (legacy)
// 1 + 63*N + 4*N + 13*N + 4 + 4
static_assert(sizeof(GCLoginSuccess) == 1 + 63 * 4 + 4 * 4 + 13 * 4 + 4 + 4); // 329
static_assert(sizeof(GCLoginSuccess3) == 1 + 63 * 3 + 4 * 3 + 13 * 3 + 4 + 4); // 249
struct GCLoginFailure { uint8_t header; char status[LOGIN_STATUS_MAX_LEN + 1]; };
static_assert(sizeof(GCLoginFailure) == 10);
struct GCEmpire { uint8_t header; uint8_t empire; };
// Account/security packets are not part of the Godot login flow, but the
// stock 40250 header map treats them as static. Keep their exact sizes so a
// deployment which enables one can still drain the stream safely.
struct GCAuthSuccess { uint8_t header; uint32_t login_key; uint8_t result; };
struct GCAuthSuccessOpenID {
uint8_t header;
uint32_t login_key;
uint8_t result;
char login[ID_MAX_NUM + 1];
};
struct GCMatrixCard { uint8_t header; uint32_t rows; uint32_t cols; };
struct GCRunupMatrixQuiz { uint8_t header; char quiz[8 + 1]; };
struct GCRequestPasspod { uint8_t header; };
struct GCPasspodFailed { uint8_t header; char message[128 + 1]; };
struct GCXTrapRequest { uint8_t header; uint8_t packet_data[128]; };
// Optional Panama package IV delivery. It is not needed by the Godot asset
// loader, but it is sent by a stock server when panama.lst is configured.
struct GCPanamaPack { uint8_t header; char pack_name[256]; uint8_t iv[32]; };
// FreeBSD's game build wraps AHNHS_TRANS_BUFFER in #pragma pack(1):
// byte[400] + uint16 length, followed by the one-byte packet header.
struct GCHSRequest { uint8_t header; uint8_t buffer[400]; uint16_t length; };
static_assert(sizeof(GCAuthSuccess) == 6 && sizeof(GCAuthSuccessOpenID) == 37 &&
sizeof(GCMatrixCard) == 9 && sizeof(GCRunupMatrixQuiz) == 10 &&
sizeof(GCRequestPasspod) == 1 && sizeof(GCPasspodFailed) == 130 &&
sizeof(GCXTrapRequest) == 129 && sizeof(GCPanamaPack) == 289 &&
sizeof(GCHSRequest) == 403);
static_assert(sizeof(GCEmpire) == 2);
// --- select / create / delete ---
struct CGPlayerSelect { uint8_t header; uint8_t player_index; };
static_assert(sizeof(CGPlayerSelect) == 2);
struct CGPlayerCreate { // command_player_create — note `job` is WORD here
uint8_t header;
uint8_t index;
char name[CHARACTER_NAME_MAX_LEN + 1];
uint16_t job;
uint8_t shape;
uint8_t con, intel, str, dex;
};
static_assert(sizeof(CGPlayerCreate) == 1 + 1 + 25 + 2 + 1 + 4);
struct CGPlayerDelete { uint8_t header; uint8_t index; char private_code[PRIVATE_CODE_LENGTH]; };
static_assert(sizeof(CGPlayerDelete) == 10);
struct CGChangeName {
uint8_t header;
uint8_t index;
char name[CHARACTER_NAME_MAX_LEN + 1];
};
static_assert(sizeof(CGChangeName) == 27);
struct CGEmpire { uint8_t header; uint8_t empire; };
static_assert(sizeof(CGEmpire) == 2);
// CG_CLIENT_VERSION / CG_CLIENT_VERSION2 — TPacketCGClientVersion. The
// 40250 server records this before accepting game-phase movement; omitting it
// causes the server to close the connection on the first CG_MOVE.
struct CGClientVersion {
uint8_t header;
char filename[33];
char timestamp[33];
};
static_assert(sizeof(CGClientVersion) == 67);
// CG_CLIENT_VERSION2 (header 0xf1) — TPacketCGClientVersion2. Same layout as
// CGClientVersion; the EUROPE / non-YMIR locale sends THIS one with a fixed
// timestamp "1215955205" (REF/.../PythonNetworkStreamPhaseGame.cpp:4155,
// Locale.cpp:186). The 0xfd variant carries the C __TIMESTAMP__ instead.
struct CGClientVersion2 {
uint8_t header;
char filename[33];
char timestamp[33];
};
static_assert(sizeof(CGClientVersion2) == 67);
struct GCPlayerCreateSuccess { uint8_t header; uint8_t account_slot; SimplePlayer player; };
static_assert(sizeof(GCPlayerCreateSuccess) == 2 + 63);
struct GCPlayerCreateFailure { uint8_t header; uint8_t type; };
struct GCChangeNameNotice { uint8_t header; uint32_t pid; char name[CHARACTER_NAME_MAX_LEN + 1]; };
static_assert(sizeof(GCPlayerCreateFailure) == 2 && sizeof(GCChangeNameNotice) == 30);
struct GCPlayerDeleteSuccess { uint8_t header; uint8_t account_index; };
static_assert(sizeof(GCPlayerDeleteSuccess) == 2);
struct CGEnterGame { uint8_t header; };
static_assert(sizeof(CGEnterGame) == 1);
// --- independent guild-mark connection ---
// The mark server uses the same classic DH2/CTR control handshake, but its
// mark packets are not in the normal game length map. These fixed packet
// layouts are copied from ClientVS22/UserInterface/Packet.h.
struct CGMarkLogin { uint8_t header; uint32_t handle; uint32_t random_key; };
struct CGMarkIDXList { uint8_t header; };
struct CGMarkCRCList { uint8_t header; uint8_t img_idx; uint32_t crclist[MARK_BLOCK_TOTAL_COUNT]; };
struct CGMarkUpload {
uint8_t header;
uint32_t gid;
uint8_t image[GUILD_MARK_WIDTH * GUILD_MARK_HEIGHT * 4];
};
struct CGGuildSymbolUpload { uint8_t header; uint16_t size; uint32_t guild_id; };
struct CGSymbolCRC { uint8_t header; uint32_t guild_id; uint32_t crc; uint32_t size; };
struct GCMarkIDXList { uint8_t header; uint32_t buf_size; uint16_t count; };
struct GCMarkBlock { uint8_t header; uint32_t buf_size; uint8_t img_idx; uint32_t count; };
struct GCSymbolData { uint8_t header; uint16_t size; uint32_t guild_id; };
static_assert(sizeof(CGMarkLogin) == 9 && sizeof(CGMarkIDXList) == 1 &&
sizeof(CGMarkCRCList) == 322 && sizeof(CGMarkUpload) == 773 &&
sizeof(CGGuildSymbolUpload) == 7 && sizeof(CGSymbolCRC) == 13 &&
sizeof(GCMarkIDXList) == 7 && sizeof(GCMarkBlock) == 10 &&
sizeof(GCSymbolData) == 7);
// --- main character (header 113) ---
struct GCMainCharacter { // packet.h:1043 (== client TPacketGCMainCharacter2_EMPIRE)
uint8_t header;
uint32_t vid;
uint16_t race;
char name[CHARACTER_NAME_MAX_LEN + 1];
int32_t x, y, z;
uint8_t empire;
uint8_t skill_group;
};
static_assert(sizeof(GCMainCharacter) == 1 + 4 + 2 + 25 + 12 + 1 + 1);
// SUPPORT_BGM variants emitted by CHARACTER::MainCharacterPacket when the
// map has a configured background track. The BGM field is not needed by the
// Godot world model, but it must be part of the frame so the next GC packet is
// not mistaken for the tail of this one.
struct GCMainCharacter3BGM {
uint8_t header;
uint32_t vid;
uint16_t race;
char name[CHARACTER_NAME_MAX_LEN + 1];
char bgm_name[24 + 1];
int32_t x, y, z;
uint8_t empire;
uint8_t skill_group;
};
struct GCMainCharacter4BGM {
uint8_t header;
uint32_t vid;
uint16_t race;
char name[CHARACTER_NAME_MAX_LEN + 1];
char bgm_name[24 + 1];
float bgm_volume;
int32_t x, y, z;
uint8_t empire;
uint8_t skill_group;
};
static_assert(sizeof(GCMainCharacter3BGM) == 71 && sizeof(GCMainCharacter4BGM) == 75);
// --- spawn / despawn / move ---
struct GCCharacterAdd { // packet_add_char (server) — no name/parts
uint8_t header;
uint32_t vid;
float angle;
int32_t x, y, z;
uint8_t type;
uint16_t race;
uint8_t moving_speed;
uint8_t attack_speed;
uint8_t state_flag;
uint32_t affect_flag[2];
};
static_assert(sizeof(GCCharacterAdd) == 1 + 4 + 4 + 12 + 1 + 2 + 1 + 1 + 1 + 8);
struct GCCharacterAdd2 { // packet_add_char2 — name + parts + guild
uint8_t header;
uint32_t vid;
char name[CHARACTER_NAME_MAX_LEN + 1];
float angle;
int32_t x, y, z;
uint8_t type;
uint16_t race;
uint16_t parts[CHR_EQUIPPART_NUM];
uint8_t moving_speed;
uint8_t attack_speed;
uint8_t state_flag;
uint32_t affect_flag[2];
uint8_t empire;
uint32_t guild;
int16_t alignment;
uint8_t pk_mode;
uint32_t mount_vnum;
};
static_assert(sizeof(GCCharacterAdd2) ==
1 + 4 + 25 + 4 + 12 + 1 + 2 + 8 + 1 + 1 + 1 + 8 + 1 + 4 + 2 + 1 + 4);
struct GCCharacterDel { uint8_t header; uint32_t vid; };
static_assert(sizeof(GCCharacterDel) == 5);
struct CGMove { // command_move
uint8_t header;
uint8_t func;
uint8_t arg;
uint8_t rot;
int32_t x, y;
uint32_t time;
};
static_assert(sizeof(CGMove) == 4 + 8 + 4);
struct GCMove { // packet_move (server) — adds vid + duration
uint8_t header;
uint8_t func;
uint8_t arg;
uint8_t rot;
uint32_t vid;
int32_t x, y;
uint32_t time;
uint32_t duration;
};
static_assert(sizeof(GCMove) == 4 + 4 + 8 + 4 + 4);
// --- points ---
struct GCPoints { uint8_t header; int32_t points[POINT_MAX_NUM]; };
static_assert(sizeof(GCPoints) == 1 + 255 * 4);
struct GCPointChange { // packet_point_change — NOTE: `int header` (4 bytes) in the source
int32_t header; // wire: byte 0 = real header, bytes 1..3 = junk from the int
uint32_t vid;
uint8_t type;
int32_t amount;
int32_t value;
};
static_assert(sizeof(GCPointChange) == 4 + 4 + 1 + 4 + 4);
// --- CG in-game intents ---
struct CGAttack { // command_attack
uint8_t header;
uint8_t type;
uint32_t victim_vid;
uint8_t crc_proc;
uint8_t crc_file;
};
static_assert(sizeof(CGAttack) == 8);
struct CGTarget { uint8_t header; uint32_t vid; }; // command_target
static_assert(sizeof(CGTarget) == 5);
struct CGOnClick { uint8_t header; uint32_t vid; }; // command_on_click
static_assert(sizeof(CGOnClick) == 5);
struct CGPosition { uint8_t header; uint8_t position; }; // command_position
static_assert(sizeof(CGPosition) == 2);
struct CGScriptAnswer { uint8_t header; uint8_t answer; };
static_assert(sizeof(CGScriptAnswer) == 2);
struct CGItemUse { uint8_t header; ItemPos pos; }; // command_item_use
static_assert(sizeof(CGItemUse) == 4);
struct CGItemMove { uint8_t header; ItemPos pos; ItemPos change_pos; uint8_t num; };
static_assert(sizeof(CGItemMove) == 8);
struct CGItemDrop { uint8_t header; ItemPos pos; uint32_t gold; }; // command_item_drop
static_assert(sizeof(CGItemDrop) == 8);
struct CGItemDrop2 { uint8_t header; ItemPos pos; uint32_t gold; uint8_t count; };
static_assert(sizeof(CGItemDrop2) == 9);
struct CGItemPickup { uint8_t header; uint32_t vid; }; // command_item_pickup
static_assert(sizeof(CGItemPickup) == 5);
struct CGUseSkill { uint8_t header; uint32_t vnum; uint32_t target_vid; };
static_assert(sizeof(CGUseSkill) == 9);
struct CGFlyTargeting { uint8_t header; uint32_t target_vid; int32_t x, y; };
static_assert(sizeof(CGFlyTargeting) == 13);
struct CGShoot { uint8_t header; uint8_t type; };
static_assert(sizeof(CGShoot) == 2);
struct CGWarp { uint8_t header; };
static_assert(sizeof(CGWarp) == 1);
struct CGFishing { uint8_t header; uint8_t dir; };
static_assert(sizeof(CGFishing) == 2);
struct CGItemUseToItem { uint8_t header; ItemPos pos; ItemPos target_pos; };
static_assert(sizeof(CGItemUseToItem) == 7);
struct CGGiveItem { uint8_t header; uint32_t target_vid; ItemPos pos; uint8_t count; };
static_assert(sizeof(CGGiveItem) == 9);
struct CGQuickSlotAdd { uint8_t header; uint8_t pos; uint8_t type; uint8_t position; };
struct CGQuickSlotDel { uint8_t header; uint8_t pos; };
struct CGQuickSlotSwap { uint8_t header; uint8_t pos; uint8_t pos_to; };
static_assert(sizeof(CGQuickSlotAdd) == 4 && sizeof(CGQuickSlotDel) == 2 &&
sizeof(CGQuickSlotSwap) == 3);
// --- P8 social / exchange / storage intents -------------------------------
enum : uint8_t {
EXCHANGE_CG_START = 0,
EXCHANGE_CG_ITEM_ADD = 1,
EXCHANGE_CG_ITEM_DEL = 2,
EXCHANGE_CG_GOLD_ADD = 3,
EXCHANGE_CG_ACCEPT = 4,
EXCHANGE_CG_CANCEL = 5,
MESSENGER_CG_ADD_BY_VID = 0,
MESSENGER_CG_ADD_BY_NAME = 1,
MESSENGER_CG_REMOVE = 2,
};
struct CGExchange {
uint8_t header;
uint8_t subheader;
uint32_t arg1;
uint8_t arg2;
ItemPos pos;
};
static_assert(sizeof(CGExchange) == 10);
struct CGMessenger { uint8_t header; uint8_t subheader; };
static_assert(sizeof(CGMessenger) == 2);
struct CGSafeboxCheckout {
uint8_t header;
uint8_t safe_pos;
ItemPos item_pos;
};
struct CGSafeboxCheckin {
uint8_t header;
uint8_t safe_pos;
ItemPos item_pos;
};
struct CGMallCheckout {
uint8_t header;
uint8_t mall_pos;
ItemPos item_pos;
};
static_assert(sizeof(CGSafeboxCheckout) == 5 && sizeof(CGSafeboxCheckin) == 5 &&
sizeof(CGMallCheckout) == 5);
enum : uint8_t {
SHOP_CG_END = 0,
SHOP_CG_BUY = 1,
SHOP_CG_SELL = 2,
SHOP_CG_SELL2 = 3,
};
struct CGShop { uint8_t header; uint8_t subheader; };
struct CGShopBuy { uint8_t count; uint8_t pos; };
struct CGShopSell { uint8_t pos; };
struct CGShopSell2 { uint8_t pos; uint8_t count; };
static_assert(sizeof(CGShop) == 2 && sizeof(CGShopBuy) == 2 && sizeof(CGShopSell) == 1 &&
sizeof(CGShopSell2) == 2);
struct CGScriptButton { uint8_t header; uint32_t index; };
struct CGQuestInput { uint8_t header; char msg[64 + 1]; };
struct CGQuestConfirm { uint8_t header; uint8_t answer; uint32_t request_pid; };
struct CGScriptSelectItem { uint8_t header; uint32_t selection; };
static_assert(sizeof(CGScriptButton) == 5 && sizeof(CGQuestInput) == 66 &&
sizeof(CGQuestConfirm) == 6 && sizeof(CGScriptSelectItem) == 5);
struct CGPartyInvite { uint8_t header; uint32_t vid; };
struct CGPartyInviteAnswer { uint8_t header; uint32_t leader_vid; uint8_t accept; };
struct CGPartyRemove { uint8_t header; uint32_t pid; };
struct CGPartySetState { uint8_t header; uint32_t pid; uint8_t role; uint8_t flag; };
struct CGPartyUseSkill { uint8_t header; uint8_t skill_index; uint32_t vid; };
struct CGPartyParameter { uint8_t header; uint8_t distribute_mode; };
static_assert(sizeof(CGPartyInvite) == 5 && sizeof(CGPartyInviteAnswer) == 6 &&
sizeof(CGPartyRemove) == 5 && sizeof(CGPartySetState) == 7 &&
sizeof(CGPartyUseSkill) == 6 && sizeof(CGPartyParameter) == 2);
enum : uint8_t {
GUILD_CG_ADD_MEMBER = 0,
GUILD_CG_REMOVE_MEMBER = 1,
GUILD_CG_CHANGE_GRADE_NAME = 2,
GUILD_CG_CHANGE_GRADE_AUTHORITY = 3,
GUILD_CG_OFFER = 4,
GUILD_CG_POST_COMMENT = 5,
GUILD_CG_DELETE_COMMENT = 6,
GUILD_CG_REFRESH_COMMENT = 7,
GUILD_CG_CHANGE_MEMBER_GRADE = 8,
GUILD_CG_USE_SKILL = 9,
GUILD_CG_CHANGE_MEMBER_GENERAL = 10,
GUILD_CG_GUILD_INVITE_ANSWER = 11,
GUILD_CG_CHARGE_GSP = 12,
GUILD_CG_DEPOSIT_MONEY = 13,
GUILD_CG_WITHDRAW_MONEY = 14,
};
struct CGGuild { uint8_t header; uint8_t subheader; };
struct CGAnswerMakeGuild { uint8_t header; char guild_name[GUILD_NAME_MAX_LEN + 1]; };
struct CGGuildUseSkill { uint32_t vnum; uint32_t target_pid; };
static_assert(sizeof(CGGuild) == 2 && sizeof(CGAnswerMakeGuild) == 14 &&
sizeof(CGGuildUseSkill) == 8);
struct CGMyShopHead {
uint8_t header;
char sign[SHOP_TAB_NAME_MAX + 1];
uint8_t count;
};
// TShopItemTable is defined by the common table header. The fields used by
// the classic server are kept here in wire order; socket/attribute payloads
// are copied by ClassicSession without depending on the fork's wire.h types.
struct CGMyShopItem {
uint32_t vnum;
uint8_t count;
ItemPos pos;
uint32_t price;
uint8_t display_pos;
};
static_assert(sizeof(CGMyShopItem) == 13);
struct CGRefine { uint8_t header; uint8_t pos; uint8_t type; };
static_assert(sizeof(CGRefine) == 3);
inline constexpr int DS_REFINE_GRID_MAX_NUM = 15;
struct CGDragonSoulRefine {
uint8_t header;
uint8_t subheader;
ItemPos grid[DS_REFINE_GRID_MAX_NUM];
};
static_assert(sizeof(CGDragonSoulRefine) == 47);
// --- GC entity state ---
struct GCCharacterUpdate { // packet_update_char (header 19)
uint8_t header;
uint32_t vid;
uint16_t parts[CHR_EQUIPPART_NUM];
uint8_t moving_speed;
uint8_t attack_speed;
uint8_t state_flag;
uint32_t affect_flag[2];
uint32_t guild_id;
int16_t alignment;
uint8_t pk_mode;
uint32_t mount_vnum;
};
static_assert(sizeof(GCCharacterUpdate) == 1 + 4 + 8 + 1 + 1 + 1 + 8 + 4 + 2 + 1 + 4); // 35
struct GCStun { uint8_t header; uint32_t vid; }; // header 13
struct GCDead { uint8_t header; uint32_t vid; }; // header 14
static_assert(sizeof(GCStun) == 5 && sizeof(GCDead) == 5);
struct GCMotion { uint8_t header; uint32_t vid; uint32_t victim_vid; uint16_t motion; }; // 36
static_assert(sizeof(GCMotion) == 11);
struct GCCharacterPosition { uint8_t header; uint32_t vid; uint8_t position; }; // header 43
static_assert(sizeof(GCCharacterPosition) == 6);
struct GCChangeSpeed { uint8_t header; uint32_t vid; uint16_t moving_speed; }; // header 18
static_assert(sizeof(GCChangeSpeed) == 7);
struct GCWalkMode { uint8_t header; uint32_t vid; uint8_t mode; }; // header 111
static_assert(sizeof(GCWalkMode) == 6);
struct GCTarget { uint8_t header; uint32_t vid; uint8_t hp_percent; }; // header 63
static_assert(sizeof(GCTarget) == 6);
struct GCDamageInfo { uint8_t header; uint32_t vid; uint8_t flag; int32_t damage; }; // header 135
static_assert(sizeof(GCDamageInfo) == 10);
struct ItemAttr3 { uint8_t type; int16_t value; }; // TPlayerItemAttribute (pack(1) = 3B)
static_assert(sizeof(ItemAttr3) == 3);
struct GCAttack { uint8_t header; uint32_t vid; uint32_t victim_vid; uint8_t type; };
struct GCItemUse { uint8_t header; ItemPos cell; uint32_t ch_vid; uint32_t victim_vid; uint32_t vnum; };
struct GCItemOwnership { uint8_t header; uint32_t vid; char owner[CHARACTER_NAME_MAX_LEN + 1]; };
struct GCOwnership { uint8_t header; uint32_t owner_vid; uint32_t victim_vid; };
struct GCMount {
uint8_t header;
uint32_t vid;
uint32_t mount_vnum;
uint8_t pos;
int32_t x, y;
};
struct GCCreateFly { uint8_t header; uint8_t type; uint32_t start_vid; uint32_t end_vid; };
struct GCFlyTargeting {
uint8_t header;
uint32_t shooter_vid;
uint32_t target_vid;
int32_t x, y;
};
struct GCPvp { uint8_t header; uint32_t src_vid; uint32_t dst_vid; uint8_t mode; };
struct GCDuelStart { uint8_t header; uint16_t size; };
struct GCShopSign { uint8_t header; uint32_t vid; char sign[SHOP_SIGN_MAX_LEN + 1]; };
struct GCSkillGroup { uint8_t header; uint8_t skill_group; };
struct GCSpecialEffect { uint8_t header; uint8_t type; uint32_t vid; };
struct GCSpecificEffect { uint8_t header; uint32_t vid; char effect_file[128]; };
struct GCSkillLevelOld { uint8_t header; uint8_t levels[SKILL_MAX_NUM]; };
struct GCSkillCooltimeEnd { uint8_t header; uint8_t skill; };
struct GCFishing { uint8_t header; uint8_t subheader; uint32_t info; uint8_t dir; };
struct GCObserverAdd { uint8_t header; uint32_t vid; uint16_t x, y; };
struct GCObserverMove { uint8_t header; uint32_t vid; uint16_t x, y; };
struct GCObserverRemove { uint8_t header; uint32_t vid; };
// TPacketGCWarp in the 40250 server/client packet.h is a packed static packet
// with no dynamic length field (BYTE + LONG x 3 + WORD).
struct GCWarpClassic { uint8_t header; int32_t x, y, addr; uint16_t port; };
static_assert(sizeof(GCWarpClassic) == 15);
struct GCTime { uint8_t header; int32_t time; };
struct GCChannel { uint8_t header; uint8_t channel; };
struct GCDungeonHead { uint8_t subheader; };
struct GCDungeonDestination { int32_t x, y; };
struct GCNPCPositionHead { uint16_t count; };
struct NPCPosition34 {
uint8_t type;
char name[CHARACTER_NAME_MAX_LEN + 1];
int32_t x, y;
};
struct LandPacketElement24 {
uint32_t id;
int32_t x, y;
int32_t width, height;
uint32_t guild_id;
};
struct GCTargetCreate43 {
uint8_t header;
int32_t id;
char name[32 + 1];
uint32_t vid;
uint8_t type;
};
struct GCTargetUpdate13 { uint8_t header; int32_t id; int32_t x, y; };
struct GCTargetDelete5 { uint8_t header; int32_t id; };
struct GCLoverInfo { uint8_t header; char name[CHARACTER_NAME_MAX_LEN + 1]; uint8_t love_point; };
struct GCLovePointUpdate { uint8_t header; uint8_t love_point; };
struct GCDigMotion { uint8_t header; uint32_t vid; uint32_t target_vid; uint8_t count; };
struct EquipmentItem38 {
uint32_t vnum;
uint8_t count;
int32_t sockets[ITEM_SOCKET_MAX_NUM];
ItemAttr3 attrs[ITEM_ATTRIBUTE_MAX_NUM];
};
inline constexpr int CLASSIC_WEAR_MAX_NUM = 32; // server common/length.h
struct GCViewEquip {
uint8_t header;
uint32_t vid;
EquipmentItem38 equips[CLASSIC_WEAR_MAX_NUM];
};
struct GCChangeName { uint8_t header; uint32_t pid; char name[CHARACTER_NAME_MAX_LEN + 1]; };
struct GCRefineMaterial { uint32_t vnum; int32_t count; };
struct GCRefineInfo {
uint8_t header;
uint8_t type;
uint8_t pos;
uint32_t src_vnum;
uint32_t result_vnum;
uint8_t material_count;
int32_t cost;
int32_t prob;
GCRefineMaterial materials[5];
};
struct GCDragonSoulRefine {
uint8_t header;
uint8_t sub_type;
ItemPos pos;
};
static_assert(sizeof(GCAttack) == 10 && sizeof(GCItemUse) == 16 &&
sizeof(GCItemOwnership) == 30 && sizeof(GCOwnership) == 9 && sizeof(GCMount) == 18 &&
sizeof(GCCreateFly) == 10 && sizeof(GCFlyTargeting) == 17 &&
sizeof(GCPvp) == 10 && sizeof(GCDuelStart) == 3 && sizeof(GCShopSign) == 38 &&
sizeof(GCSkillGroup) == 2 && sizeof(GCSpecialEffect) == 6 && sizeof(GCSpecificEffect) == 133 &&
sizeof(GCSkillLevelOld) == 256 && sizeof(GCSkillCooltimeEnd) == 2 &&
sizeof(GCFishing) == 7 &&
sizeof(GCObserverAdd) == 9 && sizeof(GCObserverMove) == 9 &&
sizeof(GCObserverRemove) == 5 && sizeof(GCTime) == 5 && sizeof(GCChannel) == 2 &&
sizeof(GCDungeonHead) == 1 && sizeof(GCNPCPositionHead) == 2 &&
sizeof(NPCPosition34) == 34 && sizeof(LandPacketElement24) == 24 &&
sizeof(GCTargetCreate43) == 43 && sizeof(GCTargetUpdate13) == 13 &&
sizeof(GCTargetDelete5) == 5 && sizeof(GCLoverInfo) == 27 &&
sizeof(GCLovePointUpdate) == 2 && sizeof(GCDigMotion) == 10 &&
sizeof(EquipmentItem38) == 38 && sizeof(GCViewEquip) == 1221 &&
sizeof(GCChangeName) == 30 && sizeof(GCRefineMaterial) == 8 &&
sizeof(GCRefineInfo) == 60 && sizeof(GCDragonSoulRefine) == 5);
// packet_char_additional_info (header 136) — fills a spawned entity's name/parts/...
struct GCCharAddInfo {
uint8_t header;
uint32_t vid;
char name[CHARACTER_NAME_MAX_LEN + 1];
uint16_t parts[CHR_EQUIPPART_NUM];
uint8_t empire;
uint32_t guild_id;
uint32_t level;
int16_t alignment;
uint8_t pk_mode;
uint32_t mount_vnum;
};
static_assert(sizeof(GCCharAddInfo) == 1 + 4 + 25 + 8 + 1 + 4 + 4 + 2 + 1 + 4); // 54
// --- GC skills / quickslots ---
struct PlayerSkill3 { // TPlayerSkill (common/tables.h) — time_t is 32-bit on the -m32 server
uint8_t master_type;
uint8_t level;
uint32_t next_read;
};
static_assert(sizeof(PlayerSkill3) == 6);
struct GCSkillLevel { uint8_t header; PlayerSkill3 skills[SKILL_MAX_NUM]; }; // header 76
static_assert(sizeof(GCSkillLevel) == 1 + 255 * 6); // 1531
struct QuickSlot2 { uint8_t type; uint8_t position; }; // TQuickSlot / TQuickslot
struct GCQuickSlotAdd { uint8_t header; uint8_t pos; QuickSlot2 slot; }; // header 28
struct GCQuickSlotDel { uint8_t header; uint8_t pos; }; // header 29
struct GCQuickSlotSwap { uint8_t header; uint8_t pos; uint8_t pos_to; }; // header 30
static_assert(sizeof(GCQuickSlotAdd) == 4 && sizeof(GCQuickSlotDel) == 2 &&
sizeof(GCQuickSlotSwap) == 3);
// --- GC affects ---
struct AffectElement { // TPacketAffectElement (client Packet.h L2555)
uint32_t type;
uint8_t point_idx;
int32_t apply_value;
uint32_t flag;
int32_t duration;
int32_t sp_cost;
};
static_assert(sizeof(AffectElement) == 21);
struct GCAffectAdd { uint8_t header; AffectElement elem; }; // header 126
struct GCAffectRemove { uint8_t header; uint32_t type; uint8_t apply_on; }; // header 127
static_assert(sizeof(GCAffectAdd) == 22 && sizeof(GCAffectRemove) == 6);
// --- GC dynamic: whisper / sync-position ---
struct GCWhisperHead { // packet_whisper (header 34) — + text
uint8_t header;
uint16_t size;
uint8_t type;
char name_from[CHARACTER_NAME_MAX_LEN + 1];
};
static_assert(sizeof(GCWhisperHead) == 1 + 2 + 1 + 25); // 29
struct SyncPosElement { uint32_t vid; int32_t x, y; };
static_assert(sizeof(SyncPosElement) == 12);
// --- GC quest / script ---
struct GCScriptHead { uint8_t header; uint16_t size; uint8_t skin; uint16_t src_size; }; // 45 dyn
static_assert(sizeof(GCScriptHead) == 6);
struct GCQuestConfirm { // packet_quest_confirm (header 46, STATIC)
uint8_t header;
char msg[64 + 1];
int32_t timeout;
uint32_t request_pid;
};
static_assert(sizeof(GCQuestConfirm) == 1 + 65 + 4 + 4); // 74
// GC_QUEST_INFO (81, dynamic): after [header][u16 size] -> [u16 index][u8 flag][flag-driven...]
enum : uint8_t {
QUEST_SEND_IS_BEGIN = 1 << 0,
QUEST_SEND_TITLE = 1 << 1,
QUEST_SEND_CLOCK_NAME = 1 << 2,
QUEST_SEND_CLOCK_VALUE = 1 << 3,
QUEST_SEND_COUNTER_NAME = 1 << 4,
QUEST_SEND_COUNTER_VALUE = 1 << 5,
QUEST_SEND_ICON_FILE = 1 << 6,
};
// Field widths written by PC::SendQuestInfoPakcet() (questpc.cpp) and read back
// by the original client's field-by-field Recv() in RecvQuestInfoPacket().
inline constexpr int QUEST_INFO_HEAD_SIZE = 6; // header + u16 size + u16 index + u8 flag
inline constexpr int QUEST_INFO_IS_BEGIN_SIZE = 1;
inline constexpr int QUEST_INFO_TITLE_SIZE = 30 + 1;
inline constexpr int QUEST_INFO_CLOCK_NAME_SIZE = 16 + 1;
inline constexpr int QUEST_INFO_CLOCK_VALUE_SIZE = 4;
inline constexpr int QUEST_INFO_COUNTER_NAME_SIZE = 16 + 1;
inline constexpr int QUEST_INFO_COUNTER_VALUE_SIZE = 4;
inline constexpr int QUEST_INFO_ICON_FILE_SIZE = 24 + 1;
// The 40250 server fills the fixed head with `size = sizeof(packet_quest_info)`
// (6) and then appends the optional fields while only bumping its own local
// copy of `size` — the count on the wire never covers the tail. Framing this
// packet off its size field therefore leaves the tail in the stream and
// desyncs every packet after it, so derive the real length from `flag` the way
// the original client does.
constexpr int quest_info_packet_size(uint8_t flag) {
int n = QUEST_INFO_HEAD_SIZE;
if (flag & QUEST_SEND_IS_BEGIN) n += QUEST_INFO_IS_BEGIN_SIZE;
if (flag & QUEST_SEND_TITLE) n += QUEST_INFO_TITLE_SIZE;
if (flag & QUEST_SEND_CLOCK_NAME) n += QUEST_INFO_CLOCK_NAME_SIZE;
if (flag & QUEST_SEND_CLOCK_VALUE) n += QUEST_INFO_CLOCK_VALUE_SIZE;
if (flag & QUEST_SEND_COUNTER_NAME) n += QUEST_INFO_COUNTER_NAME_SIZE;
if (flag & QUEST_SEND_COUNTER_VALUE) n += QUEST_INFO_COUNTER_VALUE_SIZE;
if (flag & QUEST_SEND_ICON_FILE) n += QUEST_INFO_ICON_FILE_SIZE;
return n;
}
// --- GC party ---
inline constexpr int PARTY_AFFECT_SLOT_MAX_NUM = 7;
struct GCPartyInvite { uint8_t header; uint32_t leader_vid; }; // header 77
struct GCPartyAdd { uint8_t header; uint32_t pid; char name[CHARACTER_NAME_MAX_LEN + 1]; }; // 78
struct GCPartyUpdate { // header 79
uint8_t header;
uint32_t pid;
uint8_t role;
uint8_t percent_hp;
int16_t affects[PARTY_AFFECT_SLOT_MAX_NUM];
};
struct GCPartyRemove { uint8_t header; uint32_t pid; }; // header 80
struct GCPartyLink { uint8_t header; uint32_t pid; uint32_t vid; }; // header 91
struct GCPartyUnlink { uint8_t header; uint32_t pid; uint32_t vid; }; // header 92
struct GCPartyParameter { uint8_t header; uint8_t distribute_mode; }; // header 83
static_assert(sizeof(GCPartyInvite) == 5 && sizeof(GCPartyAdd) == 30 &&
sizeof(GCPartyUpdate) == 21 && sizeof(GCPartyRemove) == 5 && sizeof(GCPartyLink) == 9 &&
sizeof(GCPartyParameter) == 2);
// --- GC items ---
enum : uint8_t {
EXCHANGE_GC_START = 0,
EXCHANGE_GC_ITEM_ADD = 1,
EXCHANGE_GC_ITEM_DEL = 2,
EXCHANGE_GC_GOLD_ADD = 3,
EXCHANGE_GC_ACCEPT = 4,
EXCHANGE_GC_END = 5,
EXCHANGE_GC_ALREADY = 6,
EXCHANGE_GC_LESS_GOLD = 7,
MESSENGER_GC_LIST = 0,
MESSENGER_GC_LOGIN = 1,
MESSENGER_GC_LOGOUT = 2,
MESSENGER_GC_INVITE = 3,
MESSENGER_GC_MOBILE = 4,
};
struct GCExchange {
uint8_t header;
uint8_t subheader;
uint8_t is_me;
uint32_t arg1;
ItemPos arg2;
uint32_t arg3;
int32_t sockets[ITEM_SOCKET_MAX_NUM];
ItemAttr3 attrs[ITEM_ATTRIBUTE_MAX_NUM];
};
static_assert(sizeof(GCExchange) == 47);
struct GCItemSet { // packet_item_set (server packet.h:1134, header 21)
uint8_t header;
ItemPos cell;
uint32_t vnum;
uint8_t count;
uint32_t flags;
uint32_t anti_flags;
uint8_t highlight; // bool on the wire
int32_t sockets[ITEM_SOCKET_MAX_NUM];
ItemAttr3 attrs[ITEM_ATTRIBUTE_MAX_NUM];
};
static_assert(sizeof(GCItemSet) == 1 + 3 + 4 + 1 + 4 + 4 + 1 + 12 + 21); // 51
struct GCItemDel { uint8_t header; uint8_t pos; }; // header 20 — inventory pos only
static_assert(sizeof(GCItemDel) == 2);
// GC_MESSENGER is dynamic: after [header][u16 size], body starts with the
// subheader and then the subheader-specific records below.
struct GCMessengerHead { uint8_t header; uint16_t size; uint8_t subheader; };
struct MessengerListEntry { uint8_t connected; uint8_t length; };
struct MessengerNameLength { uint8_t length; };
static_assert(sizeof(GCMessengerHead) == 4 && sizeof(MessengerListEntry) == 2 &&
sizeof(MessengerNameLength) == 1);
struct GCSafeboxSize { uint8_t header; uint8_t size; };
struct GCSafeboxMoneyChange { uint8_t header; int32_t money; };
struct GCMallOpen { uint8_t header; uint8_t size; };
static_assert(sizeof(GCSafeboxSize) == 2 && sizeof(GCSafeboxMoneyChange) == 5 &&
sizeof(GCMallOpen) == 2);
struct GCItemUpdate { // packet_item_update (header 25)
uint8_t header;
ItemPos cell;
uint8_t count;
int32_t sockets[ITEM_SOCKET_MAX_NUM];
ItemAttr3 attrs[ITEM_ATTRIBUTE_MAX_NUM];
};
static_assert(sizeof(GCItemUpdate) == 1 + 3 + 1 + 12 + 21); // 38
struct GCItemGroundAdd { // packet_item_ground_add (header 26)
uint8_t header;
int32_t x, y, z;
uint32_t vid;
uint32_t vnum;
};
static_assert(sizeof(GCItemGroundAdd) == 1 + 12 + 4 + 4); // 21
struct GCItemGroundDel { uint8_t header; uint32_t vid; }; // header 27
static_assert(sizeof(GCItemGroundDel) == 5);
// --- GC shop (dynamic, sub-header after [header][u16 size]) ---
struct ShopItem43 { // packet_shop_item
uint32_t vnum;
uint32_t price;
uint8_t count;
uint8_t display_pos;
int32_t sockets[ITEM_SOCKET_MAX_NUM];
ItemAttr3 attrs[ITEM_ATTRIBUTE_MAX_NUM];
};
static_assert(sizeof(ShopItem43) == 4 + 4 + 1 + 1 + 12 + 21); // 43
struct ShopTabHead32 { char name[SHOP_TAB_NAME_MAX]; uint8_t coin_type; };
static_assert(sizeof(ShopTabHead32) == 33);
struct ShopUpdateItem { uint8_t pos; ShopItem43 item; };
struct ShopUpdatePrice { int32_t price; };
static_assert(sizeof(ShopUpdateItem) == 44 && sizeof(ShopUpdatePrice) == 4);
// --- GC guild (dynamic, [header][u16 size][subheader]...) ---
struct GuildMember38 {
uint32_t pid;
uint8_t grade;
uint8_t is_general;
uint8_t job;
uint8_t level;
uint32_t offer;
uint8_t name_flag;
char name[CHARACTER_NAME_MAX_LEN + 1];
};
struct GuildInfo35 {
uint16_t member_count;
uint16_t max_member_count;
uint32_t guild_id;
uint32_t master_pid;
uint32_t exp;
uint8_t level;
char name[GUILD_NAME_MAX_LEN + 1];
uint32_t gold;
uint8_t has_land;
};
struct GuildGrade10 { char name[GUILD_GRADE_NAME_MAX_LEN + 1]; uint8_t auth; };
struct GuildWar10 { uint32_t self_id; uint32_t opponent_id; uint8_t type; uint8_t state; };
struct GuildWarPoint12 { uint32_t gain_guild_id; uint32_t opponent_guild_id; int32_t point; };
struct GuildWarPair8 { uint32_t src; uint32_t dst; };
struct GuildName16 { uint32_t id; char name[GUILD_NAME_MAX_LEN]; };
struct GuildSkill17 {
uint8_t skill_point;
uint8_t levels[GUILD_SKILL_MAX_NUM];
uint16_t guild_point;
uint16_t max_guild_point;
};
struct GuildComment80 {
uint32_t id;
char name[CHARACTER_NAME_MAX_LEN + 1];
char content[GUILD_COMMENT_MAX_LEN + 1];
};
struct GuildInvite17 {
uint32_t guild_id;
char guild_name[GUILD_NAME_MAX_LEN + 1];
};
static_assert(sizeof(GuildMember38) == 38 && sizeof(GuildInfo35) == 35 &&
sizeof(GuildGrade10) == 10 && sizeof(GuildWar10) == 10 &&
sizeof(GuildWarPoint12) == 12 && sizeof(GuildWarPair8) == 8 &&
sizeof(GuildName16) == 16 && sizeof(GuildSkill17) == 17 &&
sizeof(GuildComment80) == 80 && sizeof(GuildInvite17) == 17);
// --- chat (dynamic) ---
struct CGChatHead { uint8_t header; uint16_t length; uint8_t type; }; // + char szChat[]
static_assert(sizeof(CGChatHead) == 4);
struct CGWhisper { uint8_t header; uint16_t size; char name_to[CHARACTER_NAME_MAX_LEN + 1]; };
static_assert(sizeof(CGWhisper) == 28);
struct GCChatHead { // packet_chatting — + text
uint8_t header;
uint16_t size;
uint8_t type;
uint32_t vid;
uint8_t empire;
};
static_assert(sizeof(GCChatHead) == 9);
#pragma pack(pop)
// ------------------------------------------------------------------ size table
// FULL on-wire size of a static-size packet — the 1-byte header INCLUDED, any
// trailing sequence byte EXCLUDED (add 1 when is_sequence_cg()). Matches
// packet_info.cpp's `CPacketInfo::Set(header, sizeof(TPacketXxx), ...)` (CG) and
// CMainPacketHeaderMap's `sizeof(...)` (GC). Returns 0 for "unknown / dynamic" —
// callers must check is_dynamic_*() first. Grows as classic_parser lands packets.
constexpr int packet_size_cg(uint8_t h) {
switch (h) {
case HDR_HANDSHAKE: return sizeof(Handshake);
case HDR_CG_TIME_SYNC: return sizeof(Handshake);
case HDR_CG_PONG: return 1;
case HDR_CG_TEXT: return sizeof(CGText);
case HDR_KEY_AGREEMENT: return sizeof(KeyAgreement);
case HDR_CG_LOGIN: return sizeof(CGLogin);
case HDR_CG_LOGIN2: return sizeof(CGLogin2);
case HDR_CG_LOGIN3: return sizeof(CGLogin3);
case HDR_CG_CHARACTER_SELECT: return sizeof(CGPlayerSelect);
case HDR_CG_CHARACTER_CREATE: return sizeof(CGPlayerCreate);
case HDR_CG_CHARACTER_DELETE: return sizeof(CGPlayerDelete);
case HDR_CG_CHANGE_NAME: return sizeof(CGChangeName);
case HDR_CG_EMPIRE: return sizeof(CGEmpire);
case HDR_CG_CLIENT_VERSION: return sizeof(CGClientVersion);
case HDR_CG_CLIENT_VERSION2: return sizeof(CGClientVersion2);
case HDR_CG_ENTERGAME: return sizeof(CGEnterGame);
case HDR_CG_MOVE: return sizeof(CGMove);
case HDR_CG_ATTACK: return sizeof(CGAttack);
case HDR_CG_TARGET: return sizeof(CGTarget);
case HDR_CG_ON_CLICK: return sizeof(CGOnClick);
case HDR_CG_CHARACTER_POSITION: return sizeof(CGPosition);
case HDR_CG_SCRIPT_ANSWER: return sizeof(CGScriptAnswer);
case HDR_CG_SCRIPT_BUTTON: return sizeof(CGScriptButton);
case HDR_CG_SCRIPT_SELECT_ITEM: return sizeof(CGScriptSelectItem);
case HDR_CG_QUEST_INPUT_STRING: return sizeof(CGQuestInput);
case HDR_CG_QUEST_CONFIRM: return sizeof(CGQuestConfirm);
case HDR_CG_ITEM_USE: return sizeof(CGItemUse);
case HDR_CG_ITEM_MOVE: return sizeof(CGItemMove);
case HDR_CG_ITEM_DROP: return sizeof(CGItemDrop);
case HDR_CG_ITEM_DROP2: return sizeof(CGItemDrop2);
case HDR_CG_ITEM_PICKUP: return sizeof(CGItemPickup);
case HDR_CG_ITEM_USE_TO_ITEM: return sizeof(CGItemUseToItem);
case HDR_CG_ITEM_GIVE: return sizeof(CGGiveItem);
case HDR_CG_QUICKSLOT_ADD: return sizeof(CGQuickSlotAdd);
case HDR_CG_QUICKSLOT_DEL: return sizeof(CGQuickSlotDel);
case HDR_CG_QUICKSLOT_SWAP: return sizeof(CGQuickSlotSwap);
case HDR_CG_USE_SKILL: return sizeof(CGUseSkill);
case HDR_CG_FLY_TARGETING: return sizeof(CGFlyTargeting);
case HDR_CG_ADD_FLY_TARGETING: return sizeof(CGFlyTargeting);
case HDR_CG_SHOOT: return sizeof(CGShoot);
case HDR_CG_WARP: return sizeof(CGWarp);
case HDR_CG_FISHING: return sizeof(CGFishing);
case HDR_CG_SYNC_POSITION: return sizeof(DynHead);
case HDR_CG_WHISPER: return sizeof(CGWhisper);
case HDR_CG_PARTY_INVITE: return sizeof(CGPartyInvite);
case HDR_CG_PARTY_INVITE_ANSWER: return sizeof(CGPartyInviteAnswer);
case HDR_CG_PARTY_REMOVE: return sizeof(CGPartyRemove);
case HDR_CG_PARTY_SET_STATE: return sizeof(CGPartySetState);
case HDR_CG_PARTY_USE_SKILL: return sizeof(CGPartyUseSkill);
case HDR_CG_PARTY_PARAMETER: return sizeof(CGPartyParameter);
case HDR_CG_GUILD: return sizeof(CGGuild);
case HDR_CG_ANSWER_MAKE_GUILD: return sizeof(CGAnswerMakeGuild);
case HDR_CG_MYSHOP: return sizeof(CGMyShopHead);
case HDR_CG_REFINE: return sizeof(CGRefine);
case HDR_CG_DRAGON_SOUL_REFINE: return sizeof(CGDragonSoulRefine);
case HDR_CG_SHOP: return sizeof(CGShop);
case HDR_CG_EXCHANGE: return sizeof(CGExchange);
case HDR_CG_MESSENGER: return sizeof(CGMessenger);
case HDR_CG_MALL_CHECKOUT: return sizeof(CGMallCheckout);
case HDR_CG_SAFEBOX_CHECKIN: return sizeof(CGSafeboxCheckin);
case HDR_CG_SAFEBOX_CHECKOUT: return sizeof(CGSafeboxCheckout);
case HDR_CG_SAFEBOX_ITEM_MOVE: return sizeof(CGItemMove);
case HDR_CG_STATE_CHECKER: return 1;
default: return 0;
}
}
constexpr int packet_size_gc(uint8_t h) {
switch (h) {
case HDR_HANDSHAKE: return sizeof(Handshake);
case HDR_GC_TIME_SYNC: return sizeof(Blank);
case HDR_GC_PHASE: return sizeof(Phase_);
case HDR_GC_BINDUDP: return sizeof(BindUDP);
case HDR_GC_PING: return sizeof(Blank);
case HDR_KEY_AGREEMENT: return sizeof(KeyAgreement);
case HDR_GC_KEY_AGREEMENT_COMPLETED: return sizeof(KeyAgreementCompleted);
case HDR_GC_LOGIN_SUCCESS: return sizeof(GCLoginSuccess3);
case HDR_GC_LOGIN_SUCCESS_NEWSLOT: return sizeof(GCLoginSuccess);
case HDR_GC_LOGIN_FAILURE: return sizeof(GCLoginFailure);
case HDR_GC_AUTH_SUCCESS: return sizeof(GCAuthSuccess);
case HDR_GC_AUTH_SUCCESS_OPENID: return sizeof(GCAuthSuccessOpenID);
case HDR_GC_MATRIX_CARD: return sizeof(GCMatrixCard);
case HDR_GC_RUNUP_MATRIX_QUIZ: return sizeof(GCRunupMatrixQuiz);
case HDR_GC_REQUEST_PASSPOD: return sizeof(GCRequestPasspod);
case HDR_GC_REQUEST_PASSPOD_FAILED: return sizeof(GCPasspodFailed);
case HDR_GC_HS_REQUEST: return sizeof(GCHSRequest);
case HDR_GC_XTRAP_CS1_REQUEST: return sizeof(GCXTrapRequest);
case HDR_GC_PANAMA_PACK: return sizeof(GCPanamaPack);
case HDR_GC_EMPIRE: return sizeof(GCEmpire);
case HDR_GC_LOGIN_KEY: return sizeof(GCLoginKey);
case HDR_GC_CHARACTER_CREATE_SUCCESS: return sizeof(GCPlayerCreateSuccess);
case HDR_GC_CREATE_FAILURE: return sizeof(GCPlayerCreateFailure);
case HDR_GC_DELETE_SUCCESS: return sizeof(GCPlayerDeleteSuccess);
case HDR_GC_DELETE_WRONG_SOCIAL_ID: return sizeof(Blank);
case HDR_GC_MAIN_CHARACTER: return sizeof(GCMainCharacter);
case HDR_GC_MAIN_CHARACTER3_BGM: return sizeof(GCMainCharacter3BGM);
case HDR_GC_MAIN_CHARACTER4_BGM_VOL: return sizeof(GCMainCharacter4BGM);
case HDR_GC_CHARACTER_ADD: return sizeof(GCCharacterAdd);
case HDR_GC_CHARACTER_ADD2: return sizeof(GCCharacterAdd2);
case HDR_GC_CHARACTER_UPDATE_NEW: return sizeof(GCCharacterUpdate);
case HDR_GC_CHARACTER_UPDATE2: return sizeof(GCCharacterUpdate);
case HDR_GC_CHARACTER_DEL: return sizeof(GCCharacterDel);
case HDR_GC_CHANGE_NAME: return sizeof(GCChangeNameNotice);
case HDR_GC_ATTACK: return sizeof(GCAttack);
case HDR_GC_MOVE: return sizeof(GCMove);
case HDR_GC_CHARACTER_POINTS: return sizeof(GCPoints);
case HDR_GC_CHARACTER_POINT_CHANGE: return sizeof(GCPointChange);
case HDR_GC_ITEM_SET: return sizeof(GCItemSet);
case HDR_GC_ITEM_DEL: return sizeof(GCItemDel);
case HDR_GC_ITEM_USE: return sizeof(GCItemUse);
case HDR_GC_ITEM_UPDATE: return sizeof(GCItemUpdate);
case HDR_GC_ITEM_GROUND_ADD: return sizeof(GCItemGroundAdd);
case HDR_GC_ITEM_GROUND_DEL: return sizeof(GCItemGroundDel);
case HDR_GC_ITEM_OWNERSHIP: return sizeof(GCItemOwnership);
case HDR_GC_OWNERSHIP: return sizeof(GCOwnership);
case HDR_GC_CHARACTER_UPDATE: return sizeof(GCCharacterUpdate);
case HDR_GC_STUN: return sizeof(GCStun);
case HDR_GC_DEAD: return sizeof(GCDead);
case HDR_GC_MOTION: return sizeof(GCMotion);
case HDR_GC_CHARACTER_POSITION: return sizeof(GCCharacterPosition);
case HDR_GC_CHANGE_SPEED: return sizeof(GCChangeSpeed);
case HDR_GC_WALK_MODE: return sizeof(GCWalkMode);
case HDR_GC_TARGET: return sizeof(GCTarget);
case HDR_GC_DAMAGE_INFO: return sizeof(GCDamageInfo);
case HDR_GC_CHAR_ADDITIONAL_INFO: return sizeof(GCCharAddInfo);
case HDR_GC_SKILL_LEVEL: return sizeof(GCSkillLevel);
case HDR_GC_SKILL_COOLTIME_END: return sizeof(GCSkillCooltimeEnd);
case HDR_GC_QUICKSLOT_ADD: return sizeof(GCQuickSlotAdd);
case HDR_GC_QUICKSLOT_DEL: return sizeof(GCQuickSlotDel);
case HDR_GC_QUICKSLOT_SWAP: return sizeof(GCQuickSlotSwap);
case HDR_GC_AFFECT_ADD: return sizeof(GCAffectAdd);
case HDR_GC_AFFECT_REMOVE: return sizeof(GCAffectRemove);
case HDR_GC_EXCHANGE: return sizeof(GCExchange);
case HDR_GC_PVP: return sizeof(GCPvp);
case HDR_GC_MOUNT: return sizeof(GCMount);
case HDR_GC_CREATE_FLY: return sizeof(GCCreateFly);
case HDR_GC_FLY_TARGETING: return sizeof(GCFlyTargeting);
case HDR_GC_ADD_FLY_TARGETING: return sizeof(GCFlyTargeting);
case HDR_GC_SHOP_SIGN: return sizeof(GCShopSign);
case HDR_GC_SKILL_LEVEL_OLD: return sizeof(GCSkillLevelOld);
case HDR_GC_SKILL_GROUP: return sizeof(GCSkillGroup);
case HDR_GC_SPECIAL_EFFECT: return sizeof(GCSpecialEffect);
case HDR_GC_SPECIFIC_EFFECT: return sizeof(GCSpecificEffect);
case HDR_GC_OBSERVER_ADD: return sizeof(GCObserverAdd);
case HDR_GC_OBSERVER_REMOVE: return sizeof(GCObserverRemove);
case HDR_GC_OBSERVER_MOVE: return sizeof(GCObserverMove);
case HDR_GC_PARTY_INVITE: return sizeof(GCPartyInvite);
case HDR_GC_PARTY_ADD: return sizeof(GCPartyAdd);
case HDR_GC_PARTY_UPDATE: return sizeof(GCPartyUpdate);
case HDR_GC_PARTY_REMOVE: return sizeof(GCPartyRemove);
case HDR_GC_PARTY_LINK: return sizeof(GCPartyLink);
case HDR_GC_PARTY_UNLINK: return sizeof(GCPartyUnlink);
case HDR_GC_PARTY_PARAMETER: return sizeof(GCPartyParameter);
case HDR_GC_SAFEBOX_MONEY_CHANGE: return sizeof(GCSafeboxMoneyChange);
case HDR_GC_SAFEBOX_SET: return sizeof(GCItemSet);
case HDR_GC_SAFEBOX_DEL: return sizeof(GCItemDel);
case HDR_GC_SAFEBOX_WRONG_PASSWORD: return sizeof(Blank);
case HDR_GC_SAFEBOX_SIZE: return sizeof(GCSafeboxSize);
case HDR_GC_FISHING: return sizeof(GCFishing);
case HDR_GC_REQUEST_MAKE_GUILD: return sizeof(Blank);
case HDR_GC_MALL_OPEN: return sizeof(GCMallOpen);
case HDR_GC_MALL_SET: return sizeof(GCItemSet);
case HDR_GC_MALL_DEL: return sizeof(GCItemDel);
case HDR_GC_TIME: return sizeof(GCTime);
case HDR_GC_CHANNEL: return sizeof(GCChannel);
case HDR_GC_WARP: return sizeof(GCWarpClassic);
case HDR_GC_TARGET_UPDATE: return sizeof(GCTargetUpdate13);
case HDR_GC_TARGET_DELETE: return sizeof(GCTargetDelete5);
case HDR_GC_TARGET_CREATE: return sizeof(GCTargetCreate43);
case HDR_GC_LOVER_INFO: return sizeof(GCLoverInfo);
case HDR_GC_LOVE_POINT_UPDATE: return sizeof(GCLovePointUpdate);
case HDR_GC_DIG_MOTION: return sizeof(GCDigMotion);
case HDR_GC_VIEW_EQUIP: return sizeof(GCViewEquip);
case HDR_GC_REFINE_INFORMATION_OLD:
case HDR_GC_REFINE_INFORMATION: return sizeof(GCRefineInfo);
case HDR_GC_DRAGON_SOUL_REFINE: return sizeof(GCDragonSoulRefine);
case HDR_GC_QUEST_CONFIRM: return sizeof(GCQuestConfirm);
default: return 0;
}
}
// docs §2.2: CG packets flagged bSeq=true carry a trailing sequence byte once
// sequence mode is enabled. bSeq=false set: TEXT, HANDSHAKE, MARK_*, KEY_AGREEMENT,
// GUILD_SYMBOL_UPLOAD, SYMBOL_CRC, DRAGON_SOUL_REFINE, STATE_CHECKER.
constexpr bool is_sequence_cg(uint8_t h) {
switch (h) {
case HDR_CG_TEXT:
case HDR_HANDSHAKE:
case HDR_CG_MARK_LOGIN:
case HDR_CG_MARK_IDXLIST:
case HDR_CG_MARK_CRCLIST:
case HDR_CG_MARK_UPLOAD:
case HDR_KEY_AGREEMENT:
case HDR_CG_GUILD_SYMBOL_UPLOAD:
case HDR_CG_SYMBOL_CRC:
case HDR_CG_DRAGON_SOUL_REFINE:
case HDR_CG_STATE_CHECKER:
return false;
default:
return true; // every other registered CG packet
}
}
// docs §2.2: client CMainPacketHeaderMap DYNAMIC_SIZE_PACKET set (GC) + the two
// CG packets whose struct carries its own length word.
constexpr bool is_dynamic_gc(uint8_t h) {
switch (h) {
case HDR_GC_CHAT:
case HDR_GC_SCRIPT:
case HDR_GC_SHOP:
case HDR_GC_GUILD:
case HDR_GC_MESSENGER:
case HDR_GC_QUEST_INFO:
case HDR_GC_DUEL_START:
case HDR_GC_SYNC_POSITION:
// client CMainPacketHeaderMap registers GC_WHISPER STATIC, but its
// `packet_whisper.wSize` sits exactly where TDynamicSizePacketHeader.size
// does, so the dynamic path frames it correctly (the handler reads the
// text tail itself anyway).
case HDR_GC_WHISPER:
case HDR_GC_DUNGEON:
case HDR_GC_NPC_POSITION:
case HDR_GC_LAND_LIST:
case HDR_GC_SYMBOL_DATA:
case HDR_GC_HYBRIDCRYPT_KEYS:
case HDR_GC_HYBRIDCRYPT_SDB:
return true;
default:
return false;
}
}
constexpr bool is_dynamic_cg(uint8_t h) { return h == HDR_CG_CHAT || h == HDR_CG_WHISPER; }
} // namespace mtnet::classic