aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Metin2

Metin2 Packet Structure: Client-Server Communication

When a character moves, an item is picked up, or a chat message is sent, everything flowing between the client and the server is a metin2 packet. Metin2's network protocol consists of binary messages sent over TCP, where the very first byte identifies the type of the packet. In this article I walk through how packets are structured, what the header naming means, the difference between fixed and dynamic packets, and how the server decodes incoming bytes and routes them to the correct handler.

What a packet looks like: the header byte

In the Metin2 protocol every packet begins with a single header byte. Because it is a BYTE, it holds a value from 0 to 255 and identifies the packet type. When either the server or the client reads the first byte from the stream, it knows which packet it is and therefore how many more bytes it must read.

Packets are defined as C++ structs and packed with #pragma pack(1) so there is no byte padding in memory. This is critical: if the compiler applies its default alignment, the client and server see different byte layouts and the protocol breaks.

#pragma pack(1)

// Client -> Game: basic movement packet (example)
typedef struct command_move
{
    BYTE    bHeader;     // packet type
    BYTE    bFunc;       // movement type (walk/run/stop)
    BYTE    bArg;
    BYTE    bRot;        // rotation
    long    lX;          // target coordinate
    long    lY;
    DWORD   dwTime;      // client timestamp
} TPacketCGMove;

Here the CG prefix indicates the direction of the packet, which we cover next. The key point is that the header alone determines the layout of the rest. The moment the server reads bHeader, it reads exactly sizeof(TPacketCGMove) bytes and copies them straight into the structure.

CG, GC and the inter-process header naming

Metin2 is made up of several processes (auth, db, game/core), and packet names encode their direction with two letters. This convention is the fastest way to orient yourself when reading the source:

  • CG — Client to Game: from the client to the game core (movement, attack, chat, item use).
  • GC — Game to Client: from the game core to the client (character add, HP update, chat broadcast).
  • GD / DG — communication between the game and the DB process (loading and saving players).
  • GG — Game to Game: peer-to-peer messaging between cores (channels).

So HEADER_CG_ATTACK is the attack packet sent by the client, while HEADER_GC_CHARACTER_ADD is the packet the server sends to the client to place a new character on screen. Header constants are typically kept in an enum:

enum
{
    HEADER_CG_HANDSHAKE       = 253,
    HEADER_CG_LOGIN           = 1,
    HEADER_CG_ATTACK          = 2,
    HEADER_CG_MOVE            = 3,
    HEADER_CG_CHAT            = 4,
    // ...
};

The numeric values here vary by version and source and may differ between private server bases. What matters is not the number itself but that the client and server share the same header table. If the two sides use different values, packets are misinterpreted.

Fixed and dynamic-sized packets

The vast majority of packets are fixed size: once the header is read, the size of the struct is already known. Movement, attack and HP-update packets fall into this group. But a chat message, item names, or a packet carrying a variable-length list does not fit a predetermined size. These are dynamic packets and carry a WORD size field right after the header:

#pragma pack(1)

// Client -> Game: chat packet (dynamic)
typedef struct command_chat
{
    BYTE    bHeader;     // HEADER_CG_CHAT
    WORD    wSize;       // total length of the whole packet
    BYTE    bType;       // normal / party / guild ...
    // followed by text bytes up to wSize
} TPacketCGChat;

The server first reads the header, then the wSize field to learn the total packet length. The message text is computed by subtracting the fixed part of the struct from that size. Dynamic packets are therefore read in two stages: first the fixed head, then the body up to wSize.

Connection flow: from handshake to game

The client does not start sending packets immediately. The connection passes through several phases, and in each phase only the packets belonging to that phase are accepted. A typical flow looks like this:

  • Handshake: after the TCP connection is established, the server sends a handshake packet. Client and server exchange timestamps back and forth a few times to measure network latency and synchronize their clocks.
  • Login / Auth: once time synchronization is precise enough, the client sends the login packet (with its session key).
  • Select: the character list is sent and the player picks a character.
  • Loading: the map and character data are loaded; the client signals it is ready.
  • Game: the actual gameplay phase. Movement, combat and chat packets now flow freely.

The handshake phase is especially important: because Metin2 validates movement and attacks against timestamps, the client and server clocks need to be close. Handshake packets are repeated until the round-trip measurement falls below an acceptable threshold.

How does the server read a packet?

On the server side, each connection has a buffer and an input processor matching its current phase. Because TCP is stream-based, bytes arrive in fragments; the server must never process an incomplete packet. The parsing loop is roughly as follows:

  • Bytes arriving from the socket are appended to the connection's buffer.
  • If the buffer holds at least 1 byte, the header is read (without consuming it from the buffer yet).
  • The header determines the packet type and its expected size. If dynamic, wSize is read too.
  • If the buffer does not yet hold the whole packet, the loop stops and waits for more bytes.
  • Once the complete packet is present it is handed to its handler and those bytes are consumed.
// Core of the parsing loop (pseudocode)
while (buffer.size() >= 1)
{
    BYTE header = buffer.peek_byte(0);
    int  packetSize = GetPacketSize(header);   // from wSize if dynamic

    if (buffer.size() < packetSize)
        break;                                 // packet not complete yet

    Dispatch(header, buffer.read(packetSize)); // route to the handler
}

If the header is an unknown value (not in the header table), this is usually treated as a protocol violation and the connection is closed; in many private server bases this is an extra line of defense for detecting cheating or manipulation.

Encryption and a security note

In the earliest Metin2 versions packets were sent in plain (unencrypted) form. Later versions and today's private servers perform a key exchange during the handshake to encrypt the packet body. This makes packet sniffing and forged-packet injection harder. Even so, the server must never trust the client: every field of an incoming packet (coordinates, damage, quantity) has to be validated server-side. Blindly accepting a client-controlled value is the root cause of exploits like speed hacks, teleporting and item duplication (dupe).

Frequently Asked Questions

Do Metin2 packets use TCP or UDP?

Game traffic goes over TCP. All packets, including movement and combat, require reliable ordered delivery, so stream-based TCP is used; that is why partial packets accumulating in the server buffer is normal and must be handled correctly.

Why is the header a single byte, and is that enough?

A single BYTE addresses 256 distinct packet types, which is more than enough for the core game protocol. When more sub-types are needed, an extra bSubHeader or bType field is placed inside the packet, so the single header byte is preserved while the types expand.

What should I watch out for when adding my own packet?

Define the header constant with the same value on both client and server, pack the struct with #pragma pack(1), decide whether you need a wSize based on fixed vs. dynamic, and always validate every incoming field in the server handler.

Want to build a custom packet system on your Metin2 server or fix a protocol-level bug? Let's review the client-server communication together and put a solid solution in place. Get in touch with me.

Bu kategorideki tüm yazılar →

Devamı için