Skip to content

Networking

Networking in Minecraft is required so that client and server can communicate. Mod loaders already provide networking APIs, but DragonLib includes its own networking system built on top of vanilla Minecraft with the goal of being simple and consistent to use.

The following components are needed to transfer data.

Network Manager

To use the networking system you must create a DLNetworkManager. Usually one network manager is created per mod, but it is also possible to use multiple managers.

public static final DLNetworkManager MY_NETWORK_MANAGER = new DLNetworkManager(DLUtils.resourceLocation("my_mod_id", "network"), "v1");

A network manager uses a fixed ResourceLocation as its ID and also receives a protocol version.

!!! note The protocol version is used to compare versions when attempting to join a server.

Packet Data

Network packet data is a small class that contains all data to be transmitted, and handles serialization and deserialization. The NBT format is used as the transfer medium. A packet data class must extend NetworkPacketData.

public class MyNetworkPacketData extends NetworkPacketData {

    private UUID id;
    private String name;

    public MyNetworkPacketData(DLStatus status, UUID id, String name) {
        super(status);
        this.id = id;
        this.name = name;
    }

    @Override
    protected void write(CompoundTag nbt) {
        nbt.putUUID("Id", id);
        nbt.putString("Name", name);
    }

    @Override
    protected void read(CompoundTag nbt) {
        this.id = nbt.getUUID("Id");
        this.name = nbt.getString("Name");
    }
}

To simplify it, two constructors can be used: one for creating the class with data and one for creating an empty instance in the registration process.

public class MyNetworkPacketData extends NetworkPacketData {

    public MyNetworkPacketData(DLStatus status) {
        super(status);
    }

    public MyNetworkPacketData(UUID id, String name) {
        super(DLStatus.OK); // <-- Depends on whether you want to use this
        this.id = id;
        this.name = name;
    }

    // ...
}

Packet Type

DragonLib provides several ways to transfer data. While vanilla Minecraft only allows data to be sent from one sender to one receiver, DragonLib also supports requesting data from the other side, waiting for a response when sending data, or streaming data. The standard types are listed as inner classes in NetworkPacketType. The choice of type depends on the use case.

Name Description
Send Sends data from a sender to a receiver, where it is processed. No response is returned. (Vanilla behavior)
Receive Requests data from the other side, which will then send that data back.
Send and Receive Sends data from a sender to a receiver, which processes it and sends a response back to the sender, with or without data.
Stream Similar to Send and Receive, but sends smaller data chunks and only sends the next chunk after the other side responds.

!!! warn For very large amounts of data (e.g., files) you should always use Stream! With other types, all data is fully cached in memory, and a response is only sent once all data has arrived. With Stream, the data can be processed immediately, and RAM consumption remains low. This theoretically allows for the transmission of infinitely large amounts of data.

Packet Handler

A packet handler is basically a functional interface that takes the packet and processes it. For the different packet types there are specialized implementations, found as inner classes in NetworkProcessor.

Name Description
Send Receives the input packet and has no return value.
Receive Returns only the output packet.
Send and Receive Receives the input packet and returns the output packet.
StreamProvider Similar to Send and Receive with minor differences.
Output handle(Input packet, NetworkPacketContext context) {
    // ... do stuff ...
    return new Output(...);
}

!!! info Usually the network packet type determines which processor is needed. Custom network types, however, can be more flexible.

!!! tip For convenience, the handler can be implemented as a static method inside the input or output packet class to reduce the number of classes. For example:

public static class MyRequestPacket extends NetworkPacketData {

    // ...

    public static MyResponsePacket handle(MyRequestPacket packet, NetworkPacketContext context) {
        // ... do stuff ...
        return new ResponsePacket(...);
    }
}

Registering packets

To register a packet you need the created network manager instance. Use the appropriate register method that matches the required NetworkPacketType. As parameters, provide the packet name and the intended send direction (client -> server or server -> client). Then pass the handler(s) and supplier(s) to create empty data instances. It doesn't matter whether fields in the class are initialized, since the data will be populated during deserialization.

The return value is always an instance of the corresponding network packet type that should be stored for sending data later.

