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.
GTProxy uses an event-driven architecture powered by the eventpp library. Events enable decoupled communication between components, allowing handlers and Lua scripts to react to network activity without tight coupling.
Event Architecture
The event system follows the observer pattern with priority support:
Event Source → Dispatcher.dispatch(event) → Listeners (by priority)
↓
[Highest Priority]
↓
[Normal Priority]
↓
[Lowest Priority]
Each listener can cancel an event, preventing lower-priority listeners from executing.
Event Dispatcher
The event::Dispatcher (/home/daytona/workspace/source/src/event/event.hpp:231) is GTProxy’s central event hub:
using Dispatcher = PriorityEventDispatcher ;
class PriorityEventDispatcher {
public:
using Handle = BaseDispatcher :: Handle ;
using Callback = std :: function < void ( const Event & )>;
Handle appendListener ( const Type event , const Callback & callback ,
const int8_t priority = Priority :: Normal );
Handle prependListener ( const Type event , const Callback & callback );
bool removeListener ( Type event , const Handle & handle );
void dispatch ( Type event , const Event & e ) const ;
void dispatch ( const Event & e ) const ;
};
Priority Levels
Priorities control listener execution order (/home/daytona/workspace/source/src/event/event.hpp:19):
struct Priority {
static constexpr int8_t Highest = std :: numeric_limits < int8_t >:: min (); // -128
static constexpr int8_t FairlyHigh = Highest / 2 ; // -64
static constexpr int8_t Normal = 0 ;
static constexpr int8_t FairlyLow = std :: numeric_limits < int8_t >:: max () / 2 ; // 63
static constexpr int8_t Lowest = std :: numeric_limits < int8_t >:: max (); // 127
};
Lower numeric values mean higher priority. Highest (-128) executes before Normal (0), which executes before Lowest (127).
Dispatcher Implementation
The dispatcher maintains a priority queue for each event type (/home/daytona/workspace/source/src/event/event.hpp:176):
Handle appendListener ( const Type event , const Callback & callback , const int8_t priority = Priority :: Normal )
{
auto & entries = handles_ [event];
// Insert in priority order
for ( auto it = entries . begin (); it != entries . end (); ++ it) {
if ( it -> priority > priority) {
auto handle = dispatcher_ . insertListener (event, callback, it -> handle );
entries . insert (it, {priority, handle});
return handle;
}
}
// Append at end if lowest priority
auto handle = dispatcher_ . appendListener (event, callback);
entries . push_back ({ priority, handle });
return handle;
}
Event Types
Core event types are defined in /home/daytona/workspace/source/src/event/event.hpp:27:
enum class Type : uint32_t {
ClientConnect = 0 ,
ServerConnect ,
ClientDisconnect ,
ServerDisconnect ,
ClientBoundPacket ,
ServerBoundPacket ,
PacketEventOffset = 0x 1000 ,
Max
};
Event Categories
Connection Events
Packet Events
Typed Packet Events
ClientConnect : Fired when the Growtopia client connects to GTProxy’s serverServerConnect : Fired when GTProxy successfully connects to the Growtopia serverClientDisconnect : Fired when the Growtopia client disconnectsServerDisconnect : Fired when GTProxy loses connection to the Growtopia serverdispatcher . appendListener ( event :: Type ::ClientConnect,
[]( const event :: Event & e ) {
spdlog :: info ( "Client connected!" );
});
ClientBoundPacket : Raw packet data heading to the client (server → client)ServerBoundPacket : Raw packet data heading to the server (client → server)dispatcher . appendListener ( event :: Type ::ClientBoundPacket,
[]( const event :: Event & e ) {
const auto & packet_event = static_cast < const event ::RawPacketEvent &> (e);
spdlog :: info ( "Packet to client: {} bytes" , packet_event . data . size ());
});
Packet-specific events use IDs starting at PacketEventOffset (0x1000): // Calculated as: PacketEventOffset + PacketId
constexpr event :: Type packet_event_type ( const packet :: PacketId id ) {
return static_cast < event ::Type > ( 0x 1000 + static_cast < uint32_t > (id));
}
Example: // Listen for OnSpawn packets
auto spawn_event_type = event :: packet_event_type ( packet :: PacketId ::OnSpawn);
dispatcher . appendListener (spawn_event_type,
[]( const event :: Event & e ) {
const auto & spawn_event = static_cast < const event ::TypedPacketEvent < packet :: PacketId ::OnSpawn >&> (e);
// Handle spawn...
});
Event Direction
Packet events include direction information (/home/daytona/workspace/source/src/event/event.hpp:41):
enum class Direction {
ClientBound , // Server → Client
ServerBound , // Client → Server
};
Event Base Classes
All events inherit from the base Event class (/home/daytona/workspace/source/src/event/event.hpp:62):
struct Event {
Type type;
mutable bool canceled;
explicit Event ( const Type t )
: type { t }
, canceled { false }
{ }
virtual ~Event () = default ;
void cancel () const { canceled = true ; }
};
Event Cancellation
Listeners can cancel events to prevent:
Lower-priority listeners from executing
Packets from being forwarded
Default behavior from occurring
dispatcher . appendListener ( event :: Type ::ClientBoundPacket,
[]( const event :: Event & e ) {
e . cancel (); // Block this packet from being forwarded
}, event :: Priority ::Highest);
Event cancellation only affects subsequent listeners. Higher-priority listeners have already executed and cannot be “undone.”
Event Subtypes
ConnectionEvent
struct ConnectionEvent : Event {
explicit ConnectionEvent ( const Type t ) : Event { t } { }
};
Used for ClientConnect, ServerConnect, ClientDisconnect, ServerDisconnect.
RawPacketEvent
struct RawPacketEvent : Event {
std ::span < const std ::byte > data;
RawPacketEvent ( const Type t , std :: span < const std :: byte > d )
: Event { t }
, data { d }
{ }
};
Fired for ClientBoundPacket and ServerBoundPacket with raw byte data before decoding.
PacketEvent
struct PacketEvent : Event {
packet ::PacketId packet_id;
std ::shared_ptr < packet ::IPacket > packet;
PacketEvent ( const Type t , std :: shared_ptr < packet :: IPacket > pkt )
: Event { t }
, packet_id { pkt -> id () }
, packet { std :: move (pkt) }
{ }
[[ nodiscard ]] bool has_packet () const { return packet != nullptr ; }
template < typename T >
[[ nodiscard ]] std :: shared_ptr < T > get () const {
if (packet && packet -> id () == T ::ID) {
return std :: static_pointer_cast < T >(packet);
}
return nullptr ;
}
template < typename T >
[[ nodiscard ]] bool is () const {
return packet && packet -> id () == T ::ID;
}
};
Fired after raw packets are decoded into structured packet objects.
TypedPacketEvent
For specific packet types (/home/daytona/workspace/source/src/event/event.hpp:122):
template < packet :: PacketId PacketTypeId >
struct TypedPacketEvent : Event {
Direction direction;
std ::shared_ptr < packet ::IPacket > packet;
TypedPacketEvent ( Direction dir , std :: shared_ptr < packet :: IPacket > pkt )
: Event { packet_event_type (PacketTypeId) }
, direction { dir }
, packet { std :: move (pkt) }
{ }
[[ nodiscard ]] Direction get_direction () const { return direction; }
[[ nodiscard ]] bool is_client_bound () const { return direction == Direction ::ClientBound; }
[[ nodiscard ]] bool is_server_bound () const { return direction == Direction ::ServerBound; }
template < typename T >
[[ nodiscard ]] std :: shared_ptr < T > get () const {
if (packet && packet -> id () == T ::ID) {
return std :: static_pointer_cast < T >(packet);
}
return nullptr ;
}
};
Event Policies
The dispatcher uses custom policies (/home/daytona/workspace/source/src/event/event.hpp:160):
struct EventPolicies {
static Type getEvent ( const Event & e ) { return e . type ; }
static bool canContinueInvoking ( const Event & e ) { return ! e . canceled ; }
};
using BaseDispatcher = eventpp ::EventDispatcher <
Type,
void ( const Event & ),
EventPolicies
> ;
getEvent : Extracts event type from event object
canContinueInvoking : Stops propagation if event is canceled
Scoped Handles
The ScopedHandle class (/home/daytona/workspace/source/src/event/event.hpp:233) provides RAII for event listener lifetime:
class ScopedHandle {
public:
ScopedHandle ( Dispatcher & d , const Type t , const Dispatcher :: Handle h );
~ScopedHandle () { reset (); }
void reset (); // Manually remove listener
// Move-only (non-copyable)
ScopedHandle ( ScopedHandle && other ) noexcept ;
ScopedHandle & operator = ( ScopedHandle && other ) noexcept ;
};
Usage Example
void MyHandler :: setup () {
// Store handle as member variable
handle_ = event :: ScopedHandle (
dispatcher_,
event :: Type ::ClientConnect,
dispatcher_ . appendListener ( event :: Type ::ClientConnect,
[ this ]( const event :: Event & e ) {
on_client_connect (e);
})
);
}
// Handle automatically unregisters when MyHandler is destroyed
Using ScopedHandle prevents listener leaks and ensures cleanup when objects are destroyed.
Registering Event Listeners
C++ Listeners
class MyHandler {
public:
MyHandler ( event :: Dispatcher & dispatcher )
: dispatcher_ { dispatcher }
{
setup_listeners ();
}
private:
void setup_listeners () {
// Listen to client connection
handles_ . emplace_back (
dispatcher_,
event :: Type ::ClientConnect,
dispatcher_ . appendListener ( event :: Type ::ClientConnect,
[ this ]( const event :: Event & e ) {
spdlog :: info ( "Client connected" );
})
);
// Listen to specific packet type
auto spawn_type = event :: packet_event_type ( packet :: PacketId ::OnSpawn);
handles_ . emplace_back (
dispatcher_,
spawn_type,
dispatcher_ . appendListener (spawn_type,
[ this ]( const event :: Event & e ) {
const auto & spawn = static_cast < const event ::TypedPacketEvent < packet :: PacketId ::OnSpawn >&> (e);
if ( spawn . is_client_bound ()) {
handle_spawn (spawn);
}
},
event :: Priority ::FairlyHigh)
);
}
void handle_spawn ( const event :: TypedPacketEvent < packet :: PacketId :: OnSpawn > & e ) {
// Process spawn event
}
private:
event ::Dispatcher & dispatcher_;
std ::vector < event ::ScopedHandle > handles_;
};
Lua Listeners
Lua scripts register listeners through the ScriptEventBridge:
-- Listen to connection event
events . on ( "ClientConnect" , function ()
print ( "Client connected!" )
end )
-- Listen to packet by name
events . on_packet ( "OnSpawn" , function ( direction , packet )
if direction == "ClientBound" then
print ( "Spawn packet going to client" )
end
end )
-- Cancel packet
events . on_packet ( "OnRemove" , function ( direction , packet )
packet : cancel () -- Block this packet
return true -- Or return true to cancel
end , { priority = "Highest" })
See Event Handling in Lua for complete Lua API documentation.
Packet Event Registration
Packet events are registered via the PacketEventRegistry (/home/daytona/workspace/source/src/packet/packet_event_registry.hpp:17):
namespace packet :: event_registry {
using PacketEventBuilder = std ::function < std :: shared_ptr < event :: Event >(
event ::PriorityEventDispatcher & ,
event ::Direction,
std ::shared_ptr < IPacket >
) > ;
class PacketEventRegistry {
public:
void register_event ( PacketId id , PacketEventBuilder builder );
[[ nodiscard ]] std :: shared_ptr < event :: Event > emit (
event :: PriorityEventDispatcher & dispatcher ,
const event :: Direction direction ,
const std :: shared_ptr < IPacket > & packet
) const ;
};
template < typename PacketType , PacketId PacketTypeId >
PacketEventBuilder make_event_builder () {
return []( event :: PriorityEventDispatcher & dispatcher ,
const event :: Direction direction ,
const std :: shared_ptr < IPacket > & packet ) -> std::shared_ptr<event::Event>
{
auto typed_packet = std :: static_pointer_cast < PacketType >(packet);
auto evt = std :: make_shared < event :: TypedPacketEvent < PacketTypeId >>(
direction,
std :: move (typed_packet)
);
dispatcher . dispatch ( * evt);
return evt;
};
}
}
Registering Packet Events
In register_packet_events.hpp:
auto & registry = packet :: event_registry :: PacketEventRegistry :: instance ();
registry . register_event (
packet :: PacketId ::OnSpawn,
packet :: event_registry ::make_event_builder <
packet :: game ::OnSpawn,
packet :: PacketId ::OnSpawn
> ()
);
Event Flow Example
Here’s the complete event flow when a packet arrives:
Network layer receives raw bytes
Dispatches RawPacketEvent (client/server bound)
Decoder parses bytes into structured packet
Registry instantiates packet object
Dispatches TypedPacketEvent for specific packet type
Listeners execute in priority order
If not canceled, ForwardingHandler forwards packet
Event Timing
Events are dispatched synchronously on the network thread. Long-running event handlers will block packet processing. For expensive operations, use the scheduler: dispatcher_ . appendListener ( event :: Type ::ClientConnect,
[ this , & scheduler ]( const event :: Event & e ) {
scheduler . schedule_immediate ([ this ]() {
// Heavy work on worker thread
process_connection ();
});
});
Common Patterns
Packet Inspection
// Log all outgoing packets
dispatcher . appendListener ( event :: Type ::ClientBoundPacket,
[]( const event :: Event & e ) {
const auto & pkt = static_cast < const event ::RawPacketEvent &> (e);
spdlog :: info ( "→ Client: {} bytes" , pkt . data . size ());
});
Packet Modification
// Modify spawn packets
auto spawn_type = event :: packet_event_type ( packet :: PacketId ::OnSpawn);
dispatcher . appendListener (spawn_type,
[]( const event :: Event & e ) {
const auto & spawn_event = static_cast < const event ::TypedPacketEvent < packet :: PacketId ::OnSpawn >&> (e);
if ( auto spawn_pkt = spawn_event . get < packet :: game ::OnSpawn > ()) {
// Modify spawn data
spawn_pkt -> modify_field ( "customValue" );
}
});
Packet Blocking
// Block all disconnect packets
auto disconnect_type = event :: packet_event_type ( packet :: PacketId ::Disconnect);
dispatcher . appendListener (disconnect_type,
[]( const event :: Event & e ) {
e . cancel (); // Prevent disconnect
}, event :: Priority ::Highest);
Conditional Handling
// Only process client-bound spawn packets
auto spawn_type = event :: packet_event_type ( packet :: PacketId ::OnSpawn);
dispatcher . appendListener (spawn_type,
[]( const event :: Event & e ) {
const auto & spawn = static_cast < const event ::TypedPacketEvent < packet :: PacketId ::OnSpawn >&> (e);
if ( ! spawn . is_client_bound ()) {
return ; // Ignore server-bound spawns
}
// Process client-bound spawn
});
Best Practices
Use Appropriate Priorities
Highest : Security checks, critical validation
Normal : Most handlers and Lua scripts
Lowest : Logging, analytics, non-critical processing
Higher priority = earlier execution = more control
Always use ScopedHandle or store handles as members: class MyHandler {
std ::vector < event ::ScopedHandle > handles_; // ✓ Good
};
Never let handles go out of scope: void bad_example () {
auto handle = dispatcher . appendListener (...); // ✗ Bad - leaks!
}
Avoid Blocking Operations
Event handlers run on the network thread. Long operations block packet flow: // ✗ Bad - blocks network thread
dispatcher . appendListener ( event :: Type ::ClientConnect,
[]( const event :: Event & e ) {
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 5 ));
});
// ✓ Good - schedules on worker thread
dispatcher . appendListener ( event :: Type ::ClientConnect,
[ & scheduler ]( const event :: Event & e ) {
scheduler . schedule_immediate ([]() {
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 5 ));
});
});
Canceling events prevents default behavior:
Canceled RawPacketEvent → packet not decoded
Canceled TypedPacketEvent → packet not forwarded
Canceled connection events → connection may be aborted
Always verify the impact before canceling.
Next Steps
Architecture Overview See how events fit in the overall system
Packet System Learn about packet structures and decoding
Event Handling (Lua) Handle events from Lua scripts
Development Guide Build custom C++ event handlers