Views
Actions
Difference between revisions of "SimpleChannel"
m (changed name of EntityPlayerMP to ServerPlayerEntity) |
Ferri Arnus (talk | contribs) |
||
(5 intermediate revisions by 3 users not shown) | |||
Line 31: | Line 31: | ||
* 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 <code><nowiki>id++</nowiki></code>. This will guarantee 100% unique IDs. | * 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 <code><nowiki>id++</nowiki></code>. This will guarantee 100% unique IDs. | ||
* The second parameter is the actual packet class <code><nowiki>MSG</nowiki></code>. | * The second parameter is the actual packet class <code><nowiki>MSG</nowiki></code>. | ||
− | * The third parameter is a <code><nowiki>BiConsumer<MSG, | + | * The third parameter is a <code><nowiki>BiConsumer<MSG, FriendlyByteBuf></nowiki></code> responsible for encoding the message into the provided <code><nowiki>FriendlyByteBuf</nowiki></code> |
− | * The fourth parameter is a <code><nowiki>Function< | + | * The fourth parameter is a <code><nowiki>Function<FriendlyByteBuf, MSG></nowiki></code> responsible for decoding the message from the provided <code><nowiki>FriendlyByteBuf</nowiki></code> |
* The final parameter is a <code><nowiki>BiConsumer<MSG, Supplier<NetworkEvent.Context>></nowiki></code> responsible for handling the message itself | * The final parameter is a <code><nowiki>BiConsumer<MSG, Supplier<NetworkEvent.Context>></nowiki></code> 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 <code><nowiki>MSG.encode( | + | The last three parameters can be method references to either static or instance methods in Java. Remember that an instance method <code><nowiki>MSG.encode(FriendlyByteBuf)</nowiki></code> still satisfies <code><nowiki>BiConsumer<MSG, FriendlyByteBuf></nowiki></code>, the <code><nowiki>MSG</nowiki></code> simply becomes the implicit first argument. |
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | class MessagePacket { | ||
+ | public void encoder(FriendlyByteBuf buffer) { | ||
+ | // Write to buffer | ||
+ | } | ||
+ | |||
+ | public static MessagePacket decoder(FriendlyByteBuf buffer) { | ||
+ | // Create packet from buffer data | ||
+ | } | ||
+ | |||
+ | public void messageConsumer(Supplier<NetworkEvent.Context> ctx) { | ||
+ | // Handle message | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Original direct registration syntax | ||
+ | INSTANCE.registerMessage(id, MessagePacket.class, | ||
+ | MessagePacket::encoder, | ||
+ | MessagePacket::decoder, | ||
+ | MessagePacket::messageConsumer); | ||
+ | // Consumer here must use enqueueWork and setPacketHandled | ||
+ | |||
+ | // New builder-based definition | ||
+ | INSTANCE.messageBuilder(MessagePacket.class, id) | ||
+ | .encoder(MessagePacket::encoder) | ||
+ | .decoder(MessagePacket::decoder) | ||
+ | .consumerMainThread(MessagePacket::messageConsumer) | ||
+ | .add(); | ||
+ | // You can use consumerMainThread or consumerNetworkThread. | ||
+ | // If you use consumerMainThread, the builder will take care of the enqueueWork and setPacketHandled. | ||
+ | // With consumerNetworkThread, you can return a value instead of calling setPacketHandled. | ||
+ | </syntaxhighlight> | ||
== Handling Packets == | == Handling Packets == | ||
Line 45: | Line 78: | ||
ctx.get().enqueueWork(() -> { | ctx.get().enqueueWork(() -> { | ||
// Work that needs to be threadsafe (most work) | // Work that needs to be threadsafe (most work) | ||
− | + | ServerPlayer sender = ctx.get().getSender(); // the client that sent this packet | |
// do stuff | // do stuff | ||
}); | }); | ||
Line 52: | Line 85: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Packets sent from the server to the client should be handled in another class and wrapped via <code>DistExecutor#unsafeRunWhenOn</code>. | |
− | + | <syntaxhighlight lang="java"> | |
− | + | // In Packet class | |
− | + | public static void handle(MyClientMessage msg, Supplier<NetworkEvent.Context> 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); | ||
+ | } | ||
− | {{Colored box|title= | + | // In ClientPacketHandlerClass |
− | + | public static void handlePacket(MyClientMessage msg, Supplier<NetworkEvent.Context> ctx) { | |
− | A common problem is vulnerability to <code>arbitrary chunk generation</code>. This typically happens when the server is trusting a block position sent by a client to access blocks and | + | // Do stuff |
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Note the presence of <code><nowiki>#setPacketHandled</nowiki></code>, which used to tell the network system that the packet has successfully completed handling. | ||
+ | |||
+ | === Common Packet Handling Pitfalls === | ||
+ | |||
+ | {{Colored box|title=Know that packets are by default handled on the network thread.|content= | ||
+ | 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 through the supplied <code><nowiki>NetworkEvent$Context</nowiki></code>. | ||
+ | Simply call <code><nowiki>NetworkEvent$Context#enqueueWork(Runnable)</nowiki></code>, which will call the given <code><nowiki>Runnable</nowiki></code> on the main thread at the next opportunity.}} | ||
+ | |||
+ | {{Colored box|title=Be defensive when handling packets on the server.|content= | ||
+ | A client could attempt to exploit the packet handling by sending unexpected data. | ||
+ | <br> | ||
+ | A common problem is vulnerability to <code>arbitrary chunk generation</code>. 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 promply write it to disk. This can be exploited to cause <code>catastrophic damage</code> to a server's performance and storage space without leaving a trace. | ||
<br> | <br> | ||
− | To avoid this problem, a general rule of thumb is to only access blocks and | + | To avoid this problem, a general rule of thumb is to only access blocks and block entities if <code><nowiki>Level#hasChunkAt</nowiki></code> is true.}} |
+ | |||
+ | {{Colored box|title=Register encoders on the physical client, as well as the physical server.|content= | ||
+ | If you only register an encoder for a clientbound packet on the physical server, your mod will likely crash or present unintended behaviour in single-player worlds. ''Forge will happily send packets that have no encoder defined, '''without a warning or error message!''''' They will be encoded as a 256-long byte buffer filled only with null bytes, by default. | ||
+ | <br>}} | ||
== Sending Packets == | == Sending Packets == | ||
Line 74: | Line 131: | ||
=== Sending to Clients === | === Sending to Clients === | ||
− | Packets can be sent directly to a client using the <code><nowiki>SimpleChannel</nowiki></code>: <code><nowiki>HANDLER.sendTo(MSG, | + | Packets can be sent directly to a client using the <code><nowiki>SimpleChannel</nowiki></code>: <code><nowiki>HANDLER.sendTo(MSG, serverPlayer.connection.getConnection(), NetworkDirection.PLAY_TO_CLIENT)</nowiki></code>. However, this can be quite inconvenient. Forge has some convenience functions that can be used: |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
// Sending to one player | // Sending to one player | ||
− | INSTANCE.send(PacketDistributor.PLAYER.with( | + | INSTANCE.send(PacketDistributor.PLAYER.with(serverPlayer), new MyMessage()); |
− | // Send to all players tracking this chunk | + | // Send to all players tracking this level chunk |
− | INSTANCE.send(PacketDistributor.TRACKING_CHUNK.with( | + | INSTANCE.send(PacketDistributor.TRACKING_CHUNK.with(levelChunk), new MyMessage()); |
// Sending to all connected players | // Sending to all connected players |
Latest revision as of 21:21, 28 August 2023
SimpleChannel 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:
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<String>
returning the current network protocol version. The third and fourth arguments respectively are Predicate<String>
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.
Protocol Versions
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<String>
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 the INSTANCE.registerMessage
method, 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<MSG, FriendlyByteBuf>
responsible for encoding the message into the providedFriendlyByteBuf
- The fourth parameter is a
Function<FriendlyByteBuf, MSG>
responsible for decoding the message from the providedFriendlyByteBuf
- The final parameter is a
BiConsumer<MSG, Supplier<NetworkEvent.Context>>
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<MSG, FriendlyByteBuf>
, the MSG
simply becomes the implicit first argument.
class MessagePacket { public void encoder(FriendlyByteBuf buffer) { // Write to buffer } public static MessagePacket decoder(FriendlyByteBuf buffer) { // Create packet from buffer data } public void messageConsumer(Supplier<NetworkEvent.Context> ctx) { // Handle message } } // Original direct registration syntax INSTANCE.registerMessage(id, MessagePacket.class, MessagePacket::encoder, MessagePacket::decoder, MessagePacket::messageConsumer); // Consumer here must use enqueueWork and setPacketHandled // New builder-based definition INSTANCE.messageBuilder(MessagePacket.class, id) .encoder(MessagePacket::encoder) .decoder(MessagePacket::decoder) .consumerMainThread(MessagePacket::messageConsumer) .add(); // You can use consumerMainThread or consumerNetworkThread. // If you use consumerMainThread, the builder will take care of the enqueueWork and setPacketHandled. // With consumerNetworkThread, you can return a value instead of calling setPacketHandled.
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 threadsafe work.
public static void handle(MyMessage msg, Supplier<NetworkEvent.Context> ctx) { ctx.get().enqueueWork(() -> { // Work that needs to be threadsafe (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
.
// In Packet class public static void handle(MyClientMessage msg, Supplier<NetworkEvent.Context> 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<NetworkEvent.Context> ctx) { // Do stuff }
Note the presence of #setPacketHandled
, which used to tell the network system that the packet has successfully completed handling.
Common Packet Handling Pitfalls
Know that 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 through the supplied NetworkEvent$Context
.
NetworkEvent$Context#enqueueWork(Runnable)
, which will call the given Runnable
on the main thread at the next opportunity.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 promply write it to disk. This can be exploited to cause catastrophic damage
to a server's performance and storage space without leaving a trace.
Level#hasChunkAt
is true.Register encoders on the physical client, as well as the physical server.
If you only register an encoder for a clientbound packet on the physical server, your mod will likely crash or present unintended behaviour in single-player worlds. Forge will happily send packets that have no encoder defined, without a warning or error message! They will be encoded as a 256-long byte buffer filled only with null bytes, by default.
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(MSG, serverPlayer.connection.getConnection(), NetworkDirection.PLAY_TO_CLIENT)
. However, this can be quite inconvenient. Forge has some convenience functions that can be used:
// Sending 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()); // Sending 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.