Skip to content

Packet Header

All server and client UDP messages begin with a compact, fixed-size header.\ It provides essential metadata for validation, ordering, and synchronization, while keeping bandwidth usage low.


Structure

Wire format: 7 bytes (big-endian)

Field Type Size Endianness Description
messageType uint8 1 N/A Logical packet kind (e.g., Snapshot, Input)
sequenceId uint16 2 Big-endian Monotonic per-peer counter (ordering/dedup)
tickId uint32 4 Big-endian Authoritative server tick index

Notes: - messageType uses a single byte; unknown types are ignored by receivers.- sequenceId increases per peer and wraps naturally; stale or out-of-order packets can be detected. - tickId references the simulation tick for snapshot payloads; for input packets, it may be set to the most recent tick known to the sender.

                           ***

                       ##**Encoding Rules * *

                       -Endianness : sequence and tick are encoded in big -
                   endian(network byte order).- Size : always 7 bytes on the wire; never padded.
  • Platform neutrality: do not rely on in-memory struct layout for transmission.

C++ Definition

Location: shared/include/network/PacketHeader.hpp

#include <array>
#include <cstdint>
#include <optional>

enum class MessageType : std::uint8_t {
    Invalid   = 0,
    Snapshot  = 1, // Server -> Client
    Input     = 2, // Client -> Server
    Handshake = 3,
    Ack       = 4,
    PlayerDisconnected = 0x1C,
    EntitySpawn        = 0x1D,
    EntityDestroyed    = 0x1E
};

struct PacketHeader
{
    std::uint8_t messageType = static_cast<std::uint8_t>(MessageType::Invalid);
    std::uint16_t sequenceId = 0;
    std::uint32_t tickId     = 0;

    static constexpr std::size_t kSize = 7; // 1 + 2 + 4

    std::array<std::uint8_t, kSize> encode() const noexcept
    {
        std::array<std::uint8_t, kSize> out{};
        out[0] = messageType;
        out[1] = static_cast<std::uint8_t>((sequenceId >> 8) & 0xFF);
        out[2] = static_cast<std::uint8_t>(sequenceId & 0xFF);
        out[3] = static_cast<std::uint8_t>((tickId >> 24) & 0xFF);
        out[4] = static_cast<std::uint8_t>((tickId >> 16) & 0xFF);
        out[5] = static_cast<std::uint8_t>((tickId >> 8) & 0xFF);
        out[6] = static_cast<std::uint8_t>(tickId & 0xFF);
        return out;
    }

    static std::optional<PacketHeader> decode(const std::uint8_t* data, std::size_t len) noexcept
    {
        if (!data || len < kSize)
            return std::nullopt;
        PacketHeader h{};
        h.messageType = data[0];
        h.sequenceId  = static_cast<std::uint16_t>((static_cast<std::uint16_t>(data[1]) << 8) |
                                                   static_cast<std::uint16_t>(data[2]));
        h.tickId      = (static_cast<std::uint32_t>(data[3]) << 24) | (static_cast<std::uint32_t>(data[4]) << 16) |
                   (static_cast<std::uint32_t>(data[5]) << 8) | static_cast<std::uint32_t>(data[6]);
        return h;
    }
};
This approach avoids platform -
specific headers(`winsock2.h`, `<arpa / inet.h>`) inside the
public interface and guarantees identical wire encoding on Windows,
Linux,
and macOS.

                ***

            ##**Usage Guidelines * *

            -Validate `messageType` and size before decoding the payload.-
        Maintain a per - client `sequenceId` to ignore stale
    or
    duplicated packets.- Treat `tickId` as the authoritative server timeline when processing snapshots.- Do not cast
    or
    memcpy the struct directly to the socket buffer—always use `encode()`/`decode()`.

                * **

                   ## *
                *Compatibility *
                *

                -Fixed
            - size wire header(7 bytes) ensures consistent behavior across compilers
        and platforms.- Big - endian encoding follows standard network byte order conventions.- The in
                - memory `sizeof(PacketHeader)` is not relied upon;

only PacketHeader::kSize and the encode / decode functions define the wire format.