#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 #include // §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, // 40250 extended quest/script notification; payload is dynamically framed. HDR_GC_QUEST_SCRIPT = 148, // Extended servers also emit a dynamically framed mount/status notice here. HDR_GC_EXTENDED_STATUS = 59, // 40250 extended character/status packet (dynamic framing). HDR_GC_EXTENDED_CHARACTER = 105, 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 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; // header 32 using GCLoginSuccess3 = GCLoginSuccessT; // 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, }; // --- 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_QUEST_SCRIPT: case HDR_GC_EXTENDED_STATUS: case HDR_GC_EXTENDED_CHARACTER: 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