Skip to content

Instantly share code, notes, and snippets.

@apple502j
Last active July 2, 2024 18:09
Show Gist options
  • Save apple502j/9c6b9e5e8dec37cbf6f3916472a79d57 to your computer and use it in GitHub Desktop.
Save apple502j/9c6b9e5e8dec37cbf6f3916472a79d57 to your computer and use it in GitHub Desktop.
Fabric API guide: Migrating to packet object-based API

Migrating to Packet Object-based Networking

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.

When the old API should still be used

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.

Design

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.

Packet types and packet classes

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 a static final field. This is used when registering a receiver.
  • Two method implementations: getType (which should return TYPE) and write.
  • 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.

Packet type instance

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.

Registering callbacks

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 via player.server.
  • ServerPlayNetworkHandler instance (handler) can be obtained via player.networkHandler.
  • ClientPlayNetworkHandler instance (handler) can be obtained via player.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

Thread safety

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.

Sending a packet

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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment