From 5bb679c5698efcdbb1a065dfb51cc0e43836714b Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 31 Dec 2023 21:35:35 +0100 Subject: [PATCH 1/2] Add networking documentation --- docs/networking/configuration-tasks.md | 123 +++++++++++++++++++++++++ docs/networking/entities.md | 11 +-- docs/networking/index.md | 7 +- docs/networking/payload.md | 99 ++++++++++++++++++++ docs/networking/simpleimpl.md | 119 ------------------------ 5 files changed, 229 insertions(+), 130 deletions(-) create mode 100644 docs/networking/configuration-tasks.md create mode 100644 docs/networking/payload.md delete mode 100644 docs/networking/simpleimpl.md diff --git a/docs/networking/configuration-tasks.md b/docs/networking/configuration-tasks.md new file mode 100644 index 000000000..bdd24b1ed --- /dev/null +++ b/docs/networking/configuration-tasks.md @@ -0,0 +1,123 @@ +Using Configuration Tasks +==================== + +The networking protocol for the client and server has a specific phase where the server can configure the client before the player actually joins the game. +This phase is called the configuration phase, and is for example used by the vanilla server to send the resource pack information to the client. + +This phase can also be used by mods to configure the client before the player joins the game. + +## Registering a configuration task +The first step to using the configuration phase is to register a configuration task. +This can be done by registering a new configuration task in the `OnGameConfigurationEvent` event. +```java +@SubscribeEvent +public static void register(final OnGameConfigurationEvent event) { + event.register(new MyConfigurationTask()); +} +``` +The `OnGameConfigurationEvent` event is fired on the mod bus, and exposes the current listener used by the server to configure the relevant client. +A modder can use the exposed listener to figure out if the client is running the mod, and if so, register a configuration task. + +## Implementing a configuration task +A configuration task is a simple interface: `ICustomConfigurationTask`. +This interface has two methods: `void run(Consumer sender);`, and `ConfigurationTask.Type type();` which returns the type of the configuration task. +The type is used to identify the configuration task. +An example of a configuration task is shown below: +```java +public record MyConfigurationTask implements ICustomConfigurationTask { + public static final ConfigurationTask.Type TYPE = new ConfigurationTask.Type(new ResourceLocation("mymod:my_task")); + + @Override + public void run(final Consumer sender) { + final MyData payload = new MyData(); + sender.accept(payload); + } + + @Override + public ConfigurationTask.Type type() { + return TYPE; + } +} +``` + +## Acknowledging a configuration task +Your configuration is executed on the server, and the server needs to know when the next configuration task can be executed. +This is done by acknowledging the execution of said configuration task. + +There are two primary ways of achieving this: + +### Capturing the listener +When the client does not need to acknowledge the configuration task, then the listener can be captured, and the configuration task can be acknowledged directly on the server side. +```java +public record MyConfigurationTask(ServerConfigurationListener listener) implements ICustomConfigurationTask { + public static final ConfigurationTask.Type TYPE = new ConfigurationTask.Type(new ResourceLocation("mymod:my_task")); + + @Override + public void run(final Consumer sender) { + final MyData payload = new MyData(); + sender.accept(payload); + listener.finishCurrentTask(type()); + } + + @Override + public ConfigurationTask.Type type() { + return TYPE; + } +} +``` +To use such a configuration task, the listener needs to be captured in the `OnGameConfigurationEvent` event. +```java +@SubscribeEvent +public static void register(final OnGameConfigurationEvent event) { + event.register(new MyConfigurationTask(event.listener())); +} +``` +Then the next configuration task will be executed immediately after the current configuration task has completed, and the client does not need to acknowledge the configuration task. +Additionally, the server will not wait for the client to properly process the send payloads. + +### Acknowledging the configuration task +When the client needs to acknowledge the configuration task, then you will need to send your own payload to the client: +```java +public record AckPayload() implements CustomPacketPayload { + public static final ResourceLocation ID = new ResourceLocation("mymod:ack"); + + @Override + public void write(final FriendlyByteBuf buffer) { + // No data to write + } + + @Override + public ResourceLocation id() { + return ID; + } +} +``` +When a payload from a server side configuration task is properly processed you can send this payload to the server to acknowledge the configuration task. +```java +public void onMyData(MyData data, ConfigurationPayloadContext context) { + context.submitAsync(() -> { + blah(data.name()); + }) + .exceptionally(e -> { + // Handle exception + context.packetHandler().disconnect(Component.translatable("my_mod.configuration.failed", e.getMessage())); + return null; + }) + .thenAccept(v -> { + context.replyHandler().send(new AckPayload()); + }); +} +``` +Where `onMyData` is the handler for the payload that was sent by the server side configuration task. + +When the server receives this payload it will acknowledge the configuration task, and the next configuration task will be executed: +```java +public void onAck(AckPayload payload, ConfigurationPayloadContext context) { + context.taskCompletedHandler().onTaskCompleted(MyConfigurationTask.TYPE); +} +``` +Where `onAck` is the handler for the payload that was sent by the client. + +## Stalling the login process +When the configuration is not acknowledged, then the server will wait forever, and the client will never join the game. +So it is important to always acknowledge the configuration task, unless the configuration task failed, then you can disconnect the client. diff --git a/docs/networking/entities.md b/docs/networking/entities.md index 466b6cfcb..2f51b3e94 100644 --- a/docs/networking/entities.md +++ b/docs/networking/entities.md @@ -6,17 +6,14 @@ In addition to regular network messages, there are various other systems provide Spawn Data ---------- -In general, the spawning of modded entities is handled separately, by Forge. - -:::note -This means that simply extending a vanilla entity class may not inherit all its behavior. You may need to implement certain vanilla behaviors yourself. -::: +Since 1.20.2 Mojang introduced the concept of Bundle packets, which are used to send entity spawn packets together. +This allows for more data to be sent with the spawn packet, and for that data to be sent more efficiently. You can add extra data to the spawn packet Forge sends by implementing the following interface. -### IEntityAdditionalSpawnData - +### IEntityWithComplexSpawn If your entity has data that is needed on the client, but does not change over time, then it can be added to the entity spawn packet using this interface. `#writeSpawnData` and `#readSpawnData` control how the data should be encoded to/decoded from the network buffer. +Alternatively you can override the method `sendPairingData(...)` which is called when the entity is paired with a client. This method is called on the server, and can be used to send additional payloads to the client within the same bundle as the spawn packet. Dynamic Data ------------ diff --git a/docs/networking/index.md b/docs/networking/index.md index 9d88ae644..2d3a956fc 100644 --- a/docs/networking/index.md +++ b/docs/networking/index.md @@ -12,9 +12,8 @@ There are two primary goals in network communication: The most common way to accomplish these goals is to pass messages between the client and the server. These messages will usually be structured, containing data in a particular arrangement, for easy sending and receiving. -There are a variety of techniques provided by Forge to facilitate communication mostly built on top of [netty][]. - -The simplest, for a new mod, would be [SimpleImpl][channel], where most of the complexity of the netty system is abstracted away. It uses a message and handler style system. +There is a technique provided by Forge to facilitate communication mostly built on top of [netty][]. +This technique can be used by listening for the `RegisterPayloadHandlerEvent` event, and then registering a specific type of [payloads][], its reader, and its handler function to the registrar. [netty]: https://netty.io "Netty Website" -[channel]: ./simpleimpl.md "SimpleImpl in Detail" +[payloads]: ./payload.md "Registering custom Payloads" diff --git a/docs/networking/payload.md b/docs/networking/payload.md new file mode 100644 index 000000000..eb892f634 --- /dev/null +++ b/docs/networking/payload.md @@ -0,0 +1,99 @@ +Registering Payloads +==================== + +Payloads are a way to send arbitrary data between the client and the server. They are registered using the `IPayloadRegistrar` that can be retrieved for a given namespace from the `RegisterPayloadHandlerEvent` event. +```java +@SubscribeEvent +public static void register(final RegisterPacketHandlerEvent event) { + final IPayloadRegistrar registrar = event.registrar("mymod"); +} +``` + +Assuming we want to send the following data: +```java +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. +```java +public record MyData(String name, int age) implements CustomPacketPayload { + + public static final ResourceLocation ID = new ResourceLocation("mymod", "my_data"); + + public MyData(final FriendlyByteBuf buffer) { + this(buffer.readUtf(), buffer.readInt()); + } + + @Override + public void write(final FriendlyByteBuf buffer) { + buffer.writeUtf(name()); + buffer.writeInt(age()); + } + + @Override + public ResourceLocation id() { + return ID; + } +} +``` +As you can see from the example above the `CustomPacketPayload` interface requires us to implement the `write` and `id` methods. The `write` method is responsible for writing the data to the buffer, and the `id` method is responsible for returning a unique identifier for this payload. +We then also need a reader to register this later on, here we can use a custom constructor to read the data from the buffer. + +Finally, we can register this payload with the registrar: +```java +@SubscribeEvent +public static void register(final RegisterPacketHandlerEvent event) { + final IPayloadRegistrar registrar = event.registrar("mymod"); + registar.play(MyData.ID, MyData::new, handler -> handler + .client(ClientPayloadHandler.getInstance()::handleData) + .server(ServerPayloadHandler.getInstance()::handleData)); +} +``` +Dissecting the code above we can notice a couple of things: +- The registrar has a `play` method, that can be used for registering payloads which are send 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 constructor of `MyData` is used as a method reference to create a reader for the payload. +- The third argument for the registration method is a callback that can be used to register the handlers for when the payload arrives at either the client or server side. + - The `client` method is used to register a handler for when the payload arrives at the client side. + - The `server` method is used to register a handler for when the payload arrives at the server side. + - There is additionally a secondary registration method `play` on the registrar itself that accepts a handler for both the client and server side, this can be used to register a handler for both sides at once. + +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. +```java +public class ClientPayloadHandler { + + private static final ClientPayloadHandler INSTANCE = new ClientPayloadHandler(); + + public static ClientPayloadHandler getInstance() { + return INSTANCE; + } + + public void handleData(final MyData data, final PlayPayloadContext context) { + // Do something with the data, on the network thread + blah(data.name()); + + // Do something with the data, on the main thread + context.workHandler().submitAsync(() -> { + blah(data.age()); + }) + .exceptionally(e -> { + // Handle exception + context.packetHandler().disconnect(Component.translatable("my_mod.networking.failed", e.getMessage())); + return null; + }); + } +} +``` +Here a couple of things are of note: +- The handling method here gets the payload, and a contextual object. The contextual object is different for the play and configuration phase, and if you register a common packet, then it will need to accept the super type of both contexts. +- The handler of the payload method is invoked on the networking thread, so it is important to do all the heavy work here, instead of blocking the main game thread. +- If you want to run code on the main game thread you can use the `workHandler` of the context to submit a task to the main thread. + - The `workHandler` will return a `CompletableFuture` that will be completed on the main thread, and can be used to submit tasks to 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**. + +Now that you know how you can facilitate the communication between the client and the server for your mod, you can start implementing your own payloads. +With your own payloads you can then use those to configure the client and server using [Configuration Tasks][] + +[Configuration Tasks]: ./configuration-tasks.md diff --git a/docs/networking/simpleimpl.md b/docs/networking/simpleimpl.md deleted file mode 100644 index 88380f089..000000000 --- a/docs/networking/simpleimpl.md +++ /dev/null @@ -1,119 +0,0 @@ -SimpleImpl -========== - -SimpleImpl is the name given to the packet system that revolves around the `SimpleChannel` class. Using this system is by far the easiest way to send custom data between clients and the server. - -Getting Started ---------------- - -First you need to create your `SimpleChannel` object. We recommend that you do this in a separate class, possibly something like `ModidPacketHandler`. Create your `SimpleChannel` as a static field in this class, like so: - -```java -private static final String PROTOCOL_VERSION = "1"; -public static final SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel( - new ResourceLocation("mymodid", "main"), - () -> PROTOCOL_VERSION, - PROTOCOL_VERSION::equals, - PROTOCOL_VERSION::equals -); -``` - -The first argument is a name for the channel. The second argument is a `Supplier` returning the current network protocol version. The third and fourth arguments respectively are `Predicate` checking whether an incoming connection protocol version is network-compatible with the client or server, respectively. -Here, we simply compare with the `PROTOCOL_VERSION` field directly, meaning that the client and server `PROTOCOL_VERSION`s must always match or FML will deny login. - -The Version Checker -------------------- - -If your mod does not require the other side to have a specific network channel, or to be a Forge instance at all, you should take care that you properly define your version compatibility checkers (the `Predicate` parameters) to handle additional "meta-versions" (defined in `NetworkRegistry`) that can be received by the version checker. These are: - -* `ABSENT` - if this channel is missing on the other endpoint. Note that in this case, the endpoint is still a Forge endpoint, and may have other mods. -* `ACCEPTVANILLA` - if the endpoint is a vanilla (or non-Forge) endpoint. - -Returning `false` for both means that this channel must be present on the other endpoint. If you just copy the code above, this is what it does. Note that these values are also used during the list ping compatibility check, which is responsible for showing the green check / red cross in the multiplayer server select screen. - -Registering Packets -------------------- - -Next, we must declare the types of messages that we would like to send and receive. This is done using `INSTANCE#registerMessage`, which takes 5 parameters: - -- The first parameter is the discriminator for the packet. This is a per-channel unique ID for the packet. We recommend you use a local variable to hold the ID, and then call registerMessage using `id++`. This will guarantee 100% unique IDs. -- The second parameter is the actual packet class `MSG`. -- The third parameter is a `BiConsumer` responsible for encoding the message into the provided `FriendlyByteBuf`. -- The fourth parameter is a `Function` responsible for decoding the message from the provided `FriendlyByteBuf`. -- The final parameter is a `BiConsumer>` responsible for handling the message itself. - -The last three parameters can be method references to either static or instance methods in Java. Remember that an instance method `MSG#encode(FriendlyByteBuf)` still satisfies `BiConsumer`; the `MSG` simply becomes the implicit first argument. - -Handling Packets ----------------- - -There are a couple things to highlight in a packet handler. A packet handler has both the message object and the network context available to it. The context allows access to the player that sent the packet (if on the server), and a way to enqueue thread-safe work. - -```java -public static void handle(MyMessage msg, Supplier ctx) { - ctx.get().enqueueWork(() -> { - // Work that needs to be thread-safe (most work) - ServerPlayer sender = ctx.get().getSender(); // the client that sent this packet - // Do stuff - }); - ctx.get().setPacketHandled(true); -} -``` - -Packets sent from the server to the client should be handled in another class and wrapped via `DistExecutor#unsafeRunWhenOn`. - -```java -// In Packet class -public static void handle(MyClientMessage msg, Supplier ctx) { - ctx.get().enqueueWork(() -> - // Make sure it's only executed on the physical client - DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> ClientPacketHandlerClass.handlePacket(msg, ctx)) - ); - ctx.get().setPacketHandled(true); -} - -// In ClientPacketHandlerClass -public static void handlePacket(MyClientMessage msg, Supplier ctx) { - // Do stuff -} -``` - -Note the presence of `#setPacketHandled`, which is used to tell the network system that the packet has successfully completed handling. - -:::caution -As of Minecraft 1.8 packets are by default handled on the network thread. - -That means that your handler can _not_ interact with most game objects directly. Forge provides a convenient way to make your code execute on the main thread instead through the supplied `NetworkEvent$Context`. Simply call `NetworkEvent$Context#enqueueWork(Runnable)`, which will call the given `Runnable` on the main thread at the next opportunity. -::: - -:::caution -Be defensive when handling packets on the server. A client could attempt to exploit the packet handling by sending unexpected data. - -A common problem is vulnerability to **arbitrary chunk generation**. This typically happens when the server is trusting a block position sent by a client to access blocks and block entities. When accessing blocks and block entities in unloaded areas of the level, the server will either generate or load this area from disk, then promptly write it to disk. This can be exploited to cause **catastrophic damage** to a server's performance and storage space without leaving a trace. - -To avoid this problem, a general rule of thumb is to only access blocks and block entities if `Level#hasChunkAt` is true. -::: - -Sending Packets ---------------- - -### Sending to the Server - -There is but one way to send a packet to the server. This is because there is only ever *one* server the client can be connected to at once. To do so, we must again use that `SimpleChannel` that was defined earlier. Simply call `INSTANCE.sendToServer(new MyMessage())`. The message will be sent to the handler for its type, if one exists. - -### Sending to Clients - -Packets can be sent directly to a client using the `SimpleChannel`: `HANDLER.sendTo(new MyClientMessage(), serverPlayer.connection.getConnection(), NetworkDirection.PLAY_TO_CLIENT)`. However, this can be quite inconvenient. Forge has some convenience functions that can be used: - -```java -// Send to one player -INSTANCE.send(PacketDistributor.PLAYER.with(serverPlayer), new MyMessage()); - -// Send to all players tracking this level chunk -INSTANCE.send(PacketDistributor.TRACKING_CHUNK.with(levelChunk), new MyMessage()); - -// Send to all connected players -INSTANCE.send(PacketDistributor.ALL.noArg(), new MyMessage()); -``` - -There are additional `PacketDistributor` types available; check the documentation on the `PacketDistributor` class for more details. From e345f20673e57a44d03f98317bad47238dfc310e Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 31 Dec 2023 21:53:58 +0100 Subject: [PATCH 2/2] Address comments by IHH --- docs/networking/configuration-tasks.md | 3 +-- docs/networking/entities.md | 11 +++-------- docs/networking/index.md | 5 ++--- docs/networking/payload.md | 3 +-- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/docs/networking/configuration-tasks.md b/docs/networking/configuration-tasks.md index bdd24b1ed..1e94b3c5d 100644 --- a/docs/networking/configuration-tasks.md +++ b/docs/networking/configuration-tasks.md @@ -1,5 +1,4 @@ -Using Configuration Tasks -==================== +# Using Configuration Tasks The networking protocol for the client and server has a specific phase where the server can configure the client before the player actually joins the game. This phase is called the configuration phase, and is for example used by the vanilla server to send the resource pack information to the client. diff --git a/docs/networking/entities.md b/docs/networking/entities.md index 2f51b3e94..a63e958d1 100644 --- a/docs/networking/entities.md +++ b/docs/networking/entities.md @@ -1,11 +1,8 @@ -Entities -======== +# Entities In addition to regular network messages, there are various other systems provided to handle synchronizing entity data. -Spawn Data ----------- - +## Spawn Data Since 1.20.2 Mojang introduced the concept of Bundle packets, which are used to send entity spawn packets together. This allows for more data to be sent with the spawn packet, and for that data to be sent more efficiently. @@ -15,9 +12,7 @@ You can add extra data to the spawn packet Forge sends by implementing the follo If your entity has data that is needed on the client, but does not change over time, then it can be added to the entity spawn packet using this interface. `#writeSpawnData` and `#readSpawnData` control how the data should be encoded to/decoded from the network buffer. Alternatively you can override the method `sendPairingData(...)` which is called when the entity is paired with a client. This method is called on the server, and can be used to send additional payloads to the client within the same bundle as the spawn packet. -Dynamic Data ------------- - +## Dynamic Data ### Data Parameters This is the main vanilla system for synchronizing entity data from the server to the client. As such, a number of vanilla examples are available to refer to. diff --git a/docs/networking/index.md b/docs/networking/index.md index 2d3a956fc..d11abe4bd 100644 --- a/docs/networking/index.md +++ b/docs/networking/index.md @@ -1,5 +1,4 @@ -Networking -========== +# Networking Communication between servers and clients is the backbone of a successful mod implementation. @@ -12,7 +11,7 @@ There are two primary goals in network communication: The most common way to accomplish these goals is to pass messages between the client and the server. These messages will usually be structured, containing data in a particular arrangement, for easy sending and receiving. -There is a technique provided by Forge to facilitate communication mostly built on top of [netty][]. +There is a technique provided by NeoForge to facilitate communication mostly built on top of [netty][]. This technique can be used by listening for the `RegisterPayloadHandlerEvent` event, and then registering a specific type of [payloads][], its reader, and its handler function to the registrar. [netty]: https://netty.io "Netty Website" diff --git a/docs/networking/payload.md b/docs/networking/payload.md index eb892f634..f203c733b 100644 --- a/docs/networking/payload.md +++ b/docs/networking/payload.md @@ -1,5 +1,4 @@ -Registering Payloads -==================== +# Registering Payloads Payloads are a way to send arbitrary data between the client and the server. They are registered using the `IPayloadRegistrar` that can be retrieved for a given namespace from the `RegisterPayloadHandlerEvent` event. ```java