Skip to main content
The packet system is responsible for transforming raw network bytes into structured C++ objects and vice versa. It handles Growtopia’s custom protocol formats including text messages, binary game packets, and variant function calls.

Packet Pipeline

Every packet flows through a decode → process → encode cycle:
Raw Bytes → PacketDecoder → Payload → PacketRegistry → IPacket
    ↓                                                      ↓
Network ← PacketHelper ← Payload ← write() ← Modified IPacket

Message Types

Growtopia uses several network message types defined in /home/daytona/workspace/source/src/packet/packet_types.hpp:4:
enum NetMessageType : uint32_t {
    NET_MESSAGE_UNKNOWN,
    NET_MESSAGE_SERVER_HELLO,
    NET_MESSAGE_GENERIC_TEXT,
    NET_MESSAGE_GAME_MESSAGE,
    NET_MESSAGE_GAME_PACKET,
    NET_MESSAGE_ERROR,
    NET_MESSAGE_TRACK,
    NET_MESSAGE_CLIENT_LOG_REQUEST,
    NET_MESSAGE_CLIENT_LOG_RESPONSE,
    NET_MESSAGE_MAX
};

Message Type Breakdown

The initial server greeting containing connection parameters. Sent immediately after connection establishment.Structure: Custom binary format with server configuration Direction: Server → Client only Example: Server version info, encryption keys
Text-based messages using key-value pairs separated by newlines.Structure: key|value\n format parsed by TextParse Direction: Bidirectional Example:
action|input
text|/warp START
Binary packets containing game state updates (movement, tile changes, etc.).Structure: GameUpdatePacket struct + optional extra data Direction: Bidirectional Example: Player position updates, tile modifications, inventory changes

Packet Decoder

The PacketDecoder (/home/daytona/workspace/source/src/packet/packet_decoder.cpp:9) is the entry point for all incoming packets:
std::optional<std::shared_ptr<IPacket>> PacketDecoder::decode(
    std::span<const std::byte> data,
    const core::Config::LogConfig& log_config,
    std::string_view direction
);

Decoding Process

  1. Read Message Type: First 4 bytes indicate the NetMessageType
  2. Parse Based on Type:
    • SERVER_HELLO: Create TextPayload with server hello data
    • GENERIC_TEXT/GAME_MESSAGE: Parse string as TextParse, create TextPayload
    • GAME_PACKET: Read GameUpdatePacket struct, handle extra data
  3. Check for Variant: If packet type is PACKET_CALL_FUNCTION, deserialize variant
  4. Create Packet: Use PacketRegistry to instantiate appropriate packet class
  5. Return: Return std::optional<std::shared_ptr<IPacket>>

Decoding Example (from source)

Here’s how a game message is decoded (/home/daytona/workspace/source/src/packet/packet_decoder.cpp:36):
case NET_MESSAGE_GENERIC_TEXT:
case NET_MESSAGE_GAME_MESSAGE: {
    std::string message{};
    stream.read(message, static_cast<uint16_t>(stream.get_size() - sizeof(NetMessageType) - 1));

    utils::TextParse parser{ message };
    if (log_config.print_message) {
        spdlog::info(
            "{} ({} bytes):\n{}",
            magic_enum::enum_name(msg_type),
            message.size(),
            parser
        );
    }

    TextPayload text_payload{ msg_type, std::move(parser), data };
    auto packet = PacketRegistry::instance().create(text_payload);
    return packet;
}
The decoder preserves original raw bytes in the raw_data field, allowing unmodified packets to be forwarded without re-serialization.

Payload System

Payloads are variant-based wrappers around packet data defined in /home/daytona/workspace/source/src/packet/payload.hpp:118:
using Payload = std::variant<TextPayload, GamePayload, VariantPayload, RawPayload>;

Payload Types

TextPayload

struct TextPayload {
    NetMessageType message_type;
    utils::TextParse data;
    std::vector<std::byte> raw_data;
};
Used for text-based messages. The TextParse utility parses key|value\n format:
TextParse parser("action|input\ntext|hello\n");
auto action = parser.get<std::string>("action");  // "input"
auto text = parser.get<std::string>("text");      // "hello"

GamePayload