public static final NetworkPacketType.Send<NetworkDirection.C2S, MySendData> SEND_PACKET =
    NETWORK.registerSendOnly(
        "send",                 // The name of the packet
        NetworkDirection.C2S,   // The direction (Client to server or server to client)
        MySendData::handle,     // The packet handler
        MySendData::new         // The packet instance supplier
    );

public static final NetworkPacketType.Receive<NetworkDirection.C2S, MyResponseData> RECEIVE_PACKET =
    NETWORK.registerReceiveOnly(
        "receive",              // The name of the packet
        NetworkDirection.C2S,   // The direction (Client to server or server to client)
        MyResponseData::handle, // The packet handler
        MyResponseData::new     // The packet instance supplier
    );

public static final NetworkPacketType.SendAndReceive<NetworkDirection.C2S, MyNetworkPacket.Request, MyNetworkPacket.Response> SEND_AND_RECEIVE_PACKET =
    NETWORK.registerSendAndReceivePacket(
        "send_and_receive",             // The name of the packet
        NetworkDirection.C2S,           // The direction (Client to server or server to client)
        MyNetworkPacket::handle,        // The packet handler
        MyNetworkPacket.Request::new,   // The request packet instance supplier
        MyNetworkPacket.Response::new   // The response packet instance supplier
    );

Using a packet

Use the result of the registered packet and call its send method. The first parameter determines the destination, which is fixed according to the registered network direction (C2S or S2C). Several standard methods can be found in the NetworkDirection class. Here, toServer() is used because it's a C2S packet. For S2C, a player would need to be selected as the destination, e.g., toPlayer(serverPlayer).

SEND_PACKET.send(
    NetworkDirection.toServer(),    // The destination provider. Here it's simple, but players could be filtered by position, for example.
    new MyPacketData(...)           // The input data class
);

RECEIVE_PACKET.send(
    NetworkDirection.toServer(),    // The destination provider. Here it's simple, but players could be filtered by position, for example.
    (response) -> { /* response callback */ },
    () -> { /* error callback */ }
);

SEND_AND_RECEIVE_PACKET.send(
    NetworkDirection.toServer(),    // The destination provider. Here it's simple, but players could be filtered by position, for example.
    new MyPacketData(...),          // The input data class
    (response) -> { /* response callback */ },
    () -> { /* error callback */ }
);

!!! Warning Ensure that the packet is sent from the intended side. If your packet is registered as C2S, it should be sent from the client to the server, not from the dedicated server to players.

Custom implementations are also possible, which would then return either a C2S or S2C object.

Sequencing und size limits

In vanilla Minecraft, it's crucial keep an eye on payload and packet size limits, especially when transferring large amounts of data. Otherwise, the worst-case scenario is a failed transfer, resulting in server crashes or player kicks.

DragonLib handles this, allowing you to send any amount of data in one single pass without any concerns.

DragonLib internally divides the data and sends multiple chunks, which are then reassembled on the other end. Processing begins once all the data is transferred.

Status

In addition to the user-defined data, a status is transmitted, which can be retrieved during processing using data.getStatus(). For example, if an unhandled exception is thrown somewhere, an error status with details is returned as a substitute response. It is good practice to always check the status beforehand to avoid any unexpected errors.

Developers can also intentionally set a status by using the status variable when creating a data class. Usually DLStatus.OK will be used there.

public MyNetworkPacket(DLStatus status) {
    super(status);
}

!!! Note If an error status is transmitted in a response, this will trigger the error callback!

Tips

To reduce the number of classes—especially for transfers that have both input and output packets—it is recommended to group related parts as inner classes within a parent class.

public class RequestAndReceivePacket {

    public static class Request extends NetworkPacketData {
        public Request(DLStatus status) {
            super(status);
        }

        @Override
        protected void write(CompoundTag nbt) {
            // ...
        }

        @Override
        protected void read(CompoundTag nbt) {
            // ...
        }
    }

    public static class Response extends NetworkPacketData {
        public Response(DLStatus status) {
            super(status);
        }

        @Override
        protected void write(CompoundTag nbt) {
            // ...
        }

        @Override
        protected void read(CompoundTag nbt) {
            // ...
        }              
    }

    public static Response handle(Request packet, NetworkPacketContext context) {
        // ...
        return new Response(...);
    }    

}