Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ZTzTopia/GTProxy/llms.txt

Use this file to discover all available pages before exploring further.

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

Build docs developers (and LLMs) love