Registering Payloads
Payloads are a way to send arbitrary data between the client and the server. They are registered using the PayloadRegistrar
from the RegisterPayloadHandlersEvent
event.
@SubscribeEvent
public static void register(final RegisterPayloadHandlersEvent event) {
// Sets the current network version
final PayloadRegistrar registrar = event.registrar("1");
}
Assuming we want to send the following data:
public record MyData(String name, int age) {}
Then we can implement the CustomPacketPayload
interface to create a payload that can be used to send and receive this data.
public record MyData(String name, int age) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<MyData> TYPE = new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath("mymod", "my_data"));
// Each pair of elements defines the stream codec of the element to encode/decode and the getter for the element to encode
// 'name' will be encoded and decoded as a string
// 'age' will be encoded and decoded as an integer
// The final parameter takes in the previous parameters in the order they are provided to construct the payload object
public static final StreamCodec<ByteBuf, MyData> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.STRING_UTF8,
MyData::name,
ByteBufCodecs.VAR_INT,
MyData::age,
MyData::new
)
@Override
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
As you can see from the example above the CustomPacketPayload
interface requires us to implement the type
method. The type
method is responsible for returning a unique identifier for this payload. We then also need a reader to register this later on with the StreamCodec
to read and write the payload data.
Finally, we can register this payload with the registrar:
@SubscribeEvent
public static void register(final RegisterPayloadHandlersEvent event) {
final PayloadRegistrar registrar = event.registrar("1");
registrar.playBidirectional(
MyData.TYPE,
MyData.STREAM_CODEC,
new DirectionalPayloadHandler<>(
ClientPayloadHandler::handleDataOnMain,
ServerPayloadHandler::handleDataOnMain
)
);
}
Dissecting the code above we can notice a couple of things:
- The registrar has
play*
methods, that can be used for registering payloads which are sent during the play phase of the game.- Not visible in this code are the methods
configuration*
andcommon*
; however, they can also be used to register payloads for the configuration phase. Thecommon
method can be used to register payloads for both the configuration and play phase simultaneously.
- Not visible in this code are the methods
- The registrar uses a
*Bidirectional
method, that can be used for registering payloads which are sent to both the logical server and logical client.- Not visible in this code are the methods
*ToClient
and*ToServer
; however, they can also be used to register payloads to only the logical client or only the logical server, respectively.
- Not visible in this code are the methods
- The type of the payload is used as a unique identifier for the payload.
- The stream codec is used to read and write the payload to and from the buffer sent across the network
- The payload handler is a callback for when the payload arrives on one of the logical sides.
- If a
*Bidirectional
method is used, aDirectionalPayloadHandler
can be used to provide two separate payload handlers for each of the logical sides.
- If a
Now that we have registered the payload we need to implement a handler. For this example we will specifically take a look at the client side handler, however the server side handler is very similar.
public class ClientPayloadHandler {
public static void handleDataOnMain(final MyData data, final IPayloadContext context) {
// Do something with the data, on the main thread
blah(data.age());
}
}
Here a couple of things are of note:
- The handling method here gets the payload, and a contextual object.
- The handling method of the payload is, by default, invoked on the main thread.
If you need to do some computation that is resource intensive, then the work should be done on the network thread, instead of blocking the main thread. This is done by setting the HandlerThread
of the PayloadRegistrar
to HandlerThread#NETWORK
via PayloadRegistrar#executesOn
before registering the payload.
@SubscribeEvent
public static void register(final RegisterPayloadHandlersEvent event) {
final PayloadRegistrar registrar = event.registrar("1")
.executesOn(HandlerThread.NETWORK); // All subsequent payloads will register on the network thread
registrar.playBidirectional(
MyData.TYPE,
MyData.STREAM_CODEC,
new DirectionalPayloadHandler<>(
ClientPayloadHandler::handleDataOnNetwork,
ServerPayloadHandler::handleDataOnNetwork
)
);
}
All payloads registered after an executesOn
call will retain the same thread execution location until executesOn
is called again.
PayloadRegistrar registrar = event.registrar("1");
registrar.playBidirectional(...); // On the main thread
registrar.playBidirectional(...); // On the main thread
// Configuration methods modify the state of the registrar
// by creating a new instance, so the change needs to be
/// updated by storing the result
registrar = registrar.executesOn(HandlerThread.NETWORK);
registrar.playBidirectional(...); // On the network thread
registrar.playBidirectional(...); // On the network thread
registrar = registrar.executesOn(HandlerThread.MAIN);
registrar.playBidirectional(...); // On the main thread
registrar.playBidirectional(...); // On the main thread
Here a couple of things are of note:
- If you want to run code on the main game thread you can use
enqueueWork
to submit a task to the main thread.- The method will return a
CompletableFuture
that will be completed on the main thread. - Notice: A
CompletableFuture
is returned, this means that you can chain multiple tasks together, and handle exceptions in a single place. - If you do not handle the exception in the
CompletableFuture
then it will be swallowed, and you will not be notified of it.
- The method will return a
public class ClientPayloadHandler {
public static void handleDataOnNetwork(final MyData data, final IPayloadContext context) {
// Do something with the data, on the network thread
blah(data.name());
// Do something with the data, on the main thread
context.enqueueWork(() -> {
blah(data.age());
})
.exceptionally(e -> {
// Handle exception
context.disconnect(Component.translatable("my_mod.networking.failed", e.getMessage()));
return null;
});
}
}
With your own payloads you can then use those to configure the client and server using Configuration Tasks.
Sending Payloads
CustomPacketPayload
s are sent across the network using vanilla's packet system by wrapping the payload via ServerboundCustomPayloadPacket
when sending to the server, or ClientboundCustomPayloadPacket
when sending to the client. Payloads sent to the client can only contain at most 1 MiB of data while payloads to the server can only contain less than 32 KiB.
All payloads are sent via Connection#send
with some level of abstraction; however, it is generally inconvenient to call these methods if you want to send packets to multiple people based on a given condition. Therefore, PacketDistributor
contains a number of convenience implementations to send payloads. There is only one method to send packets to the server (sendToServer
); however, there are numerous methods to send packets to the client depending on which players should receive the payload.
// ON THE CLIENT
// Send payload to server
PacketDistributor.sendToServer(new MyData(...));
// ON THE SERVER
// Send to one player (ServerPlayer serverPlayer)
PacketDistributor.sendToPlayer(serverPlayer, new MyData(...));
/// Send to all players tracking this chunk (ServerLevel serverLevel, ChunkPos chunkPos)
PacketDistributor.sendToPlayersTrackingChunk(serverLevel, chunkPos, new MyData(...));
/// Send to all connected players
PacketDistributor.sendToAllPlayers(new MyData(...));
See the PacketDistributor
class for more implementations.