struct GamePayload {
    GameUpdatePacket packet;
    std::vector<std::byte> extra;
    std::vector<std::byte> raw_data;
};
Contains the core GameUpdatePacket structure (/home/daytona/workspace/source/src/packet/packet_types.hpp:92):
#pragma pack(push, 1)
struct GameUpdatePacket {
    PacketType type;
    uint8_t pad[3];
    
    union {
        uint32_t net_id;
        int32_t object_change_type;
    };
    int32_t item_net_id;

    union {
        PacketFlag value;
        struct {
            uint32_t none : 1;
            uint32_t unk : 1;
            uint32_t reset_visual_state : 1;
            uint32_t extended : 1;
            uint32_t rotate_left : 1;
            uint32_t on_solid : 1;
            // ... more flags ...
        };
    } flags;

    float float_var;

    union {
        uint32_t decompressed_data_size;
        int32_t object_id;
        int32_t int_data;
        int32_t item_id;
    };

    union {
        uint8_t pad_4[28];
        struct {
            float pos_x;
            float pos_y;
            float pos_x2;
            float pos_y2;
            uint8_t pad_5[4];
            int32_t int_x;
            int32_t int_y;
        };
    };

    uint32_t data_size;
};
#pragma pack(pop)
The #pragma pack(push, 1) ensures the struct has no padding, matching Growtopia’s wire format exactly. Do not modify without understanding implications.

VariantPayload

struct VariantPayload {
    GameUpdatePacket game_packet;
    PacketVariant variant;
    std::vector<std::byte> raw_data;
    
    [[nodiscard]] std::string function_name() const {
        return variant.get<std::string>(0);
    }
};
Variant packets are function calls with typed arguments. The first variant is always the function name.

RawPayload

struct RawPayload {
    std::vector<std::byte> data;
};
Used for unknown or pass-through packets that don’t need parsing.

Packet Variant System

The PacketVariant class (/home/daytona/workspace/source/src/packet/packet_variant.hpp:24) handles Growtopia’s variant function call format:
class PacketVariant {
public:
    PacketVariant();
    template<typename... Args>
    explicit PacketVariant(const Args&... args);

    template<typename T = std::string>
    void add(const T& value);
    
    template <typename T = std::string>
    [[nodiscard]] T get(const std::size_t index) const;
    
    void set(const std::size_t index, const variant& value);
    
    [[nodiscard]] std::vector<std::byte> serialize() const;
    [[nodiscard]] bool deserialize(std::span<const std::byte> data);
};

Variant Types

enum class VariantType : uint8_t {
    UNKNOWN,
    FLOAT,
    STRING,
    VEC2,
    VEC3,
    UNSIGNED,
    SIGNED = 9
};

using variant = std::variant<float, std::string, glm::vec2, glm::vec3, uint32_t, int32_t>;

Creating Variants

// Create OnSpawn variant
PacketVariant spawn_variant(
    "OnSpawn",                    // Function name (index 0)
    "spawn|avatar\n...",          // Spawn data (index 1)
    std::uint32_t(12345),         // Net ID (index 2)
    glm::vec2(100.0f, 200.0f)     // Position (index 3)
);

// Access values
std::string func_name = spawn_variant.get<std::string>(0);
glm::vec2 pos = spawn_variant.get<glm::vec2>(3);

Serialization Format

Variants serialize as:
[1 byte: count]
For each variant:
  [1 byte: index]
  [1 byte: type]
  [N bytes: value data]
Example from source (/home/daytona/workspace/source/src/packet/packet_variant.hpp:56):
std::vector<std::byte> PacketVariant::serialize() const {
    const size_t size{ variants_.size() };
    utils::ByteStream<uint32_t> stream{};
    stream.write<uint8_t>(size);

    for (size_t i{ 0 }; i < size; i++) {
        VariantType type{ get_type(variants_[i]) };
        stream.write<uint8_t>(i);
        stream.write(static_cast<std::underlying_type_t<VariantType>>(type));

        if (type == VariantType::FLOAT) {
            stream.write(std::get<float>(variants_[i]));
        }
        else if (type == VariantType::STRING) {
            stream.write(std::get<std::string>(variants_[i]));
        }
        // ... other types ...
    }
    return stream.take_data();
}

Packet ID System

