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
NET_MESSAGE_GENERIC_TEXT / NET_MESSAGE_GAME_MESSAGE
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
Read Message Type : First 4 bytes indicate the NetMessageType
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
Check for Variant : If packet type is PACKET_CALL_FUNCTION, deserialize variant
Create Packet : Use PacketRegistry to instantiate appropriate packet class
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 \n text|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.0 f , 200.0 f ) // Position (index 3)
);
// Access values
std ::string func_name = spawn_variant . get < std ::string > ( 0 );
glm ::vec2 pos = spawn_variant . get < glm ::vec2 > ( 3 );
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 = 0x 1000 ,
Quit ,
QuitToExit ,
JoinRequest ,
ValidateWorld ,
Input ,
Log ,
OnNameChanged ,
OnChangeSkin ,
Padding2 = 0x 2000 ,
Disconnect ,
TileChangeRequest ,
SendMapData ,
SendTileUpdateData ,
SendItemDatabaseData ,
SendInventoryState ,
ModifyItemInventory ,
ItemChangeObject ,
Padding3 = 0x 3000 ,
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 > ( 0x 00 )); // 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.14 f );
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