Fabric API 0.77.0 for 1.19.4 and 1.20 introduced a new way of writing networking stack. This applies to ServerPlayNetworking
and ClientPlayNetworking
. This is a guide on migrating existing code using Networking API v1 to new, packet-based code.
If you are still using the deprecated Networking API v0, it is recommended to switch to v1 first.
The new API makes some assumptions on the usage. While this covers most of the common and expected uses, there are cases where the old API still works better. Therefore, the old API is not deprecated in anyway. Also, the two APIs are network-compatible so long as the packet serialization is the same.
For example, you might want to continue using the old API if:
- Directly writing large amounts of data to
PacketByteBuf
in a complex way (such as Registry Sync) - Reading such data from
PacketByteBuf
- Slicing a
PacketByteBuf
into multiple buffers - API that allows users to provide a raw
PacketByteBuf
- Handling certain logics in the network thread for some reason
- A very performance-critical code, where allocating packet is not a good option
- Dynamic creation of channels
However, it's likely that your use is not one of those.
The packet-based API minics the Minecraft's packet system. However there are key differences:
- There is no need to define a
PacketListener
interface and implementing class. A functional interface can be used as a receiver. - The same packet can be used as both C2S (serverbound) and S2C (clientbound).
The API differs from the previous networking API in that:
- It is thread safe by default.
- It uses a packet object instead of writing directly to
PacketByteBuf
. - It encourages modders to write packet reading and writing in the same class, reducing bugs from inconsistent ordering.
A packet class represents a packet. This is similar to vanilla packets:
public class PlayRickrollPacket implements FabricPacket {
public static final PacketType<PlayRickrollPacket> TYPE = PacketType.create(
new Identifier(MODID, "rickroll"),
PlayRickrollPacket::new
);
public final BlockPos pos;
public final int startSeconds;
public PlayRickrollPacket(BlockPos pos, int startSeconds) {
this.pos = pos;
this.startSeconds = startSeconds;
}
public PlayRickrollPacket(PacketByteBuf buf) {
this(buf.readBlockPos(), buf.readVarInt());
}
@Override
public PacketType<?> getType() {
return TYPE;
@Override
public void write(PacketByteBuf buf) {
buf.writeBlockPos(this.pos);
buf.writeVarInt(this.startSeconds);
}
}
As seen above, a packet class should have the following:
- Fields representing the packet's contents. For readability, they should be in the form and order they are serialized.
- A constructor that constructs the packet from the contents.
- A constructor that reads the buffer.
- An instance of
PacketType
stored in astatic final
field. This is used when registering a receiver. - Two method implementations:
getType
(which should returnTYPE
) andwrite
. - Getters of the fields, if not using a public field.
A packet class should be immutable. Modders might want to use a record to reduce code duplication.
A packet type instance has two parts: the channel ID (which was previously passed to send
and registerGlobalReceiver
methods) and the constructor that takes PacketByteBuf
. This instance can be stored anywhere, although it is recommended to store it in the same class as the packet.
A packet type class should never be constructed at runtime.
To register a callback, first replace the channel ID with the packet type:
- registerGlobalReceiver(new Identifier(MODID, "rickroll"), NetworkHandler::onPlayRickroll);
+ registerGlobalReceiver(PlayRickrollPacket.TYPE, NetworkHandler::onPlayRickroll);
Those who implement the interface in a class instead of using a functional interface should change implements PlayChannelHandler
to implements PlayPacketHandler
.
The passed parameters have changed to be more consistent and reduce redundant ones. For C2S packets, they are now ServerPlayerEntity, T, PacketSender
where T
is the type of the packet object. For S2C packets, they are now MinecraftClient, ClientPlayerEntity, T, PacketSender
.
An example of C2S packet changes:
- registerGlobalReceiver(new Identifier(MODID, "start_dancing"), (server, player, handler, buf, sender) -> {});
+ registerGlobalReceiver(new Identifier(MODID, "start_dancing"), (player, packet, sender) -> {});
An example of S2C packet changes:
- registerGlobalReceiver(new Identifier(MODID, "rickroll"), (client, handler, buf, sender) -> {});
+ registerGlobalReceiver(new Identifier(MODID, "rickroll"), (client, player, packet, sender) -> {});
A few notes:
MinecraftServer
instance (server
) can be obtained viaplayer.server
.ServerPlayNetworkHandler
instance (handler
) can be obtained viaplayer.networkHandler
.ClientPlayNetworkHandler
instance (handler
) can be obtained viaplayer.networkHandler
.
As noted above, the new system makes the mods read the packet buffer in the packet object. Therefore, the contents of the received packet should be read from the fields (or getter) of packet
.
- int range = buf.readVarInt();
+ int range = packet.range; // or packet.getRange() if you prefer getters
The callback now by default runs in the main thread - the server thread for C2S packets, and the render thread for S2C packets. Therefore, server.execute
or client.execute
can be safely removed.
- server.execute(() -> player.world.spawnEntity(createEntity(player, color)));
+ player.world.spawnEntity(createEntity(player, color));
A few notes: in an extremely rare case, the function previously might have ran after the player has disconnected from the game. To align with the vanilla code of NetworkThreadUtils
, the function no longer runs if the player is disconnected at the time the function is processed in the main thread.
To send a packet, use the send
methods in ClientPlayNetworking
, ServerPlayNetworking
, or PacketSender
. A packet object must first be constructed; then, the packet object is passed to the method. There is no need to specify the packet type, as it is provided from FabricPacket#getType
.
PlayRickrollPacket packet = new PlayRickrollPacket(player.getPos(), 0);
ServerPlayNetworking.send(player, packet);