Packet IDs identify specific packet types defined in /home/daytona/workspace/source/src/packet/packet_id.hpp:11:
enum class PacketId : uint32_t {
    ServerHello,
    Padding = 0x1000,
    Quit,
    QuitToExit,
    JoinRequest,
    ValidateWorld,
    Input,
    Log,
    OnNameChanged,
    OnChangeSkin,
    Padding2 = 0x2000,
    Disconnect,
    TileChangeRequest,
    SendMapData,
    SendTileUpdateData,
    SendItemDatabaseData,
    SendInventoryState,
    ModifyItemInventory,
    ItemChangeObject,
    Padding3 = 0x3000,
    OnSendToServer,
    OnSpawn,
    OnRemove,
    OnSuperMainStartAcceptLogonHrdxs47254722215a,
    Unknown = std::numeric_limits<uint32_t>::max(),
};

Packet ID Derivation

Packet IDs are derived from the payload content:

Text Packets (Regex-based)

inline std::vector<TextRegexPattern>& get_text_regex_patterns() {
    static std::vector<TextRegexPattern> patterns = []() {
        std::vector<TextRegexPattern> p;
        p.emplace_back(R"(^action\|quit$)", PacketId::Quit);
        p.emplace_back(R"(^action\|quit_to_exit$)", PacketId::QuitToExit);
        p.emplace_back(R"(^action\|join_request$)", PacketId::JoinRequest);
        p.emplace_back(R"(^action\|input)", PacketId::Input);
        return p;
    }();
    return patterns;
}

Variant Packets (Function Name Map)

inline const std::unordered_map<std::string_view, PacketId> VARIANT_FUNCTION_MAP = {
    { "OnSendToServer", PacketId::OnSendToServer },
    { "OnSpawn", PacketId::OnSpawn },
    { "OnRemove", PacketId::OnRemove },
    { "OnNameChanged", PacketId::OnNameChanged },
    { "OnChangeSkin", PacketId::OnChangeSkin },
    { "OnSuperMainStartAcceptLogonHrdxs47254722215a", PacketId::OnSuperMainStartAcceptLogonHrdxs47254722215a },
};

Game Packets (Packet Type Map)

inline const std::unordered_map<PacketType, PacketId> GAME_PACKET_MAP = {
    { PACKET_DISCONNECT, PacketId::Disconnect },
    { PACKET_TILE_CHANGE_REQUEST, PacketId::TileChangeRequest },
    { PACKET_SEND_MAP_DATA, PacketId::SendMapData },
    { PACKET_SEND_TILE_UPDATE_DATA, PacketId::SendTileUpdateData },
    { PACKET_SEND_ITEM_DATABASE_DATA, PacketId::SendItemDatabaseData },
    { PACKET_SEND_INVENTORY_STATE, PacketId::SendInventoryState },
    { PACKET_MODIFY_ITEM_INVENTORY, PacketId::ModifyItemInventory },
    { PACKET_ITEM_CHANGE_OBJECT, PacketId::ItemChangeObject },
};

Packet Registry

The PacketRegistry (/home/daytona/workspace/source/src/packet/packet_registry.hpp:26) uses the factory pattern to create packet instances:
class PacketRegistry : public utils::Singleton<PacketRegistry> {
public:
    template<typename T>
    void register_packet() {
        static_assert(
            std::is_base_of_v<IPacket, T>,
            "Registered type must derive from IPacket"
        );
        registry_[T::ID] = [] { return std::make_shared<T>(); };
    }

    [[nodiscard]] std::shared_ptr<IPacket> create(const PacketId id) const;
    [[nodiscard]] std::shared_ptr<IPacket> create(const Payload& payload) const;
    [[nodiscard]] bool is_registered(const PacketId id) const;
};

Registering Packets

Packets are registered at startup in register_packets.hpp:
PacketRegistry::instance().register_packet<packet::game::SendMapData>();
PacketRegistry::instance().register_packet<packet::game::SendTileUpdateData>();
PacketRegistry::instance().register_packet<packet::message::Input>();
// ...

Packet Base Classes

All packets inherit from IPacket (/home/daytona/workspace/source/src/packet/packet_helper.hpp:21):
class IPacket : public std::enable_shared_from_this<IPacket> {
public:
    virtual ~IPacket() = default;

    [[nodiscard]] virtual PacketId id() const = 0;
    [[nodiscard]] virtual int channel() const { return 0; }
    [[nodiscard]] virtual bool read(const Payload& payload) = 0;
    [[nodiscard]] virtual Payload write() = 0;

