Skip to main content
Version: 1.21.6 - 1.21.7

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 // on the mod event bus
public static void register(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:

// In some common event class

@SubscribeEvent // on the mod event bus
public static void register(RegisterPayloadHandlersEvent event) {
final PayloadRegistrar registrar = event.registrar("1");
registrar.playBidirectional(
MyData.TYPE,
MyData.STREAM_CODEC,
ServerPayloadHandler::handleDataOnMain
);
}

// In some client-only event class

@SubscribeEvent // on the mod event bus only on the physical client
public static void register(RegisterClientPayloadHandlersEvent event) {
event.register(
MyData.TYPE,
ClientPayloadHandler::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* and common*; however, they can also be used to register payloads for the configuration phase. The common method can be used to register payloads for both the configuration and play phase simultaneously.
  • 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.
  • 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 *ToServer method is used, the payload handler will be the last parameter in the method.
    • If a *ToClient method is used, the payload handler will need to be registered via RegisterClientPayloadHandlersEvent, passing in the payload type and the handler.
    • If a *Bidirectional method is used, a payload handler will need to use both.
note

The client payload handler has its own event RegisterClientPayloadHandlersEvent to protect against code reaching across both logical and physical sides.

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 for serverbound connections, and by passing in the HandlerThread to RegisterClientPayloadHandlersEvent#register for clientbound connections.

// In some common event class

@SubscribeEvent // on the mod event bus
public static void register(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,
ServerPayloadHandler::handleDataOnNetwork
);
}

// In some client-only event class

@SubscribeEvent // on the mod event bus only on the physical client
public static void register(RegisterClientPayloadHandlersEvent event) {
event.register(
MyData.TYPE,
HandlerThread.NETWORK // Payload handler will be invoked on the network thread
ClientPayloadHandler::handleDataOnNetwork
);
}
note

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.
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

CustomPacketPayloads 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 to the client, and ClientPacketDistributor contains one method to send packets to the server (sendToServer).

// ON THE CLIENT

// Send payload to server
ClientPacketDistributor.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 and ClientPacketDistributor classes for more implementations.