Sending Packets
Sending packets to clients isn’t as trivial as it is with other Minecraft plugin APIs:
const b, const size = try ctx.encode( protocol.ClientPlayChatMessage.{ // here we write plain JSON, but we could've used // a formatted Chat struct .json = "{\"text\":\"hello, world!\"}", .position = .chat, }, .@"6", // the buffer pool we want to use, more on this later);ctx.prepareOneshot(client.fd, b, size);In this snippet, ctx.encode will acquire a buffer from the pool we
requested and encode the packet we provided onto it.
It will then return the acquired buffer, and the total number
of bytes written.
We can pass this buffer to the server thread with ctx.prepareOneshot.
It will send it to the specified client.
Performance
Section titled “Performance”Under the hood, the graphite server thread uses io_uring to batch syscalls massively and reduce latency:
...ctx.prepareBroadcast(b, size);Using prepareBroadcast would ask the server thread to broadcast
the packet you provided to all connected players. To achieve this,
instead of using one syscall for each player, the server thread
would batch requests into the SQE ring, then emit one syscall to
submit the batched requests to the kernel.
This should significantly reduce latency, especially if
your server is running under pressure.
The verbose way
Section titled “The verbose way”Sometimes you might want to use the lower-level way to encode packets:
// acquire a buffer from the pool.// there are 4 different pools, each with differently-sized buffers.// this will give us a buffer holding 2^6 = 64 bytes// that's enough bytes to encode a hello world message.const b = try ctx.buffer_pools.allocBuf(.@"6");
// encode the packet onto the bufferconst size = try protocol.ClientPlayChatMessage.encode( &.{ .json = "{\"text\":\"hello, world!\"}", .position = .chat, }, b.ptr,) catch { // if this operation fails we want to release the buffer back to the pool // so it can be used again. // you may handle the encoding failure however you'd like, but you need to release the buffer ctx.buffer_pools.releaseBuf(b.idx); return;};
// this notifies the server thread that a packet needs to be sent.ctx.prepareOneshot(client.fd, b, size);
// from this point onward we don't touch the buffer// the server thread temporarily owns it// it'll notify the game thread again when it can be releasedThis is useful when you need to encode multiple packets onto the same, single buffer:
const b = try ctx.buffer_pools.allocBuf(.@"10");
var offset: usize = 0;offset += try protocol.ClientPlayChatMessage.encode( &.{ .json = "{\"text\":\"haiii!\"}", .position = .chat, }, b.ptr,) catch { ctx.buffer_pools.releaseBuf(b.idx); return;};offset += protocol.ClientPlaySoundEffect.encode( &.{ .sound_name = "note.harp", .x = 0, .y = 0, .z = 0, .volume = 100.0, .pitch = common.pitchFromMidi(69), }, b.ptr,) catch { ctx.buffer_pools.releaseBuf(b.idx); return;};
// broadcast two packets to each playerctx.prepareBroadcast(b, size);Buffer pools
Section titled “Buffer pools”Right now, there are 4 buffer pools, each with differently-sized buffers (i.e, the max number of bytes you can write onto them). Here’s the capacity of each buffer pool:
6:2^6 = 6410:2^10 = 102414:2^14 = 1638418:2^18 = 262144
This system bypasses the need to allocate new data from the heap every time you want to encode a packet.
const b = try ctx.buffer_pools.allocBuf(.@"18");// you now temporarily own a big buffer!// better write some stuff in it