    std::vector<std::byte> raw_data;
    [[nodiscard]] bool has_raw_data() const { return !raw_data.empty(); }
};

Packet Templates

TextPacket

template <PacketId Id, NetMessageType MsgType = NET_MESSAGE_GAME_MESSAGE, int Channel = 0>
struct TextPacket : IPacket {
    static constexpr PacketId ID = Id;
    static constexpr NetMessageType MESSAGE_TYPE = MsgType;
    static constexpr int CHANNEL = Channel;

    utils::TextParse text_parse;

    [[nodiscard]] PacketId id() const override { return ID; }
    [[nodiscard]] int channel() const override { return CHANNEL; }
};

GamePacket

template <PacketId Id, PacketType PktType, int Channel = 0>
struct GamePacket : IPacket {
    static constexpr PacketId ID = Id;
    static constexpr PacketType PACKET_TYPE = PktType;

    GameUpdatePacket game_packet{};
    std::vector<std::byte> extra;
};

VariantPacket

template <PacketId Id, int Channel = 0>
struct VariantPacket : IPacket {
    static constexpr PacketId ID = Id;
    static constexpr PacketType PACKET_TYPE = PACKET_CALL_FUNCTION;

    PacketVariant variant;
    GameUpdatePacket game_packet{};
};

Creating Custom Packets

To define a new packet structure:

1. Add Packet ID

// In packet_id.hpp
enum class PacketId : uint32_t {
    // ... existing IDs ...
    MyCustomPacket,
};

2. Add ID Mapping

// For variant packets
inline const std::unordered_map<std::string_view, PacketId> VARIANT_FUNCTION_MAP = {
    // ... existing mappings ...
    { "OnMyCustomFunction", PacketId::MyCustomPacket },
};

// OR for game packets
inline const std::unordered_map<PacketType, PacketId> GAME_PACKET_MAP = {
    // ... existing mappings ...
    { PACKET_MY_CUSTOM_TYPE, PacketId::MyCustomPacket },
};

3. Define Packet Struct

// In packet/game/my_custom.hpp
namespace packet::game {
struct MyCustomPacket : VariantPacket<PacketId::MyCustomPacket> {
    [[nodiscard]] bool read(const Payload& payload) override {
        if (const auto* var = get_payload_if<VariantPayload>(payload)) {
            variant = var->variant;
            game_packet = var->game_packet;
            raw_data = var->raw_data;
            return true;
        }
        return false;
    }

    [[nodiscard]] Payload write() override {
        return VariantPayload{ game_packet, variant };
    }
    
    // Helper methods
    std::string get_custom_field() const {
        return variant.get<std::string>(1);
    }
    
    void set_custom_field(const std::string& value) {
        variant.set(1, value);
    }
};
}

4. Register Packet

// In register_packets.hpp
PacketRegistry::instance().register_packet<packet::game::MyCustomPacket>();

Packet Serialization

The PacketHelper (/home/daytona/workspace/source/src/packet/packet_helper.hpp:90) handles serialization:
struct PacketHelper {
    static std::vector<std::byte> serialize(const Payload& payload);
    static std::vector<std::byte> serialize(IPacket& packet);
    
    static bool write(IPacket& packet, NetworkSender auto& sender) {
        auto data{ serialize(packet) };
        if (data.empty()) {
            return false;
        }
        data.push_back(static_cast<std::byte>(0x00));  // Null terminator
        return sender.write(data, packet.channel());
    }
};

Serialization Example (from source)

std::vector<std::byte> PacketHelper::serialize(const Payload& payload) {
    utils::ByteStream byte_stream{};

    if (const auto* text = get_payload_if<TextPayload>(payload)) {
        byte_stream.write(magic_enum::enum_underlying(text->message_type));
        byte_stream.write(text->data.get_raw(), false);
    }
    else if (const auto* game = get_payload_if<GamePayload>(payload)) {
        byte_stream.write(magic_enum::enum_underlying(NET_MESSAGE_GAME_PACKET));

        GameUpdatePacket header = game->packet;
        if (!game->extra.empty()) {
            header.flags.extended = 1;
            header.data_size = static_cast<uint32_t>(game->extra.size());
        }

        byte_stream.write(header);
        byte_stream.write_data(game->extra.data(), game->extra.size());
    }
    else if (const auto* var = get_payload_if<VariantPayload>(payload)) {
        byte_stream.write(magic_enum::enum_underlying(NET_MESSAGE_GAME_PACKET));

        GameUpdatePacket game_packet{ var->game_packet };
        game_packet.type = PACKET_CALL_FUNCTION;

        const auto ext_data = var->variant.serialize();
        game_packet.flags.extended = 1;
        game_packet.data_size = static_cast<uint32_t>(ext_data.size());

        byte_stream.write(game_packet);
        byte_stream.write_data(ext_data.data(), ext_data.size());
    }
    
    return byte_stream.take_data();
}
If a packet has raw_data set, serialization uses the original bytes directly without re-encoding. This optimization avoids unnecessary work for unmodified packets.

