-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Description
Is your feature request related to a problem?
Hi, I'd like to discuss an idea or suggestion regarding an internal client-bound packet listener API at the player Connection class level. First, I will start with the problem(s) that I'm actually trying to solve.
The traditional way is to inject a listener into the Netty channel pipeline. In my opinion, this hook is already very late in the process for some use cases. Running listener logic on the main thread, for example, involves multiple thread or tick context jumps between originally dispatching the packet (e.g., on the main thread) and handling it in the listener.
If the listener logic depends on packet data and server state, this inherent delay could lead to unexpected problems. (Just to give an example: Imagine handling a block update packet using the coordinates from the packet, but the server player changed worlds in the meantime.)
The ProtocolLib approach, as shown in the flowchart, is also interesting because it avoids thread jumps by proxying the channel and its event loop completely. However, this approach is very "invasive" and impractical for most simple applications. Moreover, this approach can only be used by a single plugin at the same time, otherwise, the (un)injection logic will conflict with each other.
Describe the solution you'd like.
My suggestion to solve these problems, and to generally make it much easier for plugins to hook into the packet-sending process, is to add an internal API to the Connection class. This API should be kept simple and stupid to avoid performance overhead (i.e., no allocation of new event objects, no processing, no validation, etc.). This shouldn't be a problem because it should be an internal API anyway.
I've implemented a proof of concept to show how such an internal API could work (see diff below). Plugins can implement a CustomPacketListener interface and add (or remove) instances to (from) a CopyOnWriteArrayList in the Connection class. There are three different stages where listeners may hook:
onPacketDispatch: This is executed immediately and guaranteed in the same thread and tick context in which the packet was originally sent.onPacketReadyToSend: In stage 1, packets may not be ready (mainly due to Anti-Xray packet processing and delaying subsequent packets to preserve the packet order). Stage 2 is executed when the packet is ready to be sent but before switching to the Netty event loop thread. This is either the same thread and tick context as in stage 1 (usually the case if the packet hasn't been queued), or later, likely due toConnection#tickon the server main thread. This is similar to the ProtocolLib approach, but without Netty proxying.onPacketSend: This is executed right beforeChannel#writeAndFlushon the Netty event loop thread. This is similar to the traditional Netty pipeline injection approach, but occurs just before the Netty pipeline.
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
index 19ec939..c102f61 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -25,8 +25,10 @@ import io.netty.handler.timeout.TimeoutException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
+import java.util.List;
import java.util.Objects;
import java.util.Queue;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Consumer;
import javax.crypto.Cipher;
@@ -63,6 +65,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public static final Marker PACKET_RECEIVED_MARKER = Util.make(MarkerFactory.getMarker("PACKET_RECEIVED"), marker -> marker.add(PACKET_MARKER));
public static final Marker PACKET_SENT_MARKER = Util.make(MarkerFactory.getMarker("PACKET_SENT"), marker -> marker.add(PACKET_MARKER));
private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
+ public static final List<CustomPacketListener> customPacketListeners = new CopyOnWriteArrayList<>();
private final PacketFlow receiving;
private volatile boolean sendLoginDisconnect = true;
private final Queue<WrappedConsumer> pendingActions = Queues.newConcurrentLinkedQueue(); // Paper - Optimize network
@@ -111,6 +114,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
private boolean stopReadingPackets;
private void killForPacketSpam() {
+ // TODO: Call onPacketDispatch here because Connection#send is skipped.
this.sendPacket(new ClientboundDisconnectPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage)), PacketSendListener.thenRun(() -> {
this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage));
}), true);
@@ -384,6 +388,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
this.disconnectListener = packetListener;
this.runOnceConnected(connection -> {
this.setupInboundProtocol(clientboundProtocol, packetListener);
+ // TODO: Call onPacketDispatch here because Connection#send is skipped.
connection.sendPacket(new ClientIntentionPacket(SharedConstants.getCurrentVersion().protocolVersion(), hostName, port, intention), null, true);
this.setupOutboundProtocol(serverboundProtocol);
});
@@ -405,6 +410,14 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
return;
}
+ for (CustomPacketListener customPacketListener : customPacketListeners) {
+ packet = customPacketListener.onPacketDispatch(this, packet); // TODO: Error handling?
+
+ if (packet == null) {
+ return;
+ }
+ }
+
packet.onPacketDispatch(this.getPlayer());
if (connected && (InnerUtil.canSendImmediate(this, packet)
|| (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty()
@@ -444,11 +457,20 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
private void sendPacket(Packet<?> packet, @Nullable ChannelFutureListener sendListener, boolean flush) {
+ for (CustomPacketListener customPacketListener : customPacketListeners) {
+ packet = customPacketListener.onPacketReadyToSend(this, packet); // TODO: Error handling?
+
+ if (packet == null) {
+ return;
+ }
+ }
+
this.sentPackets++;
if (this.channel.eventLoop().inEventLoop()) {
this.doSendPacket(packet, sendListener, flush);
} else {
- this.channel.eventLoop().execute(() -> this.doSendPacket(packet, sendListener, flush));
+ Packet<?> finalPacket = packet;
+ this.channel.eventLoop().execute(() -> this.doSendPacket(finalPacket, sendListener, flush));
}
}
@@ -459,6 +481,15 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
packet.onPacketDispatchFinish(player, null);
return;
}
+
+ for (CustomPacketListener customPacketListener : customPacketListeners) {
+ packet = customPacketListener.onPacketSend(this, packet); // TODO: Error handling?
+
+ if (packet == null) {
+ return;
+ }
+ }
+
try {
final ChannelFuture channelFuture;
// Paper end - Optimize network
@@ -473,7 +504,8 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
// Paper start - Optimize network
if (packet.hasFinishListener()) {
- channelFuture.addListener((ChannelFutureListener) future -> packet.onPacketDispatchFinish(player, future));
+ Packet<?> finalPacket = packet;
+ channelFuture.addListener((ChannelFutureListener) future -> finalPacket.onPacketDispatchFinish(player, future));
}
} catch (final Exception e) {
LOGGER.error("NetworkException: {}", player, e);
@@ -946,4 +978,18 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
}
// Paper end - Optimize network
+
+ public interface CustomPacketListener { // TODO: Maybe create an interface and a listener list per method?
+ default @Nullable Packet<?> onPacketDispatch(Connection connection, Packet<?> packet) {
+ return packet;
+ }
+
+ default @Nullable Packet<?> onPacketReadyToSend(Connection connection, Packet<?> packet) {
+ return packet;
+ }
+
+ default @Nullable Packet<?> onPacketSend(Connection connection, Packet<?> packet) {
+ return packet;
+ }
+ }
}Please let me know what you think about this suggestion. Thanks!
Describe alternatives you've considered.
I've already mentioned some alternatives above. Especially ProtocolLib avoids the mentioned problems with thread and context jumps. However, this suggestion is about making it easier to implement an own packet listener.
Other
No response