ByteStream Utility

The ByteStream class (/home/daytona/workspace/source/src/utils/byte_stream.hpp:9) provides binary I/O:
template <typename LengthType = std::uint16_t>
class ByteStream {
public:
    // Writing
    void write_data(const void* ptr, const std::size_t size);
    template <typename T> void write(const T& value);
    void write(const std::string& str, const bool write_length_info = true);
    void write_vector(const std::vector<std::byte>& vec, const bool write_length_info = true);

    // Reading
    bool read_data(void* ptr, const std::size_t size);
    template <typename T> bool read(T& value);
    bool read(std::string& str, LengthType length = 0);
    bool read_vector(std::vector<std::byte>& vec, LengthType length = 0);

    // Utilities
    void skip(const std::size_t size);
    void backtrack(const std::size_t size);
    [[nodiscard]] std::size_t get_read_offset() const;
    [[nodiscard]] std::size_t get_size() const;
    [[nodiscard]] std::vector<std::byte> take_data();
};

ByteStream Example

utils::ByteStream stream;

// Write data
stream.write<uint32_t>(123);
stream.write("hello");              // Writes length prefix + string
stream.write("world", false);       // No length prefix
stream.write<float>(3.14f);

auto data = stream.take_data();

// Read data
utils::ByteStream read_stream{ data };
uint32_t value;
read_stream.read(value);            // 123

std::string str1;
read_stream.read(str1);             // "hello"

std::string str2;
read_stream.read(str2, 5);          // "world" (explicit length)

float pi;
read_stream.read(pi);               // 3.14f

Packet Flags

Game packets use bit flags for state (/home/daytona/workspace/source/src/packet/packet_types.hpp:68):
enum PacketFlag : uint32_t {
    PACKET_FLAG_NONE = 0,
    PACKET_FLAG_UNK = 1 << 1,
    PACKET_FLAG_RESET_VISUAL_STATE = 1 << 2,
    PACKET_FLAG_EXTENDED = 1 << 3,
    PACKET_FLAG_ROTATE_LEFT = 1 << 4,
    PACKET_FLAG_ON_SOLID = 1 << 5,
    PACKET_FLAG_ON_FIRE_DAMAGE = 1 << 6,
    PACKET_FLAG_ON_JUMP = 1 << 7,
    PACKET_FLAG_ON_KILLED = 1 << 8,
    PACKET_FLAG_ON_PUNCHED = 1 << 9,
    PACKET_FLAG_ON_PLACED = 1 << 10,
    PACKET_FLAG_ON_TILE_ACTION = 1 << 11,
    PACKET_FLAG_ON_GOT_PUNCHED = 1 << 12,
    PACKET_FLAG_ON_RESPAWNED = 1 << 13,
    PACKET_FLAG_ON_COLLECT_OBJECT = 1 << 14,
    PACKET_FLAG_ON_TRAMPOLINE = 1 << 15,
    PACKET_FLAG_ON_DAMAGE = 1 << 16,
    PACKET_FLAG_ON_SLIDE = 1 << 17,
    PACKET_FLAG_ON_WALL_HANG = 1 << 21,
    PACKET_FLAG_ON_ACID_DAMAGE = 1 << 26
};

Using Flags

GameUpdatePacket packet{};
packet.flags.extended = 1;
packet.flags.on_solid = 1;
packet.flags.on_jump = 1;

// Or using the value directly
packet.flags.value = PACKET_FLAG_EXTENDED | PACKET_FLAG_ON_SOLID;

Next Steps

Event System

Learn how packets trigger events

Architecture

Understand the overall system design

Packet Handling

Handle packets from Lua scripts

Development Guide

Define custom packet structures