From 9b2ad0bbfdddf1301ca43fb8d96ecf65e57b022b Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sun, 13 Jul 2025 22:03:53 +0200 Subject: [PATCH 01/22] Initial draft of the Geyser Networking API --- .../api/connection/GeyserConnection.java | 10 + .../SessionDefineNetworkChannelsEvent.java | 61 +++++ .../ServerReceiveNetworkMessageEvent.java | 99 +++++++ .../api/network/ExtensionNetworkChannel.java | 91 +++++++ .../api/network/ExternalNetworkChannel.java | 92 +++++++ .../geyser/api/network/MessageDirection.java | 41 +++ .../geyser/api/network/NetworkChannel.java | 96 +++++++ .../geyser/api/network/NetworkManager.java | 59 +++++ .../geyser/api/network/PacketChannel.java | 92 +++++++ .../geyser/api/network/message/DataType.java | 234 +++++++++++++++++ .../geyser/api/network/message/Message.java | 79 ++++++ .../api/network/message/MessageBuffer.java | 63 +++++ .../api/network/message/MessageCodec.java | 247 ++++++++++++++++++ .../api/network/message/MessageFactory.java | 46 ++++ .../geyser/network/GeyserNetworkManager.java | 221 ++++++++++++++++ .../geyser/network/UpstreamPacketHandler.java | 5 + .../network/message/BedrockPacketMessage.java | 57 ++++ .../geyser/network/message/ByteBufCodec.java | 178 +++++++++++++ .../network/message/ByteBufCodecLE.java | 176 +++++++++++++ .../network/message/ByteBufMessageBuffer.java | 58 ++++ .../network/message/JavaPacketMessage.java | 38 +++ .../loader/ProviderRegistryLoader.java | 20 ++ .../geyser/session/GeyserSession.java | 17 +- .../geyser/session/UpstreamSession.java | 12 +- .../java/JavaCustomPayloadTranslator.java | 21 ++ .../protocol/java/JavaLoginTranslator.java | 13 + 26 files changed, 2122 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/message/Message.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index b7a678aa6b1..b95c4198a67 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -35,6 +35,7 @@ import org.geysermc.geyser.api.entity.EntityData; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; +import org.geysermc.geyser.api.network.NetworkManager; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -118,6 +119,15 @@ public interface GeyserConnection extends Connection, CommandSource { */ void sendCommand(String command); + /** + * Gets the {@link NetworkManager} used for handling + * network channels and sending messages. + * + * @return the network manager + */ + @NonNull + NetworkManager networkManager(); + /** * @param javaId the Java entity ID to look up. * @return a {@link GeyserEntity} if present in this connection's entity tracker. diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java new file mode 100644 index 00000000000..1148345aeec --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.bedrock; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.connection.ConnectionEvent; +import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.message.MessageBuffer; +import org.geysermc.geyser.api.network.message.MessageCodec; +import org.geysermc.geyser.api.network.message.MessageFactory; + +/** + * Called whenever Geyser is registering network channels. + * @since 2.8.2 + */ +public abstract class SessionDefineNetworkChannelsEvent extends ConnectionEvent { + + public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection) { + super(connection); + } + + /** + * Registers a new network channel with a message factory. + * + * @param channel the channel to register + * @param messageFactory the factory to create messages from the buffer + */ + public abstract void register(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory); + + /** + * Registers a new network channel with a message factory. + * + * @param channel the channel to register + * @param messageFactory the factory to create messages from the buffer + */ + public abstract void register(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java new file mode 100644 index 00000000000..8e9f68332cb --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.java; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.Cancellable; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.connection.ConnectionEvent; +import org.geysermc.geyser.api.network.MessageDirection; +import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.message.Message; + +/** + * Called when the server receives a network message. + * @since 2.8.2 + */ +public class ServerReceiveNetworkMessageEvent extends ConnectionEvent implements Cancellable { + private final NetworkChannel channel; + private final Message message; + private final MessageDirection direction; + private boolean cancelled = false; + + public ServerReceiveNetworkMessageEvent(@NonNull GeyserConnection connection, @NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction) { + super(connection); + + this.channel = channel; + this.message = message; + this.direction = direction; + } + + /** + * Gets the channel that received the message. + * + * @return the channel that received the message + */ + @NonNull + public NetworkChannel channel() { + return this.channel; + } + + /** + * Gets the message that was received. + * + * @return the received message + */ + @NonNull + public Message message() { + return this.message; + } + + /** + * Gets the direction of the message. + * + * @return the direction of the message + */ + @NonNull + public MessageDirection direction() { + return this.direction; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isCancelled() { + return this.cancelled; + } + + /** + * {@inheritDoc} + */ + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java new file mode 100644 index 00000000000..44b2becdee7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.extension.Extension; + +import java.util.Objects; + +/** + * Represents a network channel associated with an extension. + * @since 2.8.2 + */ +public class ExtensionNetworkChannel implements NetworkChannel { + private final Extension extension; + private final String channel; + + protected ExtensionNetworkChannel(@NonNull Extension extension, @NonNull String channel) { + this.extension = extension; + this.channel = channel; + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public String key() { + return this.extension.description().id(); + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public String channel() { + return this.channel; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPacket() { + return false; + } + + @Override + public boolean equals(Object o) { + if (o == null || !NetworkChannel.class.isAssignableFrom(o.getClass())) return false; + NetworkChannel that = (NetworkChannel) o; + return Objects.equals(this.key(), that.key()) && Objects.equals(this.channel(), that.channel()); + } + + @Override + public int hashCode() { + return Objects.hash(this.key(), this.channel()); + } + + @Override + public String toString() { + return "ExtensionNetworkChannel{" + + "extension=" + this.extension.description().id() + + ", channel='" + this.channel + '\'' + + '}'; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java new file mode 100644 index 00000000000..a22693a8fed --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Objects; + +/** + * Represents a network channel not associated with any specific extension. + *

+ * This can be used for external communication channels, like mods or plugins. + * @since 2.8.2 + */ +public class ExternalNetworkChannel implements NetworkChannel { + private final String key; + private final String channel; + + protected ExternalNetworkChannel(@NonNull String key, @NonNull String channel) { + this.key = key; + this.channel = channel; + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public String key() { + return this.key; + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public String channel() { + return this.channel; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPacket() { + return false; + } + + @Override + public boolean equals(Object o) { + if (o == null || !NetworkChannel.class.isAssignableFrom(o.getClass())) return false; + NetworkChannel that = (NetworkChannel) o; + return Objects.equals(this.key(), that.key()) && Objects.equals(this.channel(), that.channel()); + } + + @Override + public int hashCode() { + return Objects.hash(this.key(), this.channel()); + } + + @Override + public String toString() { + return "ExternalNetworkChannel{" + + "key='" + this.key + '\'' + + ", channel='" + this.channel + '\'' + + '}'; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java new file mode 100644 index 00000000000..56af6f2d6bc --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +/** + * Represents the direction of a message. + * @since 2.8.2 + */ +public enum MessageDirection { + /** + * Indicates that the message is sent from the server to the client. + */ + CLIENTBOUND, + /** + * Indicates that the message is sent from the client to the server. + */ + SERVERBOUND +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java new file mode 100644 index 00000000000..741a9118d84 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.extension.Extension; + +/** + * Represents a channel used for network communication. + * @since 2.8.2 + */ +public interface NetworkChannel { + + /** + * Gets the key that owns this channel. + * + * @return the key that owns this channel + */ + @NonNull + String key(); + + /** + * Gets the name of the channel. + * + * @return the channel name + */ + @NonNull + String channel(); + + /** + * Checks if this channel is a packet channel. + * + * @return true if this channel is a packet channel, false otherwise + */ + boolean isPacket(); + + /** + * Creates a new {@link NetworkChannel} instance. + * + * @param extension the extension that registered this channel + * @param channel the name of the channel + * @return a new {@link NetworkChannel} instance + */ + @NonNull + static NetworkChannel of(@NonNull Extension extension, @NonNull String channel) { + return new ExtensionNetworkChannel(extension, channel); + } + + /** + * Creates a new {@link NetworkChannel} instance. + * + * @param id the channel id + * @param channel the name of the channel + * @return a new {@link NetworkChannel} instance + */ + @NonNull + static NetworkChannel of(@NonNull String id, @NonNull String channel) { + return new ExternalNetworkChannel(id, channel); + } + + /** + * Creates a new {@link PacketChannel} instance for a packet channel. + * + * @param key the packet key + * @param packetId the packet ID + * @param packetType the type of the packet + * @return a new {@link PacketChannel} instance for a packet channel + */ + static NetworkChannel packet(@NonNull String key, @NonNegative int packetId, @NonNull Class packetType) { + return new PacketChannel(key, packetId, packetType); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java new file mode 100644 index 00000000000..546a63bef95 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.api.network.message.MessageBuffer; + +import java.util.Set; + +/** + * Represents the network manager responsible for handling network operations + * for a {@link GeyserConnection}. + * + * @since 2.8.2 + */ +public interface NetworkManager { + + /** + * Gets the registered network channels. + * + * @return the registered network channels + */ + @NonNull + Set getRegisteredChannels(); + + /** + * Sends a message to this connection on the specified channel. + * + * @param channel the channel to send the message on + * @param message the message to send + * @param direction the direction of the message (clientbound or serverbound) + */ + void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java new file mode 100644 index 00000000000..d21f93dbd36 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Objects; + +/** + * Represents a network channel associated with a packet. + *

+ * This channel is used for communication of packets between the server and client. + * @since 2.8.2 + */ +public class PacketChannel extends ExternalNetworkChannel { + private static final String PACKET_CHANNEL_KEY = "packet"; + + private final int packetId; + private final Class packetType; + + protected PacketChannel(@NonNull String key, @NonNegative int packetId, @NonNull Class packetType) { + super(PACKET_CHANNEL_KEY, key); + + this.packetId = packetId; + this.packetType = packetType; + } + + /** + * Gets the packet ID associated with this channel. + * + * @return the packet ID + */ + @NonNegative + public int packetId() { + return this.packetId; + } + + /** + * Gets the type of the packet associated with this channel. + * + * @return the class of the packet type + */ + @NonNull + public Class packetType() { + return this.packetType; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPacket() { + return true; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + PacketChannel that = (PacketChannel) o; + return this.packetId == that.packetId; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), this.packetId); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java new file mode 100644 index 00000000000..6bbd8675358 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Represents a data type that can be sent or received over the network. + * + * @param the type + * @since 2.8.2 + */ +public final class DataType { + /** + * A DataType for reading and writing boolean values. + */ + public static final DataType BOOLEAN = of(MessageCodec::readBoolean, MessageCodec::writeBoolean); + /** + * A DataType for reading and writing byte values. + */ + public static final DataType BYTE = of(MessageCodec::readByte, MessageCodec::writeByte); + /** + * A DataType for reading and writing short values. + */ + public static final DataType SHORT = of(MessageCodec::readShort, MessageCodec::writeShort); + /** + * A DataType for reading and writing integer values. + */ + public static final DataType INT = of(MessageCodec::readInt, MessageCodec::writeInt); + /** + * A DataType for reading and writing float values. + */ + public static final DataType FLOAT = of(MessageCodec::readFloat, MessageCodec::writeFloat); + /** + * A DataType for reading and writing double values. + */ + public static final DataType DOUBLE = of(MessageCodec::readDouble, MessageCodec::writeDouble); + /** + * A DataType for reading and writing long values. + */ + public static final DataType LONG = of(MessageCodec::readLong, MessageCodec::writeLong); + /** + * A DataType for reading and writing variable-length integers. + */ + public static final DataType VAR_INT = of(MessageCodec::readVarInt, MessageCodec::writeVarInt); + /** + * A DataType for reading and writing unsigned variable-length integers. + */ + public static final DataType UNSIGNED_VAR_INT = of(MessageCodec::readUnsignedVarInt, MessageCodec::writeUnsignedVarInt); + /** + * A DataType for reading and writing variable-length long values. + */ + public static final DataType VAR_LONG = of(MessageCodec::readVarLong, MessageCodec::writeVarLong); + /** + * A DataType for reading and writing unsigned variable-length long values. + */ + public static final DataType UNSIGNED_VAR_LONG = of(MessageCodec::readUnsignedVarLong, MessageCodec::writeUnsignedVarLong); + /** + * A DataType for reading and writing strings. + *

+ * Note: Strings are encoded in UTF-8 format. + */ + public static final DataType STRING = of(MessageCodec::readString, MessageCodec::writeString); + + private final Reader reader; + private final Writer writer; + + private DataType(@NonNull Reader reader, @NonNull Writer writer) { + this.reader = reader; + this.writer = writer; + } + + /** + * Reads a value of this data type from the given buffer using the specified codec. + * + * @param codec the codec to use for reading + * @param buffer the buffer to read from + * @return the read value + */ + @NonNull + public > T read(@NonNull C codec, @NonNull B buffer) { + return this.reader.read(codec, buffer); + } + + /** + * Writes a value of this data type to the given buffer using the specified codec. + * + * @param codec the codec to use for writing + * @param buffer the buffer to write to + * @param value the value to write + */ + public > void write(@NonNull C codec, @NonNull B buffer, @NonNull T value) { + this.writer.write(codec, buffer, value); + } + + /** + * Creates a new DataType with the specified reader and writer. + * + * @param reader the reader to use for reading values of this type + * @param writer the writer to use for writing values of this type + * @param the type of values this DataType handles + * @return a new DataType instance + */ + @NonNull + public static DataType of(@NonNull Reader reader, @NonNull Writer writer) { + return new DataType<>(reader, writer); + } + + /** + * Creates an optional DataType based on the provided type. + * + * @param type the underlying data type to be wrapped in an Optional + * @return a DataType that reads and writes Optional values + * @param the type of the value contained in the Optional + */ + @NonNull + public static DataType> optional(@NonNull DataType type) { + return new DataType<>(new Reader<>() { + + @Override + @NonNull + public > Optional read(@NonNull C codec, @NonNull B buffer) { + if (codec.readBoolean(buffer)) { + return Optional.of(type.read(codec, buffer)); + } else { + return Optional.empty(); + } + } + }, new Writer<>() { + + @Override + public > void write(@NonNull C codec, @NonNull B buffer, @NonNull Optional value) { + codec.writeBoolean(buffer, value.isPresent()); + value.ifPresent(t -> type.write(codec, buffer, t)); + } + }); + } + + /** + * Creates a DataType that represents a list of values of the specified type. + * + * @param type the underlying data type of the list elements + * @return a DataType that reads and writes lists of the specified type + * @param the type of the elements in the list + */ + @NonNull + public static DataType> list(@NonNull DataType type) { + return new DataType<>(new Reader<>() { + + @Override + @NonNull + public > List read(@NonNull C codec, @NonNull B buffer) { + int size = codec.readUnsignedVarInt(buffer); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(type.read(codec, buffer)); + } + + return list; + } + }, new Writer<>() { + + @Override + public > void write(@NonNull C codec, @NonNull B buffer, @NonNull List value) { + codec.writeUnsignedVarInt(buffer, value.size()); + for (T element : value) { + type.write(codec, buffer, element); + } + } + }); + } + + /** + * Represents a reader that can read values of a {@link DataType} from a buffer. + * + * @param the type of value to read + */ + public interface Reader { + + /** + * Reads a value of type {@link T} from the given buffer using the specified {@link C codec}. + * + * @param codec the codec to use for reading + * @param buffer the buffer to read from + * @return the read value + */ + @NonNull + > T read(@NonNull C codec, @NonNull B buffer); + } + + /** + * Represents a writer that can write values of a {@link DataType} to a buffer. + * + * @param the type of value to write + */ + public interface Writer { + + /** + * Writes a value of type {@link T} to the given buffer using the specified {@link C codec}. + * + * @param codec the codec to use for writing + * @param buffer the buffer to write to + * @param value the value to write + */ + > void write(@NonNull C codec, @NonNull B buffer, @NonNull T value); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java new file mode 100644 index 00000000000..bcbe5b0f709 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; + +/** + * Represents a message that can be sent over the network. + * @since 2.8.2 + */ +public interface Message { + + /** + * Reads the message from the provided buffer. + * + * @param buffer the buffer to read from + */ + void encode(@NonNull T buffer); + + /** + * Represents a simple message using the built-in {@link MessageBuffer} implementation. + */ + interface Simple extends Message { + } + + interface PacketBase extends Message { + } + + /** + * Represents a packet message that includes a packet ID. + */ + interface Packet extends PacketBase { + + /** + * Creates a new packet message from the given packet object and direction. + * + * @param packet the packet object to create the message from + * @return a new packet message + */ + static PacketWrapped of(@NonNull Object packet) { + return GeyserApi.api().provider(PacketWrapped.class, packet); + } + } + + interface PacketWrapped extends PacketBase { + + /** + * Gets the packet associated with this message. + * + * @return the packet + */ + @NonNull + Object packet(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java new file mode 100644 index 00000000000..d4234d08860 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * A buffer for messages that can be sent over the network. + * @since 2.8.2 + */ +public interface MessageBuffer { + + /** + * Reads a {@link T value} from the buffer using the + * provided {@link DataType}. + * + * @param type the type of message to read + * @return the read message + * @param the type of message to read + */ + @NonNull + T read(@NonNull DataType type); + + /** + * Writes a {@link T value} to the buffer using the + * provided {@link DataType}. + * + * @param type the type of message to write + * @param value the value to write + * @param the type of message to write + */ + void write(@NonNull DataType type, @NonNull T value); + + /** + * Serializes the buffer to a byte array. + * + * @return the serialized byte array + */ + byte @NonNull [] serialize(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java new file mode 100644 index 00000000000..b3f43c335ed --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * A codec for encoding and decoding messages. + * + * @param the type of {@link MessageBuffer} used for encoding and decoding + * @since 2.8.2 + */ +public interface MessageCodec { + + /** + * Reads a boolean value from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the boolean value read + */ + boolean readBoolean(@NonNull T buffer); + + /** + * Reads a byte value from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the byte value read + */ + byte readByte(@NonNull T buffer); + + /** + * Reads a short value from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the short value read + */ + short readShort(@NonNull T buffer); + + /** + * Reads an integer value from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the integer value read + */ + int readInt(@NonNull T buffer); + + /** + * Reads a float value from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the float value read + */ + float readFloat(@NonNull T buffer); + + /** + * Reads a double value from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the double value read + */ + double readDouble(@NonNull T buffer); + + /** + * Reads a long value from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the long value read + */ + long readLong(@NonNull T buffer); + + /** + * Reads a variable-length integer from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the variable-length integer read + */ + int readVarInt(@NonNull T buffer); + + /** + * Reads an unsigned variable-length integer from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the unsigned variable-length integer read + */ + int readUnsignedVarInt(@NonNull T buffer); + + /** + * Reads a variable-length long from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the variable-length long read + */ + long readVarLong(@NonNull T buffer); + + /** + * Reads an unsigned variable-length long from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the unsigned variable-length long read + */ + long readUnsignedVarLong(@NonNull T buffer); + + /** + * Reads a string from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the string read + */ + @NonNull + String readString(@NonNull T buffer); + + /** + * Writes a boolean value to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the boolean value to write + */ + void writeBoolean(@NonNull T buffer, boolean value); + + /** + * Writes a byte value to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the byte value to write + */ + void writeByte(@NonNull T buffer, byte value); + + /** + * Writes a short value to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the short value to write + */ + void writeShort(@NonNull T buffer, short value); + + /** + * Writes an integer value to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the integer value to write + */ + void writeInt(@NonNull T buffer, int value); + + /** + * Writes a float value to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the float value to write + */ + void writeFloat(@NonNull T buffer, float value); + + /** + * Writes a double value to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the double value to write + */ + void writeDouble(@NonNull T buffer, double value); + + /** + * Writes a long value to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the long value to write + */ + void writeLong(@NonNull T buffer, long value); + + /** + * Writes a variable-length integer to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the variable-length integer to write + */ + void writeVarInt(@NonNull T buffer, int value); + + /** + * Writes an unsigned variable-length integer to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the unsigned variable-length integer to write + */ + void writeUnsignedVarInt(@NonNull T buffer, int value); + + /** + * Writes a variable-length long to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the variable-length long to write + */ + void writeVarLong(@NonNull T buffer, long value); + + /** + * Writes an unsigned variable-length long to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the unsigned variable-length long to write + */ + void writeUnsignedVarLong(@NonNull T buffer, long value); + + /** + * Writes a string to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param value the string to write + */ + void writeString(@NonNull T buffer, @NonNull String value); + + /** + * Creates a new {@link T buffer} instance. + * + * @return a new instance of {@link MessageBuffer} + */ + @NonNull + T createBuffer(); + + /** + * Creates a new {@link T buffer} instance with the given data. + * + * @param data the byte array to initialize the buffer with + * @return a new instance of {@link MessageBuffer} initialized with the provided data + */ + @NonNull + T createBuffer(byte @NonNull [] data); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java new file mode 100644 index 00000000000..6c8e7c5084b --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * A factory interface for creating messages from a given message buffer. + * + * @param the type of the message buffer + * @since 2.8.2 + */ +public interface MessageFactory { + + /** + * Creates a new message from the provided buffer. + * + * @param buffer the buffer to create the message from + * @return a new message created from the buffer + */ + @NonNull + Message create(@NonNull T buffer); +} diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java new file mode 100644 index 00000000000..e2abd692f07 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; +import org.cloudburstmc.protocol.bedrock.codec.BedrockCodecHelper; +import org.cloudburstmc.protocol.bedrock.codec.BedrockPacketDefinition; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; +import org.geysermc.geyser.api.event.java.ServerReceiveNetworkMessageEvent; +import org.geysermc.geyser.api.network.MessageDirection; +import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.NetworkManager; +import org.geysermc.geyser.api.network.PacketChannel; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.api.network.message.MessageBuffer; +import org.geysermc.geyser.api.network.message.MessageCodec; +import org.geysermc.geyser.api.network.message.MessageFactory; +import org.geysermc.geyser.network.message.BedrockPacketMessage; +import org.geysermc.geyser.network.message.ByteBufCodec; +import org.geysermc.geyser.network.message.ByteBufMessageBuffer; +import org.geysermc.geyser.network.message.JavaPacketMessage; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +public class GeyserNetworkManager implements NetworkManager { + private final GeyserSession session; + + private final Map> definitions = new HashMap<>(); + private final Int2ObjectMap packetChannels = new Int2ObjectOpenHashMap<>(); + + public GeyserNetworkManager(GeyserSession session) { + this.session = session; + + SessionDefineNetworkChannelsEvent event = new SessionDefineNetworkChannelsEvent(session) { + + @Override + public void register(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory) { + GeyserNetworkManager.this.registerMessage(channel, new MessageDefinition<>(ByteBufCodec.INSTANCE, messageFactory)); + } + + @Override + public void register(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory) { + GeyserNetworkManager.this.registerMessage(channel, new MessageDefinition<>(codec, messageFactory)); + } + }; + + GeyserApi.api().eventBus().fire(event); + } + + @Override + public @NonNull Set getRegisteredChannels() { + return Set.copyOf(this.definitions.keySet()); + } + + @SuppressWarnings("unchecked") + @Override + public void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction) { + if (channel.isPacket() && message instanceof Message.PacketBase packetBase) { + if (packetBase instanceof BedrockPacketMessage packetMessage) { + this.session.sendUpstreamPacket(packetMessage.packet()); + } else if (packetBase instanceof JavaPacketMessage packetMessage) { + this.session.sendDownstreamPacket(packetMessage.packet()); + } else if (packetBase instanceof Message.Packet packet) { + PacketChannel packetChannel = (PacketChannel) channel; + int packetId = packetChannel.packetId(); + + ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE_LE.createBuffer(); + packet.encode(buffer); + + BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); + BedrockCodecHelper helper = this.session.getUpstream().getCodecHelper(); + + BedrockPacket bedrockPacket = codec.tryDecode(helper, buffer.buffer(), packetId); + if (bedrockPacket == null) { + throw new IllegalArgumentException("No Bedrock packet definition found for packet ID: " + packetId); + } + + // Clientbound packets are sent upstream, serverbound packets are sent downstream + if (direction == MessageDirection.CLIENTBOUND) { + this.session.sendUpstreamPacket(bedrockPacket); + } else { + this.session.getUpstream().getSession().getPacketHandler().handlePacket(bedrockPacket); + } + } + + return; + } + + MessageDefinition definition = (MessageDefinition) this.definitions.get(channel); + if (definition == null) { + throw new IllegalArgumentException("No message definition registered for channel: " + channel); + } + + T buffer = definition.codec.createBuffer(); + message.encode(buffer); + + ServerboundCustomPayloadPacket packet = new ServerboundCustomPayloadPacket( + Key.key(channel.key(), channel.channel()), + buffer.serialize() + ); + + this.session.sendDownstreamPacket(packet); + } + + public Message createMessage(@NonNull NetworkChannel channel, byte @NotNull[] data) { + return this.createMessage0(channel, definition -> definition.createBuffer(data)); + } + + public Message createMessage(@NonNull NetworkChannel channel, @NonNull T buffer) { + return this.createMessage0(channel, def -> buffer); + } + + @SuppressWarnings("unchecked") + private Message createMessage0(@NonNull NetworkChannel channel, @NonNull Function, T> creator) { + MessageDefinition definition = (MessageDefinition) this.definitions.get(channel); + if (definition == null) { + throw new IllegalArgumentException("No message definition registered for channel: " + channel); + } + + T buffer = creator.apply(definition); + Message message = definition.createMessage(buffer); + if (message instanceof BedrockPacketMessage packetMessage) { + packetMessage.postProcess(this.session, (ByteBufMessageBuffer) buffer); + } + return message; + } + + public PacketChannel getPacketChannel(int packetId) { + return this.packetChannels.get(packetId); + } + + @SuppressWarnings("unchecked") + public boolean handlePacket(BedrockPacket packet, MessageDirection direction) { + if (this.packetChannels.isEmpty()) { + return true; // Avoid processing anything if we have nothing to handle + } + + BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); + BedrockPacketDefinition definition = codec.getPacketDefinition((Class) packet.getClass()); + PacketChannel channel = this.getPacketChannel(definition.getId()); + if (channel == null) { + return true; + } + + Message message; + if (channel.packetType().isInstance(packet)) { + message = new BedrockPacketMessage(packet); + } else { + ByteBuf buffer = Unpooled.buffer(); + definition.getSerializer().serialize(buffer, this.session.getUpstream().getCodecHelper(), packet); + message = this.createMessage(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer)); + } + + ServerReceiveNetworkMessageEvent event = new ServerReceiveNetworkMessageEvent(this.session, channel, message, direction); + this.session.getGeyser().eventBus().fire(event); + + // If the event is canceled, we do not want to process the packet further + return !event.isCancelled(); + } + + private void registerMessage(@NonNull NetworkChannel channel, @NonNull MessageDefinition codec) { + if (this.definitions.containsKey(channel)) { + throw new IllegalArgumentException("Channel is already registered: " + channel); + } + + this.definitions.put(channel, codec); + if (channel.isPacket()) { + PacketChannel packetChannel = (PacketChannel) channel; + int packetId = packetChannel.packetId(); + this.packetChannels.put(packetId, packetChannel); + } + } + + public record MessageDefinition(MessageCodec codec, MessageFactory messageFactory) { + + public T createBuffer(byte @NotNull[] data) { + return this.codec.createBuffer(data); + } + + public Message createMessage(@NonNull T buffer) { + return this.messageFactory.create(buffer); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index e04b23e96c4..8e0c4e4fb5a 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -57,6 +57,7 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.bedrock.SessionInitializeEvent; import org.geysermc.geyser.api.network.AuthType; +import org.geysermc.geyser.api.network.MessageDirection; import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePackManifest; @@ -108,6 +109,10 @@ public UpstreamPacketHandler(GeyserImpl geyser, GeyserSession session) { } private PacketSignal translateAndDefault(BedrockPacket packet) { + if (!this.session.getNetworkManager().handlePacket(packet, MessageDirection.SERVERBOUND)) { + return PacketSignal.HANDLED; + } + Registries.BEDROCK_PACKET_TRANSLATORS.translate(packet.getClass(), packet, session, false); return PacketSignal.HANDLED; // PacketSignal.UNHANDLED will log a WARN publicly } diff --git a/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java b/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java new file mode 100644 index 00000000000..776514d8e2a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.protocol.bedrock.codec.BedrockPacketDefinition; +import org.cloudburstmc.protocol.bedrock.codec.BedrockPacketSerializer; +import org.cloudburstmc.protocol.bedrock.codec.PacketSerializeException; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.session.GeyserSession; + +public record BedrockPacketMessage(@NonNull BedrockPacket packet) implements Message.PacketWrapped { + + @SuppressWarnings("unchecked") + public void postProcess(@NonNull GeyserSession session, @NonNull ByteBufMessageBuffer buffer) { + BedrockPacketDefinition definition = session.getUpstream().getSession().getCodec().getPacketDefinition(this.packet.getClass()); + if (definition == null) { + throw new IllegalArgumentException("Packet definition for " + this.packet.getClass().getSimpleName() + " not found!"); + } + + BedrockPacketSerializer serializer = (BedrockPacketSerializer) definition.getSerializer(); + try { + serializer.deserialize(buffer.buffer(), session.getUpstream().getCodecHelper(), this.packet); + } catch (Exception e) { + throw new PacketSerializeException("Error whilst deserializing " + this.packet, e); + } + } + + @Override + public void encode(@NonNull ByteBufMessageBuffer buffer) { + throw new UnsupportedOperationException("BedrockPacketMessage does not support encoding directly to a MessageBuffe."); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java new file mode 100644 index 00000000000..d73e768cf89 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network.message; + +import io.netty.buffer.Unpooled; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.protocol.common.util.VarInts; +import org.geysermc.geyser.api.network.message.MessageCodec; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; + +public class ByteBufCodec implements MessageCodec { + public static final ByteBufCodec INSTANCE = new ByteBufCodec(); + public static final ByteBufCodecLE INSTANCE_LE = new ByteBufCodecLE(); + + private ByteBufCodec() { + } + + @Override + public boolean readBoolean(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readBoolean(); + } + + @Override + public byte readByte(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readByte(); + } + + @Override + public short readShort(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readShort(); + } + + @Override + public int readInt(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readInt(); + } + + @Override + public float readFloat(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readFloat(); + } + + @Override + public double readDouble(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readDouble(); + } + + @Override + public long readLong(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readLong(); + } + + @Override + public int readVarInt(@NotNull ByteBufMessageBuffer buffer) { + return VarInts.readInt(buffer.buffer()); + } + + @Override + public int readUnsignedVarInt(@NotNull ByteBufMessageBuffer buffer) { + return VarInts.readUnsignedInt(buffer.buffer()); + } + + @Override + public long readVarLong(@NotNull ByteBufMessageBuffer buffer) { + return VarInts.readLong(buffer.buffer()); + } + + @Override + public long readUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer) { + return VarInts.readUnsignedLong(buffer.buffer()); + } + + @Override + public @NotNull String readString(@NotNull ByteBufMessageBuffer buffer) { + int size = VarInts.readUnsignedInt(buffer.buffer()); + byte[] bytes = new byte[size]; + buffer.buffer().readBytes(bytes); + + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public void writeBoolean(@NotNull ByteBufMessageBuffer buffer, boolean value) { + buffer.buffer().writeBoolean(value); + } + + @Override + public void writeByte(@NotNull ByteBufMessageBuffer buffer, byte value) { + buffer.buffer().writeByte(value); + } + + @Override + public void writeShort(@NotNull ByteBufMessageBuffer buffer, short value) { + buffer.buffer().writeShort(value); + } + + @Override + public void writeInt(@NotNull ByteBufMessageBuffer buffer, int value) { + buffer.buffer().writeInt(value); + } + + @Override + public void writeFloat(@NotNull ByteBufMessageBuffer buffer, float value) { + buffer.buffer().writeFloat(value); + } + + @Override + public void writeDouble(@NotNull ByteBufMessageBuffer buffer, double value) { + buffer.buffer().writeDouble(value); + } + + @Override + public void writeLong(@NotNull ByteBufMessageBuffer buffer, long value) { + buffer.buffer().writeLong(value); + } + + @Override + public void writeVarInt(@NotNull ByteBufMessageBuffer buffer, int value) { + VarInts.writeInt(buffer.buffer(), value); + } + + @Override + public void writeUnsignedVarInt(@NotNull ByteBufMessageBuffer buffer, int value) { + VarInts.writeUnsignedInt(buffer.buffer(), value); + } + + @Override + public void writeVarLong(@NotNull ByteBufMessageBuffer buffer, long value) { + VarInts.writeLong(buffer.buffer(), value); + } + + @Override + public void writeUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer, long value) { + VarInts.writeUnsignedLong(buffer.buffer(), value); + } + + @Override + public void writeString(@NotNull ByteBufMessageBuffer buffer, @NonNull String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + VarInts.writeUnsignedInt(buffer.buffer(), bytes.length); + buffer.buffer().writeBytes(bytes); + } + + @Override + public @NotNull ByteBufMessageBuffer createBuffer() { + return new ByteBufMessageBuffer(this); + } + + @Override + public @NotNull ByteBufMessageBuffer createBuffer(byte @NotNull [] data) { + return new ByteBufMessageBuffer(this, Unpooled.wrappedBuffer(data)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java new file mode 100644 index 00000000000..98e401e7344 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network.message; + +import io.netty.buffer.Unpooled; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.protocol.common.util.VarInts; +import org.geysermc.geyser.api.network.message.MessageCodec; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; + +public class ByteBufCodecLE implements MessageCodec { + + ByteBufCodecLE() { + } + + @Override + public boolean readBoolean(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readBoolean(); + } + + @Override + public byte readByte(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readByte(); + } + + @Override + public short readShort(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readShortLE(); + } + + @Override + public int readInt(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readIntLE(); + } + + @Override + public float readFloat(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readFloatLE(); + } + + @Override + public double readDouble(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readDoubleLE(); + } + + @Override + public long readLong(@NotNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readLongLE(); + } + + @Override + public int readVarInt(@NotNull ByteBufMessageBuffer buffer) { + return VarInts.readInt(buffer.buffer()); + } + + @Override + public int readUnsignedVarInt(@NotNull ByteBufMessageBuffer buffer) { + return VarInts.readUnsignedInt(buffer.buffer()); + } + + @Override + public long readVarLong(@NotNull ByteBufMessageBuffer buffer) { + return VarInts.readLong(buffer.buffer()); + } + + @Override + public long readUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer) { + return VarInts.readUnsignedLong(buffer.buffer()); + } + + @Override + public @NotNull String readString(@NotNull ByteBufMessageBuffer buffer) { + int size = VarInts.readUnsignedInt(buffer.buffer()); + byte[] bytes = new byte[size]; + buffer.buffer().readBytes(bytes); + + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public void writeBoolean(@NotNull ByteBufMessageBuffer buffer, boolean value) { + buffer.buffer().writeBoolean(value); + } + + @Override + public void writeByte(@NotNull ByteBufMessageBuffer buffer, byte value) { + buffer.buffer().writeByte(value); + } + + @Override + public void writeShort(@NotNull ByteBufMessageBuffer buffer, short value) { + buffer.buffer().writeShortLE(value); + } + + @Override + public void writeInt(@NotNull ByteBufMessageBuffer buffer, int value) { + buffer.buffer().writeIntLE(value); + } + + @Override + public void writeFloat(@NotNull ByteBufMessageBuffer buffer, float value) { + buffer.buffer().writeFloatLE(value); + } + + @Override + public void writeDouble(@NotNull ByteBufMessageBuffer buffer, double value) { + buffer.buffer().writeDoubleLE(value); + } + + @Override + public void writeLong(@NotNull ByteBufMessageBuffer buffer, long value) { + buffer.buffer().writeLongLE(value); + } + + @Override + public void writeVarInt(@NotNull ByteBufMessageBuffer buffer, int value) { + VarInts.writeInt(buffer.buffer(), value); + } + + @Override + public void writeUnsignedVarInt(@NotNull ByteBufMessageBuffer buffer, int value) { + VarInts.writeUnsignedInt(buffer.buffer(), value); + } + + @Override + public void writeVarLong(@NotNull ByteBufMessageBuffer buffer, long value) { + VarInts.writeLong(buffer.buffer(), value); + } + + @Override + public void writeUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer, long value) { + VarInts.writeUnsignedLong(buffer.buffer(), value); + } + + @Override + public void writeString(@NotNull ByteBufMessageBuffer buffer, @NonNull String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + VarInts.writeUnsignedInt(buffer.buffer(), bytes.length); + buffer.buffer().writeBytes(bytes); + } + + @Override + public @NotNull ByteBufMessageBuffer createBuffer() { + return new ByteBufMessageBuffer(this); + } + + @Override + public @NotNull ByteBufMessageBuffer createBuffer(byte @NotNull [] data) { + return new ByteBufMessageBuffer(this, Unpooled.wrappedBuffer(data)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java new file mode 100644 index 00000000000..adff86fb3ee --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network.message; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.message.MessageBuffer; +import org.geysermc.geyser.api.network.message.MessageCodec; +import org.geysermc.geyser.api.network.message.DataType; + +public record ByteBufMessageBuffer(MessageCodec codec, ByteBuf buffer) implements MessageBuffer { + + public ByteBufMessageBuffer(MessageCodec codec) { + this(codec, Unpooled.buffer()); + } + + @Override + public @NonNull T read(@NonNull DataType type) { + return type.read(this.codec, this); + } + + @Override + public void write(@NonNull DataType type, @NonNull T value) { + type.write(this.codec, this, value); + } + + @Override + public byte @NonNull [] serialize() { + byte[] bytes = new byte[this.buffer.readableBytes()]; + this.buffer.readBytes(bytes); + this.buffer.clear(); // Clear the buffer after serialization + return bytes; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java b/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java new file mode 100644 index 00000000000..36d75ff4fb2 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket; + +public record JavaPacketMessage(MinecraftPacket packet) implements Message.PacketWrapped { + + @Override + public void encode(@NonNull ByteBufMessageBuffer buffer) { + throw new UnsupportedOperationException("JavaPacketMessage does not support encoding directly to a MessageBuffe."); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index b1f62ca9984..416923fdc97 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.registry.loader; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.api.bedrock.camera.CameraFade; import org.geysermc.geyser.api.bedrock.camera.CameraPosition; import org.geysermc.geyser.api.block.custom.CustomBlockData; @@ -39,6 +40,7 @@ import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; +import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.api.pack.option.PriorityOption; @@ -57,12 +59,15 @@ import org.geysermc.geyser.level.block.GeyserJavaBlockState; import org.geysermc.geyser.level.block.GeyserMaterialInstance; import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData; +import org.geysermc.geyser.network.message.BedrockPacketMessage; +import org.geysermc.geyser.network.message.JavaPacketMessage; import org.geysermc.geyser.pack.option.GeyserPriorityOption; import org.geysermc.geyser.pack.option.GeyserSubpackOption; import org.geysermc.geyser.pack.option.GeyserUrlFallbackOption; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.pack.url.GeyserUrlPackCodec; import org.geysermc.geyser.registry.provider.ProviderSupplier; +import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket; import java.nio.file.Path; import java.util.Map; @@ -104,6 +109,21 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov providers.put(CameraFade.Builder.class, args -> new GeyserCameraFade.Builder()); providers.put(CameraPosition.Builder.class, args -> new GeyserCameraPosition.Builder()); + // network api + providers.put(Message.PacketWrapped.class, args -> { + if (args.length < 1) { + throw new IllegalArgumentException("Message.PacketWrapped requires at least one argument, got " + args.length); + } + + if (args[0] instanceof BedrockPacket bedrockPacket) { + return new BedrockPacketMessage(bedrockPacket); + } else if (args[0] instanceof MinecraftPacket javaPacket) { + return new JavaPacketMessage(javaPacket); + } else { + throw new IllegalArgumentException("Unsupported packet type: " + args[0].getClass().getName()); + } + }); + return providers; } } diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index dceb5707a5f..3a2fec4f391 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -119,6 +119,7 @@ import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent; import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent; +import org.geysermc.geyser.api.network.NetworkManager; import org.geysermc.geyser.api.network.RemoteServer; import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.command.GeyserCommandSource; @@ -152,6 +153,7 @@ import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.network.GameProtocol; +import org.geysermc.geyser.network.GeyserNetworkManager; import org.geysermc.geyser.network.netty.LocalSession; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.BlockMappings; @@ -174,11 +176,11 @@ import org.geysermc.geyser.session.cache.StructureBlockCache; import org.geysermc.geyser.session.cache.TagCache; import org.geysermc.geyser.session.cache.TeleportCache; -import org.geysermc.geyser.session.cache.waypoint.WaypointCache; import org.geysermc.geyser.session.cache.WorldBorder; import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.geyser.session.cache.tags.DialogTag; +import org.geysermc.geyser.session.cache.waypoint.WaypointCache; import org.geysermc.geyser.session.dialog.BuiltInDialog; import org.geysermc.geyser.session.dialog.Dialog; import org.geysermc.geyser.session.dialog.DialogManager; @@ -235,10 +237,10 @@ import java.util.Queue; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -732,9 +734,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private boolean allowVibrantVisuals = true; + private final GeyserNetworkManager networkManager; + public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop tickEventLoop) { this.geyser = geyser; - this.upstream = new UpstreamSession(bedrockServerSession); + this.upstream = new UpstreamSession(this, bedrockServerSession); this.tickEventLoop = tickEventLoop; this.erosionHandler = new GeyserboundHandshakePacketHandler(this); @@ -783,6 +787,7 @@ public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSessio } this.remoteServer = geyser.defaultRemoteServer(); + this.networkManager = new GeyserNetworkManager(this); } /** @@ -2330,6 +2335,12 @@ public boolean hasFormOpen() { return formCache.hasFormOpen(); } + @Override + @NonNull + public NetworkManager networkManager() { + return this.networkManager; + } + @Override public void closeForm() { sendUpstreamPacket(new ClientboundCloseFormPacket()); diff --git a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java index 35ede56a18b..d40bd10f84b 100644 --- a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java @@ -32,6 +32,7 @@ import org.cloudburstmc.protocol.bedrock.BedrockServerSession; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodecHelper; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.geysermc.geyser.api.network.MessageDirection; import org.geysermc.geyser.network.GeyserBedrockPeer; import java.net.InetSocketAddress; @@ -40,18 +41,27 @@ @RequiredArgsConstructor public class UpstreamSession { + private final GeyserSession geyserSession; @Getter private final BedrockServerSession session; @Getter @Setter private boolean initialized = false; private Queue postStartGamePackets = new ArrayDeque<>(); public void sendPacket(@NonNull BedrockPacket packet) { + if (!this.geyserSession.getNetworkManager().handlePacket(packet, MessageDirection.CLIENTBOUND)) { + return; + } + if (!isClosed()) { session.sendPacket(packet); } } public void sendPacketImmediately(@NonNull BedrockPacket packet) { + if (!this.geyserSession.getNetworkManager().handlePacket(packet, MessageDirection.CLIENTBOUND)) { + return; + } + if (!isClosed()) { session.sendPacketImmediately(packet); } @@ -75,7 +85,7 @@ public void sendPostStartGamePackets() { BedrockPacket packet; while ((packet = postStartGamePackets.poll()) != null) { - session.sendPacket(packet); + this.sendPacket(packet); } postStartGamePackets = null; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java index d5c3c246c5a..fed4d1a156e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java @@ -40,6 +40,13 @@ import org.geysermc.floodgate.pluginmessage.PluginMessageChannels; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.api.event.EventBus; +import org.geysermc.geyser.api.event.java.ServerReceiveNetworkMessageEvent; +import org.geysermc.geyser.api.network.MessageDirection; +import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.api.network.message.MessageBuffer; +import org.geysermc.geyser.network.GeyserNetworkManager; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -143,6 +150,20 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack session.sendUpstreamPacket(toSend); }); + } else { + session.ensureInEventLoop(() -> { + GeyserNetworkManager networkManager = session.getNetworkManager(); + NetworkChannel networkChannel = NetworkChannel.of(packet.getChannel().namespace(), packet.getChannel().value()); + if (!networkManager.getRegisteredChannels().contains(networkChannel)) { + logger.debug("Received a custom payload for an unregistered channel: " + networkChannel.channel()); + return; + } + + Message message = networkManager.createMessage(networkChannel, packet.getData()); + + EventBus eventBus = session.getGeyser().getEventBus(); + eventBus.fire(new ServerReceiveNetworkMessageEvent(session, networkChannel, message, MessageDirection.CLIENTBOUND)); + }); } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index 622b2d23b65..d4efa7577dd 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -32,6 +32,7 @@ import org.geysermc.erosion.Constants; import org.geysermc.floodgate.pluginmessage.PluginMessageChannels; import org.geysermc.geyser.api.network.AuthType; +import org.geysermc.geyser.api.network.NetworkChannel; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.level.JavaDimension; @@ -50,6 +51,8 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; @Translator(packet = ClientboundLoginPacket.class) public class JavaLoginTranslator extends PacketTranslator { @@ -130,6 +133,16 @@ public void translate(GeyserSession session, ClientboundLoginPacket packet) { } session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8))); + Set registeredChannels = session.getNetworkManager().getRegisteredChannels(); + if (!registeredChannels.isEmpty()) { + String channels = registeredChannels + .stream() + .map(channel -> channel.key() + ":" + channel.channel()) + .collect(Collectors.joining("\0")); + + session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, channels.getBytes(StandardCharsets.UTF_8))); + } + if (session.getBedrockDimension().bedrockId() != newDimension.bedrockId()) { DimensionUtils.switchDimension(session, newDimension); } else if (BedrockDimension.isCustomBedrockNetherId() && newDimension.isNetherLike()) { From 43d37477535c5b294e1374102fa92ca1e0ffa212 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sun, 13 Jul 2025 22:48:16 +0200 Subject: [PATCH 02/22] Add additional packet constructors for use in channel defining --- .../geyser/api/network/message/Message.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java index bcbe5b0f709..adac7599cd0 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java @@ -28,6 +28,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; +import java.util.function.Function; +import java.util.function.Supplier; + /** * Represents a message that can be sent over the network. * @since 2.8.2 @@ -64,6 +67,29 @@ interface Packet extends PacketBase { static PacketWrapped of(@NonNull Object packet) { return GeyserApi.api().provider(PacketWrapped.class, packet); } + + /** + * Creates a new packet message from the given packet object and direction. + * + * @param packetSupplier a supplier that provides the packet object to create the message from + * @return a new packet message + */ + @NonNull + static MessageFactory of(@NonNull Supplier packetSupplier) { + return buffer -> of(packetSupplier.get()); + } + + /** + * Creates a new packet message from the given substitutor and packet supplier. + * + * @param substitutor a function that applies to the buffer to get the packet object + * @param packetSupplier a function that provides the packet object + * @return a new packet message factory + */ + @NonNull + static MessageFactory of(@NonNull Function substitutor, @NonNull Function packetSupplier) { + return buffer -> of(packetSupplier.apply(substitutor.apply(buffer))); + } } interface PacketWrapped extends PacketBase { From 3049c77975d0f45ac5f627766d6b33012724eb25 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 25 Oct 2025 21:59:25 +0100 Subject: [PATCH 03/22] Address comments --- .../SessionDefineNetworkChannelsEvent.java | 1 + .../ServerReceiveNetworkMessageEvent.java | 4 +- .../api/network/ExtensionNetworkChannel.java | 18 ++--- .../api/network/ExternalNetworkChannel.java | 29 +++---- .../geyser/api/network/MessageDirection.java | 6 ++ .../geyser/api/network/NetworkChannel.java | 76 ++++++++++++++++--- .../geyser/api/network/NetworkManager.java | 2 +- .../geyser/api/network/PacketChannel.java | 7 +- .../geyser/api/network/message/Message.java | 10 +++ .../api/network/message/MessageBuffer.java | 7 ++ .../api/network/message/MessageFactory.java | 1 + .../geyser/network/GeyserNetworkManager.java | 7 +- .../network/message/BedrockPacketMessage.java | 2 +- .../network/message/ByteBufMessageBuffer.java | 7 +- .../network/message/JavaPacketMessage.java | 2 +- .../java/JavaCustomPayloadTranslator.java | 2 +- .../protocol/java/JavaLoginTranslator.java | 2 +- 17 files changed, 125 insertions(+), 58 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index 1148345aeec..29b91702596 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -55,6 +55,7 @@ public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection) { * Registers a new network channel with a message factory. * * @param channel the channel to register + * @param codec the codec to use to encode/decode the buffer * @param messageFactory the factory to create messages from the buffer */ public abstract void register(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory); diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java index 8e9f68332cb..e6b61b51ff7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java @@ -37,7 +37,7 @@ * Called when the server receives a network message. * @since 2.8.2 */ -public class ServerReceiveNetworkMessageEvent extends ConnectionEvent implements Cancellable { +public final class ServerReceiveNetworkMessageEvent extends ConnectionEvent implements Cancellable { private final NetworkChannel channel; private final Message message; private final MessageDirection direction; @@ -53,6 +53,8 @@ public ServerReceiveNetworkMessageEvent(@NonNull GeyserConnection connection, @N /** * Gets the channel that received the message. + *

+ * See {@link NetworkChannel} for more information. * * @return the channel that received the message */ diff --git a/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java index 44b2becdee7..9cc3413d867 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java @@ -27,6 +27,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.Identifier; import java.util.Objects; @@ -48,17 +49,8 @@ protected ExtensionNetworkChannel(@NonNull Extension extension, @NonNull String */ @Override @NonNull - public String key() { - return this.extension.description().id(); - } - - /** - * {@inheritDoc} - */ - @Override - @NonNull - public String channel() { - return this.channel; + public Identifier identifier() { + return Identifier.of(this.extension.description().id(), this.channel); } /** @@ -73,12 +65,12 @@ public boolean isPacket() { public boolean equals(Object o) { if (o == null || !NetworkChannel.class.isAssignableFrom(o.getClass())) return false; NetworkChannel that = (NetworkChannel) o; - return Objects.equals(this.key(), that.key()) && Objects.equals(this.channel(), that.channel()); + return Objects.equals(this.identifier(), that.identifier()); } @Override public int hashCode() { - return Objects.hash(this.key(), this.channel()); + return Objects.hash(this.identifier()); } @Override diff --git a/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java index a22693a8fed..479727a15f1 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.api.network; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.util.Identifier; import java.util.Objects; @@ -36,12 +37,10 @@ * @since 2.8.2 */ public class ExternalNetworkChannel implements NetworkChannel { - private final String key; - private final String channel; + private final Identifier identifier; - protected ExternalNetworkChannel(@NonNull String key, @NonNull String channel) { - this.key = key; - this.channel = channel; + protected ExternalNetworkChannel(@NonNull Identifier identifier) { + this.identifier = identifier; } /** @@ -49,17 +48,8 @@ protected ExternalNetworkChannel(@NonNull String key, @NonNull String channel) { */ @Override @NonNull - public String key() { - return this.key; - } - - /** - * {@inheritDoc} - */ - @Override - @NonNull - public String channel() { - return this.channel; + public Identifier identifier() { + return this.identifier; } /** @@ -74,19 +64,18 @@ public boolean isPacket() { public boolean equals(Object o) { if (o == null || !NetworkChannel.class.isAssignableFrom(o.getClass())) return false; NetworkChannel that = (NetworkChannel) o; - return Objects.equals(this.key(), that.key()) && Objects.equals(this.channel(), that.channel()); + return Objects.equals(this.identifier(), that.identifier()); } @Override public int hashCode() { - return Objects.hash(this.key(), this.channel()); + return Objects.hash(this.identifier()); } @Override public String toString() { return "ExternalNetworkChannel{" + - "key='" + this.key + '\'' + - ", channel='" + this.channel + '\'' + + "identifier='" + this.identifier + '\'' + '}'; } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java index 56af6f2d6bc..16d4886b4ab 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java @@ -32,10 +32,16 @@ public enum MessageDirection { /** * Indicates that the message is sent from the server to the client. + *

+ * Note that extensions may also send messages in this direction, meaning + * that not every clientbound message is necessarily from the server itself. */ CLIENTBOUND, /** * Indicates that the message is sent from the client to the server. + *

+ * Note that extensions may also send messages in this direction, meaning + * that not every serverbound message is necessarily from the client itself. */ SERVERBOUND } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java index 741a9118d84..3c627d7ebde 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -28,28 +28,60 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.Identifier; /** * Represents a channel used for network communication. + *

+ * A network channel can either be an external channel or a + * packet channel. External channels are identified by a unique + * key and are used for custom payloads over the network. Packet + * channels will represent data from packets, identified by a packet + * ID and type. + *

+ * For constructing an external NetworkChannel, the following + * can be done: + * + *

+ * {@code
+ *     private final NetworkChannel myChannel = NetworkChannel.of("example", "my_channel");
+ * }
+ * 
+ * Or when inside an extension, with 'this' being the extension instance: + *
+ * {@code
+ *     private final NetworkChannel myChannel = NetworkChannel.of(this, "my_channel");
+ * }
+ * 
+ * + *

+ * For packet channels, it can get slightly more complex as you need to + * know the packet ID alongside having a constructed message type. The + * following example demonstrates this with the animate packet, assuming + * the AnimateMessage class represents the correct packet structure: + *

+ * {@code
+ *    private final NetworkChannel animateChannel = NetworkChannel.packet("animate", 44, AnimateMessage.class);
+ * }
+ * 
+ * + *

+ * Packet channels can also be registered against packet objects from + * exterbak protocol libraries, such as the ones provided in Geyser. For + * an example on how to do this, please see the + * Networking API documentation. + * * @since 2.8.2 */ public interface NetworkChannel { /** - * Gets the key that owns this channel. - * - * @return the key that owns this channel - */ - @NonNull - String key(); - - /** - * Gets the name of the channel. + * Gets the identifier that owns this channel. * - * @return the channel name + * @return the identifier that owns this channel */ @NonNull - String channel(); + Identifier identifier(); /** * Checks if this channel is a packet channel. @@ -60,6 +92,9 @@ public interface NetworkChannel { /** * Creates a new {@link NetworkChannel} instance. + *

+ * Extensions should use this method to register + * their own channels for more robust identification. * * @param extension the extension that registered this channel * @param channel the name of the channel @@ -72,6 +107,9 @@ static NetworkChannel of(@NonNull Extension extension, @NonNull String channel) /** * Creates a new {@link NetworkChannel} instance. + *

+ * This method is used for external channels provided + * by third parties, such as plugins or mods. * * @param id the channel id * @param channel the name of the channel @@ -79,7 +117,21 @@ static NetworkChannel of(@NonNull Extension extension, @NonNull String channel) */ @NonNull static NetworkChannel of(@NonNull String id, @NonNull String channel) { - return new ExternalNetworkChannel(id, channel); + return of(Identifier.of(id, channel)); + } + + /** + * Creates a new {@link NetworkChannel} instance. + *

+ * This method is used for external channels provided + * by third parties, such as plugins or mods. + * + * @param identifier the {@link Identifier} of the channel + * @return a new {@link NetworkChannel} instance + */ + @NonNull + static NetworkChannel of(@NonNull Identifier identifier) { + return new ExternalNetworkChannel(identifier); } /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java index 546a63bef95..9aa29115560 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java @@ -46,7 +46,7 @@ public interface NetworkManager { * @return the registered network channels */ @NonNull - Set getRegisteredChannels(); + Set registeredChannels(); /** * Sends a message to this connection on the specified channel. diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java index d21f93dbd36..8067cc59d9e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java @@ -27,13 +27,16 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.util.Identifier; import java.util.Objects; /** * Represents a network channel associated with a packet. *

- * This channel is used for communication of packets between the server and client. + * This channel is used for listening to communication over + * packets between the server and client and can be used to + * send or receive packets. * @since 2.8.2 */ public class PacketChannel extends ExternalNetworkChannel { @@ -43,7 +46,7 @@ public class PacketChannel extends ExternalNetworkChannel { private final Class packetType; protected PacketChannel(@NonNull String key, @NonNegative int packetId, @NonNull Class packetType) { - super(PACKET_CHANNEL_KEY, key); + super(Identifier.of(PACKET_CHANNEL_KEY, key)); this.packetId = packetId; this.packetType = packetType; diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java index adac7599cd0..e09e43b7de7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java @@ -50,6 +50,11 @@ public interface Message { interface Simple extends Message { } + /** + * Represents a packet message with an unknown packet ID. + * + * @param the type of message buffer + */ interface PacketBase extends Message { } @@ -92,6 +97,11 @@ static MessageFactory of(@NonNull Function } } + /** + * Represents a packet message that wraps an underlying packet object. + * + * @param the type of message buffer + */ interface PacketWrapped extends PacketBase { /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java index d4234d08860..7b47ecaf198 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java @@ -60,4 +60,11 @@ public interface MessageBuffer { * @return the serialized byte array */ byte @NonNull [] serialize(); + + /** + * Gets the length of the buffer. + * + * @return the length of the buffer + */ + int length(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java index 6c8e7c5084b..3d205823a3a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java @@ -33,6 +33,7 @@ * @param the type of the message buffer * @since 2.8.2 */ +@FunctionalInterface public interface MessageFactory { /** diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java index e2abd692f07..312958060bc 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java @@ -85,7 +85,7 @@ public void register(@NonNull NetworkChannel channel, } @Override - public @NonNull Set getRegisteredChannels() { + public @NonNull Set registeredChannels() { return Set.copyOf(this.definitions.keySet()); } @@ -132,7 +132,7 @@ public void send(@NonNull NetworkChannel channel, @Non message.encode(buffer); ServerboundCustomPayloadPacket packet = new ServerboundCustomPayloadPacket( - Key.key(channel.key(), channel.channel()), + Key.key(channel.identifier().toString()), buffer.serialize() ); @@ -201,8 +201,7 @@ private void registerMessage(@NonNull NetworkChannel c } this.definitions.put(channel, codec); - if (channel.isPacket()) { - PacketChannel packetChannel = (PacketChannel) channel; + if (channel.isPacket() && channel instanceof PacketChannel packetChannel) { int packetId = packetChannel.packetId(); this.packetChannels.put(packetId, packetChannel); } diff --git a/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java b/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java index 776514d8e2a..3a09d648aef 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java @@ -52,6 +52,6 @@ public void postProcess(@NonNull GeyserSession session, @NonNull ByteBufMessageB @Override public void encode(@NonNull ByteBufMessageBuffer buffer) { - throw new UnsupportedOperationException("BedrockPacketMessage does not support encoding directly to a MessageBuffe."); + throw new UnsupportedOperationException("BedrockPacketMessage does not support encoding directly to a MessageBuffer."); } } diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java index adff86fb3ee..8b414d3033e 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java @@ -28,9 +28,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.message.DataType; import org.geysermc.geyser.api.network.message.MessageBuffer; import org.geysermc.geyser.api.network.message.MessageCodec; -import org.geysermc.geyser.api.network.message.DataType; public record ByteBufMessageBuffer(MessageCodec codec, ByteBuf buffer) implements MessageBuffer { @@ -55,4 +55,9 @@ public void write(@NonNull DataType type, @NonNull T value) { this.buffer.clear(); // Clear the buffer after serialization return bytes; } + + @Override + public int length() { + return this.buffer.readableBytes(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java b/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java index 36d75ff4fb2..6a2fd336f5b 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java @@ -33,6 +33,6 @@ public record JavaPacketMessage(MinecraftPacket packet) implements Message.Packe @Override public void encode(@NonNull ByteBufMessageBuffer buffer) { - throw new UnsupportedOperationException("JavaPacketMessage does not support encoding directly to a MessageBuffe."); + throw new UnsupportedOperationException("JavaPacketMessage does not support encoding directly to a MessageBuffer."); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java index fed4d1a156e..64a46c89e69 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java @@ -154,7 +154,7 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack session.ensureInEventLoop(() -> { GeyserNetworkManager networkManager = session.getNetworkManager(); NetworkChannel networkChannel = NetworkChannel.of(packet.getChannel().namespace(), packet.getChannel().value()); - if (!networkManager.getRegisteredChannels().contains(networkChannel)) { + if (!networkManager.registeredChannels().contains(networkChannel)) { logger.debug("Received a custom payload for an unregistered channel: " + networkChannel.channel()); return; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index d4efa7577dd..eb94afdb8ec 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -133,7 +133,7 @@ public void translate(GeyserSession session, ClientboundLoginPacket packet) { } session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8))); - Set registeredChannels = session.getNetworkManager().getRegisteredChannels(); + Set registeredChannels = session.getNetworkManager().registeredChannels(); if (!registeredChannels.isEmpty()) { String channels = registeredChannels .stream() From 86b1ac979f47005eb10f7bee9709dcbf1290e7a0 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 25 Oct 2025 22:01:32 +0100 Subject: [PATCH 04/22] Missed this --- .../geyser/api/event/java/ServerReceiveNetworkMessageEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java index e6b61b51ff7..70035462c50 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java @@ -34,7 +34,7 @@ import org.geysermc.geyser.api.network.message.Message; /** - * Called when the server receives a network message. + * Called when Geyser receives a network message from the server. * @since 2.8.2 */ public final class ServerReceiveNetworkMessageEvent extends ConnectionEvent implements Cancellable { From 4542019a583f12a1b3de8f83733f3370fce1e05b Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sun, 9 Nov 2025 18:03:58 +0000 Subject: [PATCH 05/22] Refactor a bunch of things --- .../SessionDefineNetworkChannelsEvent.java | 155 ++++++++- .../ServerReceiveNetworkMessageEvent.java | 101 ------ .../geyser/api/network/MessageDirection.java | 2 +- .../geyser/api/network/NetworkChannel.java | 38 ++- .../geyser/api/network/NetworkManager.java | 2 +- .../geyser/api/network/message/DataType.java | 2 +- .../geyser/api/network/message/Message.java | 16 +- .../api/network/message/MessageBuffer.java | 2 +- .../api/network/message/MessageCodec.java | 2 +- .../api/network/message/MessageFactory.java | 7 +- .../api/network/message/MessageHandler.java | 84 +++++ .../api/network/message/MessagePriority.java | 71 +++++ .../geyser/network/BaseNetworkChannel.java | 47 +++ .../network/ExtensionNetworkChannel.java | 18 +- .../network/ExternalNetworkChannel.java | 18 +- .../geyser/network/GeyserNetworkManager.java | 293 +++++++++++++++--- .../network/NetworkDefinitionBuilder.java | 167 ++++++++++ .../geyser}/network/PacketChannel.java | 19 +- .../network/message/BedrockPacketMessage.java | 2 +- .../geyser/network/message/ByteBufCodec.java | 53 ++-- .../network/message/ByteBufCodecLE.java | 53 ++-- .../network/message/JavaPacketMessage.java | 2 +- .../loader/ProviderRegistryLoader.java | 24 +- .../java/JavaCustomPayloadTranslator.java | 29 +- .../protocol/java/JavaLoginTranslator.java | 3 +- .../network/MessageRegistrationOrderTest.java | 208 +++++++++++++ .../network/NameVisibilityScoreboardTest.java | 4 +- .../network/ScoreboardIssueTests.java | 2 +- .../BasicBelownameScoreboardTests.java | 4 +- .../BasicPlayerlistScoreboardTests.java | 4 +- .../server/CubecraftScoreboardTest.java | 6 +- .../sidebar/BasicSidebarScoreboardTests.java | 4 +- .../OrderAndLimitSidebarScoreboardTests.java | 4 +- .../VanillaSidebarScoreboardTests.java | 4 +- .../util/GeyserMockContextScoreboard.java | 7 +- .../network => }/util/AssertUtils.java | 4 +- .../network => }/util/EmptyGeyserLogger.java | 4 +- .../network => }/util/GeyserMockContext.java | 26 +- 38 files changed, 1173 insertions(+), 318 deletions(-) delete mode 100644 api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/BaseNetworkChannel.java rename {api/src/main/java/org/geysermc/geyser/api => core/src/main/java/org/geysermc/geyser}/network/ExtensionNetworkChannel.java (76%) rename {api/src/main/java/org/geysermc/geyser/api => core/src/main/java/org/geysermc/geyser}/network/ExternalNetworkChannel.java (77%) create mode 100644 core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java rename {api/src/main/java/org/geysermc/geyser/api => core/src/main/java/org/geysermc/geyser}/network/PacketChannel.java (82%) create mode 100644 core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java rename core/src/test/java/org/geysermc/geyser/{scoreboard/network => }/util/AssertUtils.java (96%) rename core/src/test/java/org/geysermc/geyser/{scoreboard/network => }/util/EmptyGeyserLogger.java (94%) rename core/src/test/java/org/geysermc/geyser/{scoreboard/network => }/util/GeyserMockContext.java (93%) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index 29b91702596..85107e26cd9 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -29,13 +29,18 @@ import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; import org.geysermc.geyser.api.network.message.MessageCodec; import org.geysermc.geyser.api.network.message.MessageFactory; +import org.geysermc.geyser.api.network.message.MessageHandler; +import org.geysermc.geyser.api.network.message.MessagePriority; + +import java.util.function.Consumer; /** * Called whenever Geyser is registering network channels. - * @since 2.8.2 + * @since 2.9.1 */ public abstract class SessionDefineNetworkChannelsEvent extends ConnectionEvent { @@ -44,19 +49,159 @@ public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection) { } /** - * Registers a new network channel with a message factory. + * Defines the registration of a new network channel with a message factory. * * @param channel the channel to register * @param messageFactory the factory to create messages from the buffer + * @param the message type created by the factory + * @return a registration builder to configure handlers */ - public abstract void register(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory); + public abstract > Builder.@NonNull Initial define(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory); /** - * Registers a new network channel with a message factory. + * Defines the registration of a new network channel with a codec and message factory. * * @param channel the channel to register * @param codec the codec to use to encode/decode the buffer * @param messageFactory the factory to create messages from the buffer + * @param the buffer type + * @param the message type created by the factory + * @return a registration builder to configure handlers */ - public abstract void register(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory); + public abstract > Builder.@NonNull Initial define(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory); + + /** + * Registration builder for attaching handlers to a channel. + * + * @param the message type + */ + public interface Builder> { + + /** + * Configures the pipeline for this handler. + * + * @param pipeline the pipeline consumer + * @return the builder instance + */ + @NonNull + Builder pipeline(@NonNull Consumer pipeline); + + /** + * Finalizes the registration. + * + * @return the completed registration + */ + @NonNull + Registration register(); + + interface Initial> extends Sided, Bidirectional { + + /** + * {@inheritDoc} + */ + @Override + @NonNull + Initial pipeline(@NonNull Consumer pipeline); + } + + interface Sided> extends Builder { + + /** + * Register a clientbound handler. + */ + @NonNull + Sided clientbound(MessageHandler.@NonNull Sided handler); + + /** + * Register a clientbound handler with a priority. + */ + @NonNull + Sided clientbound(@NonNull MessagePriority priority, MessageHandler.@NonNull Sided handler); + + /** + * Register a serverbound handler. + */ + @NonNull + Sided serverbound(MessageHandler.@NonNull Sided handler); + + /** + * Register a serverbound handler with a priority. + */ + @NonNull + Sided serverbound(@NonNull MessagePriority priority, MessageHandler.@NonNull Sided handler); + + /** + * {@inheritDoc} + */ + @Override + @NonNull + Sided pipeline(@NonNull Consumer pipeline); + } + + interface Bidirectional> extends Builder { + + /** + * Register a bidirectional handler receiving the message and direction. + */ + @NonNull + Bidirectional bidirectional(@NonNull MessageHandler handler); + + /** + * Register a bidirectional handler with a priority. + */ + @NonNull + Bidirectional bidirectional(@NonNull MessagePriority priority, @NonNull MessageHandler handler); + + /** + * {@inheritDoc} + */ + @Override + @NonNull + Bidirectional pipeline(@NonNull Consumer pipeline); + } + + /** + * Pipeline configuration for ordering handlers. + */ + interface Pipeline { + + /** + * Tags this handler in the pipeline. + * + * @param tag the tag to apply + * @return the pipeline instance + */ + @NonNull + Pipeline tag(@NonNull String tag); + + /** + * Places this handler before the handler with the given tag. + *

+ * The tag used here must be previously defined in a separate handler using {@link #tag(String)}. + * However, it should be noted that if the specified tag does not exist at the time of registration, + * the handler will be added to the end of the pipeline without throwing an error. + * + * @param tag the tag to place before + * @return the pipeline instance + */ + @NonNull + Pipeline before(@NonNull String tag); + + /** + * Places this handler after the handler with the given tag. + *

+ * The tag used here must be previously defined in a separate handler using {@link #tag(String)}. + * However, it should be noted that if the specified tag does not exist at the time of registration, + * the handler will be added to the end of the pipeline without throwing an error. + * + * @param tag the tag to place after + * @return the pipeline instance + */ + @NonNull + Pipeline after(@NonNull String tag); + } + } + + public interface Registration> { + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java deleted file mode 100644 index 70035462c50..00000000000 --- a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2025 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.api.event.java; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.event.Cancellable; -import org.geysermc.geyser.api.connection.GeyserConnection; -import org.geysermc.geyser.api.event.connection.ConnectionEvent; -import org.geysermc.geyser.api.network.MessageDirection; -import org.geysermc.geyser.api.network.NetworkChannel; -import org.geysermc.geyser.api.network.message.Message; - -/** - * Called when Geyser receives a network message from the server. - * @since 2.8.2 - */ -public final class ServerReceiveNetworkMessageEvent extends ConnectionEvent implements Cancellable { - private final NetworkChannel channel; - private final Message message; - private final MessageDirection direction; - private boolean cancelled = false; - - public ServerReceiveNetworkMessageEvent(@NonNull GeyserConnection connection, @NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction) { - super(connection); - - this.channel = channel; - this.message = message; - this.direction = direction; - } - - /** - * Gets the channel that received the message. - *

- * See {@link NetworkChannel} for more information. - * - * @return the channel that received the message - */ - @NonNull - public NetworkChannel channel() { - return this.channel; - } - - /** - * Gets the message that was received. - * - * @return the received message - */ - @NonNull - public Message message() { - return this.message; - } - - /** - * Gets the direction of the message. - * - * @return the direction of the message - */ - @NonNull - public MessageDirection direction() { - return this.direction; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isCancelled() { - return this.cancelled; - } - - /** - * {@inheritDoc} - */ - @Override - public void setCancelled(boolean cancelled) { - this.cancelled = cancelled; - } -} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java index 16d4886b4ab..bcb569618d2 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java @@ -27,7 +27,7 @@ /** * Represents the direction of a message. - * @since 2.8.2 + * @since 2.9.1 */ public enum MessageDirection { /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java index 3c627d7ebde..c383c378ddd 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -27,6 +27,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.util.Identifier; @@ -67,11 +68,11 @@ * *

* Packet channels can also be registered against packet objects from - * exterbak protocol libraries, such as the ones provided in Geyser. For + * external protocol libraries, such as the ones provided in Geyser. For * an example on how to do this, please see the * Networking API documentation. * - * @since 2.8.2 + * @since 2.9.1 */ public interface NetworkChannel { @@ -91,58 +92,61 @@ public interface NetworkChannel { boolean isPacket(); /** - * Creates a new {@link NetworkChannel} instance. + * Creates a new external {@link NetworkChannel} instance. *

* Extensions should use this method to register * their own channels for more robust identification. * * @param extension the extension that registered this channel * @param channel the name of the channel - * @return a new {@link NetworkChannel} instance + * @param messageType the type of the message sent over this channel + * @return a new external {@link NetworkChannel} instance */ @NonNull - static NetworkChannel of(@NonNull Extension extension, @NonNull String channel) { - return new ExtensionNetworkChannel(extension, channel); + static NetworkChannel of(@NonNull Extension extension, @NonNull String channel, @NonNull Class messageType) { + return GeyserApi.api().provider(NetworkChannel.class, extension, channel, messageType); } /** - * Creates a new {@link NetworkChannel} instance. + * Creates a new external {@link NetworkChannel} instance. *

* This method is used for external channels provided * by third parties, such as plugins or mods. * * @param id the channel id * @param channel the name of the channel - * @return a new {@link NetworkChannel} instance + * @param messageType the type of the message sent over this channel + * @return a new external {@link NetworkChannel} instance */ @NonNull - static NetworkChannel of(@NonNull String id, @NonNull String channel) { - return of(Identifier.of(id, channel)); + static NetworkChannel of(@NonNull String id, @NonNull String channel, @NonNull Class messageType) { + return of(Identifier.of(id, channel), messageType); } /** - * Creates a new {@link NetworkChannel} instance. + * Creates a new external {@link NetworkChannel} instance. *

* This method is used for external channels provided * by third parties, such as plugins or mods. * * @param identifier the {@link Identifier} of the channel - * @return a new {@link NetworkChannel} instance + * @param messageType the type of the message sent over this channel + * @return a new external {@link NetworkChannel} instance */ @NonNull - static NetworkChannel of(@NonNull Identifier identifier) { - return new ExternalNetworkChannel(identifier); + static NetworkChannel of(@NonNull Identifier identifier, @NonNull Class messageType) { + return GeyserApi.api().provider(NetworkChannel.class, identifier, messageType); } /** - * Creates a new {@link PacketChannel} instance for a packet channel. + * Creates a new packet {@link NetworkChannel} instance for a packet channel. * * @param key the packet key * @param packetId the packet ID * @param packetType the type of the packet - * @return a new {@link PacketChannel} instance for a packet channel + * @return a new packet {@link NetworkChannel} instance for a packet channel */ static NetworkChannel packet(@NonNull String key, @NonNegative int packetId, @NonNull Class packetType) { - return new PacketChannel(key, packetId, packetType); + return GeyserApi.api().provider(NetworkChannel.class, key, packetId, packetType); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java index 9aa29115560..76eec94fd21 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java @@ -36,7 +36,7 @@ * Represents the network manager responsible for handling network operations * for a {@link GeyserConnection}. * - * @since 2.8.2 + * @since 2.9.1 */ public interface NetworkManager { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java index 6bbd8675358..a6933ed05a9 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java @@ -35,7 +35,7 @@ * Represents a data type that can be sent or received over the network. * * @param the type - * @since 2.8.2 + * @since 2.9.1 */ public final class DataType { /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java index e09e43b7de7..2cc81fd2c51 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java @@ -33,7 +33,7 @@ /** * Represents a message that can be sent over the network. - * @since 2.8.2 + * @since 2.9.1 */ public interface Message { @@ -69,8 +69,10 @@ interface Packet extends PacketBase { * @param packet the packet object to create the message from * @return a new packet message */ - static PacketWrapped of(@NonNull Object packet) { - return GeyserApi.api().provider(PacketWrapped.class, packet); + @SuppressWarnings("unchecked") + @NonNull + static PacketWrapped of(@NonNull Object packet) { + return (PacketWrapped) GeyserApi.api().provider(PacketWrapped.class, packet); } /** @@ -80,7 +82,7 @@ static PacketWrapped of(@NonNull Object packet) { * @return a new packet message */ @NonNull - static MessageFactory of(@NonNull Supplier packetSupplier) { + static MessageFactory> of(@NonNull Supplier

packetSupplier) { return buffer -> of(packetSupplier.get()); } @@ -92,7 +94,7 @@ static MessageFactory of(@NonNull Supplier * @return a new packet message factory */ @NonNull - static MessageFactory of(@NonNull Function substitutor, @NonNull Function packetSupplier) { + static MessageFactory> of(@NonNull Function substitutor, @NonNull Function packetSupplier) { return buffer -> of(packetSupplier.apply(substitutor.apply(buffer))); } } @@ -102,7 +104,7 @@ static MessageFactory of(@NonNull Function * * @param the type of message buffer */ - interface PacketWrapped extends PacketBase { + interface PacketWrapped extends PacketBase { /** * Gets the packet associated with this message. @@ -110,6 +112,6 @@ interface PacketWrapped extends PacketBase { * @return the packet */ @NonNull - Object packet(); + P packet(); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java index 7b47ecaf198..678bbbbcceb 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java @@ -29,7 +29,7 @@ /** * A buffer for messages that can be sent over the network. - * @since 2.8.2 + * @since 2.9.1 */ public interface MessageBuffer { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java index b3f43c335ed..8e720b31381 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java @@ -31,7 +31,7 @@ * A codec for encoding and decoding messages. * * @param the type of {@link MessageBuffer} used for encoding and decoding - * @since 2.8.2 + * @since 2.9.1 */ public interface MessageCodec { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java index 3d205823a3a..14c3c7bb3cf 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java @@ -31,10 +31,11 @@ * A factory interface for creating messages from a given message buffer. * * @param the type of the message buffer - * @since 2.8.2 + * @param the type of the message + * @since 2.9.1 */ @FunctionalInterface -public interface MessageFactory { +public interface MessageFactory> { /** * Creates a new message from the provided buffer. @@ -43,5 +44,5 @@ public interface MessageFactory { * @return a new message created from the buffer */ @NonNull - Message create(@NonNull T buffer); + M create(@NonNull T buffer); } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java new file mode 100644 index 00000000000..c5fb760ee90 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.MessageDirection; + +/** + * Represents a handler for processing messages. + * + * @param the type of message to handle + */ +@FunctionalInterface +public interface MessageHandler> { + + /** + * Handles the given message in the specified direction. + * + * @param message the message to handle + * @param direction the direction of the message + * @return the state after handling the message + */ + @NonNull + State handle(@NonNull T message, @NonNull MessageDirection direction); + + /** + * A message handler that belongs to a specific side (clientbound or serverbound). + * + * @param the type of message to handle + */ + interface Sided> { + + /** + * Handles the given message in the specified direction. + * + * @param message the message to handle + * @return the state after handling the message + */ + @NonNull + State handle(@NonNull T message); + } + + /** + * Represents the state after handling a message. + */ + enum State { + /** + * The message was handled and should not be processed further. + */ + HANDLED, + /** + * The message was not handled and should be passed through for further processing. + */ + UNHANDLED, + /** + * Indicates that the message has been modified but should still be + * passed through to the next handler or processing step. + */ + MODIFIED + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java new file mode 100644 index 00000000000..de5dfe1a210 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network.message; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents the priority of a message when being processed. + * @since 2.9.1 + */ +public enum MessagePriority { + FIRST(100), + EARLY(50), + NORMAL(0), + LATE(-50), + LAST(-100); + + private final int value; + + MessagePriority(int value) { + this.value = value; + } + + /** + * Gets the numeric value associated with this priority. Higher means earlier. + * + * @return the priority value + */ + public int value() { + return value; + } + + /** + * Creates a custom priority in the range [-100, 100]. + * + * @param value the priority value + * @return the priority + * @throws IllegalArgumentException if outside allowed range + */ + @NonNull + public static MessagePriority of(int value) { + if (value >= 75) return LAST; + if (value >= 25) return LATE; + if (value <= -75) return FIRST; + if (value <= -25) return EARLY; + return NORMAL; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/BaseNetworkChannel.java b/core/src/main/java/org/geysermc/geyser/network/BaseNetworkChannel.java new file mode 100644 index 00000000000..e2f26c00b99 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/BaseNetworkChannel.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.NetworkChannel; + +public abstract class BaseNetworkChannel implements NetworkChannel { + private final Class messageType; + + public BaseNetworkChannel(Class messageType) { + this.messageType = messageType; + } + + /** + * Gets the type of message this channel handles. + * + * @return the message type + */ + @NonNull + public Class messageType() { + return messageType; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java b/core/src/main/java/org/geysermc/geyser/network/ExtensionNetworkChannel.java similarity index 76% rename from api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java rename to core/src/main/java/org/geysermc/geyser/network/ExtensionNetworkChannel.java index 9cc3413d867..5afe0d4b1b3 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java +++ b/core/src/main/java/org/geysermc/geyser/network/ExtensionNetworkChannel.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.network; +package org.geysermc.geyser.network; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.extension.Extension; @@ -33,13 +33,14 @@ /** * Represents a network channel associated with an extension. - * @since 2.8.2 */ -public class ExtensionNetworkChannel implements NetworkChannel { +public class ExtensionNetworkChannel extends BaseNetworkChannel { private final Extension extension; private final String channel; - protected ExtensionNetworkChannel(@NonNull Extension extension, @NonNull String channel) { + public ExtensionNetworkChannel(@NonNull Extension extension, @NonNull String channel, @NonNull Class messageType) { + super(messageType); + this.extension = extension; this.channel = channel; } @@ -63,14 +64,14 @@ public boolean isPacket() { @Override public boolean equals(Object o) { - if (o == null || !NetworkChannel.class.isAssignableFrom(o.getClass())) return false; - NetworkChannel that = (NetworkChannel) o; - return Objects.equals(this.identifier(), that.identifier()); + if (o == null || getClass() != o.getClass()) return false; + ExtensionNetworkChannel that = (ExtensionNetworkChannel) o; + return Objects.equals(this.extension, that.extension) && Objects.equals(this.channel, that.channel) && Objects.equals(this.messageType(), that.messageType()); } @Override public int hashCode() { - return Objects.hash(this.identifier()); + return Objects.hash(this.identifier(), this.messageType()); } @Override @@ -78,6 +79,7 @@ public String toString() { return "ExtensionNetworkChannel{" + "extension=" + this.extension.description().id() + ", channel='" + this.channel + '\'' + + ", messageType=" + this.messageType() + '}'; } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java b/core/src/main/java/org/geysermc/geyser/network/ExternalNetworkChannel.java similarity index 77% rename from api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java rename to core/src/main/java/org/geysermc/geyser/network/ExternalNetworkChannel.java index 479727a15f1..bce86b10896 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/ExternalNetworkChannel.java +++ b/core/src/main/java/org/geysermc/geyser/network/ExternalNetworkChannel.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.network; +package org.geysermc.geyser.network; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.util.Identifier; @@ -34,12 +34,13 @@ * Represents a network channel not associated with any specific extension. *

* This can be used for external communication channels, like mods or plugins. - * @since 2.8.2 */ -public class ExternalNetworkChannel implements NetworkChannel { +public class ExternalNetworkChannel extends BaseNetworkChannel { private final Identifier identifier; - protected ExternalNetworkChannel(@NonNull Identifier identifier) { + public ExternalNetworkChannel(@NonNull Identifier identifier, @NonNull Class messageType) { + super(messageType); + this.identifier = identifier; } @@ -62,20 +63,21 @@ public boolean isPacket() { @Override public boolean equals(Object o) { - if (o == null || !NetworkChannel.class.isAssignableFrom(o.getClass())) return false; - NetworkChannel that = (NetworkChannel) o; - return Objects.equals(this.identifier(), that.identifier()); + if (o == null || getClass() != o.getClass()) return false; + ExternalNetworkChannel that = (ExternalNetworkChannel) o; + return Objects.equals(this.identifier, that.identifier) && Objects.equals(this.messageType(), that.messageType()); } @Override public int hashCode() { - return Objects.hash(this.identifier()); + return Objects.hash(this.identifier(), this.messageType()); } @Override public String toString() { return "ExternalNetworkChannel{" + "identifier='" + this.identifier + '\'' + + ", messageType=" + this.messageType() + '}'; } } diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java index 312958060bc..5b795229fee 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java @@ -31,30 +31,33 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodecHelper; import org.cloudburstmc.protocol.bedrock.codec.BedrockPacketDefinition; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; -import org.geysermc.geyser.api.event.java.ServerReceiveNetworkMessageEvent; import org.geysermc.geyser.api.network.MessageDirection; import org.geysermc.geyser.api.network.NetworkChannel; import org.geysermc.geyser.api.network.NetworkManager; -import org.geysermc.geyser.api.network.PacketChannel; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; import org.geysermc.geyser.api.network.message.MessageCodec; import org.geysermc.geyser.api.network.message.MessageFactory; +import org.geysermc.geyser.api.network.message.MessageHandler; import org.geysermc.geyser.network.message.BedrockPacketMessage; import org.geysermc.geyser.network.message.ByteBufCodec; import org.geysermc.geyser.network.message.ByteBufMessageBuffer; import org.geysermc.geyser.network.message.JavaPacketMessage; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; -import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; -import java.util.HashMap; +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -62,7 +65,7 @@ public class GeyserNetworkManager implements NetworkManager { private final GeyserSession session; - private final Map> definitions = new HashMap<>(); + private final Map>> definitions = new LinkedHashMap<>(); private final Int2ObjectMap packetChannels = new Int2ObjectOpenHashMap<>(); public GeyserNetworkManager(GeyserSession session) { @@ -71,31 +74,77 @@ public GeyserNetworkManager(GeyserSession session) { SessionDefineNetworkChannelsEvent event = new SessionDefineNetworkChannelsEvent(session) { @Override - public void register(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory) { - GeyserNetworkManager.this.registerMessage(channel, new MessageDefinition<>(ByteBufCodec.INSTANCE, messageFactory)); + public > Builder.@NonNull Initial define(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory) { + return new NetworkDefinitionBuilder<>(registration -> onRegister(channel, ByteBufCodec.INSTANCE, messageFactory, registration)); } @Override - public void register(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory) { - GeyserNetworkManager.this.registerMessage(channel, new MessageDefinition<>(codec, messageFactory)); + public > Builder.@NonNull Initial define(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory) { + return new NetworkDefinitionBuilder<>(registration -> onRegister(channel, codec, messageFactory, registration)); } }; - GeyserApi.api().eventBus().fire(event); + GeyserImpl.getInstance().getEventBus().fire(event); + } + + @VisibleForTesting + > void onRegister(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory, NetworkDefinitionBuilder.@NonNull RegistrationImpl registration) { + this.onRegister(channel, ByteBufCodec.INSTANCE, messageFactory, registration); + } + + @SuppressWarnings("unchecked") + private > void onRegister(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, + @NonNull MessageFactory messageFactory, NetworkDefinitionBuilder.@NonNull RegistrationImpl registration) { + MessageHandler handler; + int priority; + + NetworkDefinitionBuilder.HandlerEntry bidirectional = registration.handler(); + NetworkDefinitionBuilder.SidedHandlerEntry clientbound = registration.clientbound(); + NetworkDefinitionBuilder.SidedHandlerEntry serverbound = registration.serverbound(); + + if (bidirectional != null) { + handler = bidirectional.handler(); + priority = bidirectional.priority() != null ? bidirectional.priority().value() : 0; + } else { + handler = null; + + int cbPriority = clientbound != null && clientbound.priority() != null ? clientbound.priority().value() : Integer.MIN_VALUE; + int sbPriority = serverbound != null && serverbound.priority() != null ? serverbound.priority().value() : Integer.MIN_VALUE; + priority = Math.max(cbPriority, sbPriority); + + if (priority == Integer.MIN_VALUE) { + priority = 0; + } + } + + MessageDefinition definition = new MessageDefinition<>((MessageCodec) codec, + messageFactory, + handler, + clientbound != null ? clientbound.handler() : null, + serverbound != null ? serverbound.handler() : null, + priority, + clientbound != null && clientbound.priority() != null ? clientbound.priority().value() : null, + serverbound != null && serverbound.priority() != null ? serverbound.priority().value() : null, + registration.tag(), + registration.beforeTag(), + registration.afterTag() + ); + + this.registerMessage(channel, definition); } @Override - public @NonNull Set registeredChannels() { + @Nonnull + public Set registeredChannels() { return Set.copyOf(this.definitions.keySet()); } - @SuppressWarnings("unchecked") @Override public void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction) { if (channel.isPacket() && message instanceof Message.PacketBase packetBase) { - if (packetBase instanceof BedrockPacketMessage packetMessage) { + if (packetBase instanceof BedrockPacketMessage packetMessage) { this.session.sendUpstreamPacket(packetMessage.packet()); - } else if (packetBase instanceof JavaPacketMessage packetMessage) { + } else if (packetBase instanceof JavaPacketMessage packetMessage) { this.session.sendDownstreamPacket(packetMessage.packet()); } else if (packetBase instanceof Message.Packet packet) { PacketChannel packetChannel = (PacketChannel) channel; @@ -123,10 +172,7 @@ public void send(@NonNull NetworkChannel channel, @Non return; } - MessageDefinition definition = (MessageDefinition) this.definitions.get(channel); - if (definition == null) { - throw new IllegalArgumentException("No message definition registered for channel: " + channel); - } + MessageDefinition> definition = this.findMessageDefinition(channel, message); T buffer = definition.codec.createBuffer(); message.encode(buffer); @@ -139,29 +185,40 @@ public void send(@NonNull NetworkChannel channel, @Non this.session.sendDownstreamPacket(packet); } - public Message createMessage(@NonNull NetworkChannel channel, byte @NotNull[] data) { - return this.createMessage0(channel, definition -> definition.createBuffer(data)); + @NonNull + public List> createMessages(@NonNull NetworkChannel channel, byte @NonNull[] data) { + return this.createMessages0(channel, definition -> definition.createBuffer(data)); } - public Message createMessage(@NonNull NetworkChannel channel, @NonNull T buffer) { - return this.createMessage0(channel, def -> buffer); + @NonNull + public List> createMessages(@NonNull NetworkChannel channel, @NonNull T buffer) { + return this.createMessages0(channel, def -> buffer); } @SuppressWarnings("unchecked") - private Message createMessage0(@NonNull NetworkChannel channel, @NonNull Function, T> creator) { - MessageDefinition definition = (MessageDefinition) this.definitions.get(channel); - if (definition == null) { + @NonNull + private > List createMessages0(@NonNull NetworkChannel channel, @NonNull Function, T> creator) { + List> definitions = this.definitions.get(channel); + if (definitions == null || definitions.isEmpty()) { throw new IllegalArgumentException("No message definition registered for channel: " + channel); } - T buffer = creator.apply(definition); - Message message = definition.createMessage(buffer); - if (message instanceof BedrockPacketMessage packetMessage) { - packetMessage.postProcess(this.session, (ByteBufMessageBuffer) buffer); + List messages = new ArrayList<>(); + for (MessageDefinition def : definitions) { + MessageDefinition definition = (MessageDefinition) def; + T buffer = creator.apply(definition); + M message = definition.createMessage(buffer); + if (message instanceof BedrockPacketMessage packetMessage) { + packetMessage.postProcess(this.session, (ByteBufMessageBuffer) buffer); + } + + messages.add(message); } - return message; + + return messages; } + @Nullable public PacketChannel getPacketChannel(int packetId) { return this.packetChannels.get(packetId); } @@ -179,42 +236,184 @@ public boolean handlePacket(BedrockPacket packet, MessageDirection direction) { return true; } - Message message; - if (channel.packetType().isInstance(packet)) { - message = new BedrockPacketMessage(packet); + List> messages; + if (channel.messageType().isInstance(packet)) { + messages = List.of(new BedrockPacketMessage<>(packet)); } else { ByteBuf buffer = Unpooled.buffer(); definition.getSerializer().serialize(buffer, this.session.getUpstream().getCodecHelper(), packet); - message = this.createMessage(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer)); + messages = this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer)); + } + + return this.handleMessages(channel, messages, direction); + } + + @SuppressWarnings("unchecked") + public boolean handleMessages(@NonNull NetworkChannel channel, @NonNull List> messages, @NonNull MessageDirection direction) { + List> rawList = this.definitions.get(channel); + if (rawList == null || rawList.isEmpty()) { + return true; + } + + // Build a direction-aware ordered list while preserving pipeline tag anchors + List> ordered = new ArrayList<>(); + List> unpinnedBlock = new ArrayList<>(); + for (MessageDefinition def : rawList) { + boolean pinned = def.tag() != null || def.beforeTag() != null || def.afterTag() != null; + if (pinned) { + // flush any accumulated unpinned block sorted by effective priority for this direction + if (!unpinnedBlock.isEmpty()) { + unpinnedBlock.sort((a, b) -> Integer.compare( + b.priority(direction), + a.priority(direction) + )); + ordered.addAll(unpinnedBlock); + unpinnedBlock.clear(); + } + ordered.add(def); + } else { + unpinnedBlock.add(def); + } + } + if (!unpinnedBlock.isEmpty()) { + unpinnedBlock.sort((a, b) -> Integer.compare( + b.priority(direction), + a.priority(direction) + )); + ordered.addAll(unpinnedBlock); + unpinnedBlock.clear(); + } + + for (Message message : messages) { + for (MessageDefinition def : ordered) { + if (!(channel instanceof BaseNetworkChannel base) || !base.messageType().isInstance(message)) { + continue; + } + + MessageDefinition> definition = (MessageDefinition>) def; + + MessageHandler.State state; + if (definition.handler != null) { + state = definition.handler.handle(message, direction); + } else if (direction == MessageDirection.CLIENTBOUND && definition.clientboundHandler != null) { + state = definition.clientboundHandler.handle(message); + } else if (direction == MessageDirection.SERVERBOUND && definition.serverboundHandler != null) { + state = definition.serverboundHandler.handle(message); + } else { + continue; // no suitable handler; try next definition + } + + if (state == MessageHandler.State.HANDLED) { + return false; + } + } + } + + return true; + } + + @SuppressWarnings("unchecked") + @NonNull + private > MessageDefinition findMessageDefinition(@NonNull NetworkChannel channel, @NonNull Message message) { + List> definitions = this.definitions.get(channel); + if (definitions == null || definitions.isEmpty()) { + throw new IllegalArgumentException("No message definition registered for channel: " + channel); } - ServerReceiveNetworkMessageEvent event = new ServerReceiveNetworkMessageEvent(this.session, channel, message, direction); - this.session.getGeyser().eventBus().fire(event); + MessageDefinition> definition = null; + for (MessageDefinition def : definitions) { + if (channel instanceof BaseNetworkChannel baseChannel) { + if (baseChannel.messageType().isInstance(message)) { + definition = (MessageDefinition>) def; + break; + } + } + } - // If the event is canceled, we do not want to process the packet further - return !event.isCancelled(); + if (definition == null) { + throw new IllegalArgumentException("No suitable message definition found for channel: " + channel + " and message type: " + message.getClass()); + } + + return (MessageDefinition) definition; } - private void registerMessage(@NonNull NetworkChannel channel, @NonNull MessageDefinition codec) { - if (this.definitions.containsKey(channel)) { - throw new IllegalArgumentException("Channel is already registered: " + channel); + private > void registerMessage(@NonNull NetworkChannel channel, @NonNull MessageDefinition definition) { + List> list = this.definitions.computeIfAbsent(channel, key -> new ArrayList<>()); + + // Determine the insert position based on pipeline tags or priority + int insertIndex = -1; + if (definition.beforeTag() != null) { + for (int i = 0; i < list.size(); i++) { + MessageDefinition existing = list.get(i); + if (definition.beforeTag().equals(existing.tag())) { + insertIndex = i; + break; + } + } + } else if (definition.afterTag() != null) { + for (int i = 0; i < list.size(); i++) { + MessageDefinition existing = list.get(i); + if (definition.afterTag().equals(existing.tag())) { + insertIndex = i + 1; + break; + } + } + } + + if (insertIndex == -1) { + // Fallback: insert by descending priority + insertIndex = list.size(); + for (int i = 0; i < list.size(); i++) { + MessageDefinition existing = list.get(i); + if (definition.priority() > existing.priority()) { + insertIndex = i; + break; + } + } } - this.definitions.put(channel, codec); + list.add(insertIndex, definition); + if (channel.isPacket() && channel instanceof PacketChannel packetChannel) { int packetId = packetChannel.packetId(); this.packetChannels.put(packetId, packetChannel); } } - public record MessageDefinition(MessageCodec codec, MessageFactory messageFactory) { - - public T createBuffer(byte @NotNull[] data) { + public record MessageDefinition>( + @NonNull MessageCodec codec, + @NonNull MessageFactory messageFactory, + @Nullable MessageHandler handler, + MessageHandler.Sided clientboundHandler, + MessageHandler.Sided serverboundHandler, + int priority, + @Nullable Integer clientboundPriority, + @Nullable Integer serverboundPriority, + @Nullable String tag, + @Nullable String beforeTag, + @Nullable String afterTag + ) { + + @NonNull + public T createBuffer(byte @NonNull[] data) { return this.codec.createBuffer(data); } - public Message createMessage(@NonNull T buffer) { + @NonNull + public M createMessage(@NonNull T buffer) { return this.messageFactory.create(buffer); } + + public int priority(@NonNull MessageDirection direction) { + if (this.handler != null) { + return this.priority; + } + + if (direction == MessageDirection.CLIENTBOUND) { + return this.clientboundPriority != null ? this.clientboundPriority : 0; + } + + return this.serverboundPriority != null ? this.serverboundPriority : 0; + } } } diff --git a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java new file mode 100644 index 00000000000..be65967332e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network; + +import com.google.common.base.Preconditions; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.api.network.message.MessageBuffer; +import org.geysermc.geyser.api.network.message.MessageHandler; +import org.geysermc.geyser.api.network.message.MessagePriority; + +import java.util.Objects; +import java.util.function.Consumer; + +public class NetworkDefinitionBuilder> implements SessionDefineNetworkChannelsEvent.Builder.Initial { + private HandlerEntry handler; + private SidedHandlerEntry clientbound; + private SidedHandlerEntry serverbound; + + private String tag; + private String beforeTag; + private String afterTag; + + private final Consumer> registrationCallback; + private boolean registered; + + public NetworkDefinitionBuilder(@NonNull Consumer> registrationCallback) { + this.registrationCallback = registrationCallback; + } + + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Initial pipeline(@NonNull Consumer pipeline) { + Objects.requireNonNull(pipeline, "pipeline"); + PipelineImpl impl = new PipelineImpl(); + pipeline.accept(impl); + this.tag = impl.tag; + this.beforeTag = impl.beforeTag; + this.afterTag = impl.afterTag; + return this; + } + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Sided clientbound(MessageHandler.@NonNull Sided handler) { + return this.clientbound(MessagePriority.NORMAL, handler); + } + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Sided clientbound(@NonNull MessagePriority priority, MessageHandler.@NonNull Sided handler) { + Objects.requireNonNull(priority, "priority"); + Objects.requireNonNull(handler, "handler"); + this.clientbound = new SidedHandlerEntry<>(priority, handler); + return this; + } + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Sided serverbound(MessageHandler.@NonNull Sided handler) { + return this.serverbound(MessagePriority.NORMAL, handler); + } + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Sided serverbound(@NonNull MessagePriority priority, MessageHandler.@NonNull Sided handler) { + Objects.requireNonNull(priority, "priority"); + Objects.requireNonNull(handler, "handler"); + this.serverbound = new SidedHandlerEntry<>(priority, handler); + return this; + } + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Bidirectional bidirectional(@NonNull MessageHandler handler) { + return this.bidirectional(MessagePriority.NORMAL, handler); + } + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Bidirectional bidirectional(@NonNull MessagePriority priority, @NonNull MessageHandler handler) { + Objects.requireNonNull(priority, "priority"); + Objects.requireNonNull(handler, "handler"); + this.handler = new HandlerEntry<>(priority, handler); + return this; + } + + @Override + public SessionDefineNetworkChannelsEvent.@NonNull Registration register() { + Preconditions.checkState(!this.registered, "This message has already been registered"); + Preconditions.checkState(this.handler == null || (this.clientbound == null && this.serverbound == null), "Cannot register both bidirectional and sided handlers for the same message"); + RegistrationImpl registration = new RegistrationImpl<>( + this.handler, + this.clientbound, + this.serverbound, + this.tag, + this.beforeTag, + this.afterTag + ); + + this.registered = true; + this.registrationCallback.accept(registration); + return registration; + } + + private static final class PipelineImpl implements SessionDefineNetworkChannelsEvent.Builder.Pipeline { + private String tag; + private String beforeTag; + private String afterTag; + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Pipeline tag(@NonNull String tag) { + this.tag = Objects.requireNonNull(tag, "tag"); + return this; + } + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Pipeline before(@NonNull String tag) { + this.beforeTag = Objects.requireNonNull(tag, "tag"); + return this; + } + + @Override + public SessionDefineNetworkChannelsEvent.Builder.@NonNull Pipeline after(@NonNull String tag) { + this.afterTag = Objects.requireNonNull(tag, "tag"); + return this; + } + } + + public record RegistrationImpl>( + HandlerEntry handler, + SidedHandlerEntry clientbound, + SidedHandlerEntry serverbound, + String tag, + String beforeTag, + String afterTag + ) implements SessionDefineNetworkChannelsEvent.Registration { + } + + public record SidedHandlerEntry>( + MessagePriority priority, + MessageHandler.Sided handler + ) { + } + + public record HandlerEntry>( + MessagePriority priority, + MessageHandler handler + ) { + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java b/core/src/main/java/org/geysermc/geyser/network/PacketChannel.java similarity index 82% rename from api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java rename to core/src/main/java/org/geysermc/geyser/network/PacketChannel.java index 8067cc59d9e..9c8fc25d999 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java +++ b/core/src/main/java/org/geysermc/geyser/network/PacketChannel.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.network; +package org.geysermc.geyser.network; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; @@ -37,19 +37,16 @@ * This channel is used for listening to communication over * packets between the server and client and can be used to * send or receive packets. - * @since 2.8.2 */ public class PacketChannel extends ExternalNetworkChannel { private static final String PACKET_CHANNEL_KEY = "packet"; private final int packetId; - private final Class packetType; - protected PacketChannel(@NonNull String key, @NonNegative int packetId, @NonNull Class packetType) { - super(Identifier.of(PACKET_CHANNEL_KEY, key)); + public PacketChannel(@NonNull String key, @NonNegative int packetId, @NonNull Class packetType) { + super(Identifier.of(PACKET_CHANNEL_KEY, key), packetType); this.packetId = packetId; - this.packetType = packetType; } /** @@ -62,16 +59,6 @@ public int packetId() { return this.packetId; } - /** - * Gets the type of the packet associated with this channel. - * - * @return the class of the packet type - */ - @NonNull - public Class packetType() { - return this.packetType; - } - /** * {@inheritDoc} */ diff --git a/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java b/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java index 3a09d648aef..744231f316a 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java @@ -33,7 +33,7 @@ import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.session.GeyserSession; -public record BedrockPacketMessage(@NonNull BedrockPacket packet) implements Message.PacketWrapped { +public record BedrockPacketMessage(@NonNull T packet) implements Message.PacketWrapped { @SuppressWarnings("unchecked") public void postProcess(@NonNull GeyserSession session, @NonNull ByteBufMessageBuffer buffer) { diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java index d73e768cf89..272170c4c65 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java @@ -29,7 +29,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.common.util.VarInts; import org.geysermc.geyser.api.network.message.MessageCodec; -import org.jetbrains.annotations.NotNull; import java.nio.charset.StandardCharsets; @@ -41,62 +40,62 @@ private ByteBufCodec() { } @Override - public boolean readBoolean(@NotNull ByteBufMessageBuffer buffer) { + public boolean readBoolean(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readBoolean(); } @Override - public byte readByte(@NotNull ByteBufMessageBuffer buffer) { + public byte readByte(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readByte(); } @Override - public short readShort(@NotNull ByteBufMessageBuffer buffer) { + public short readShort(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readShort(); } @Override - public int readInt(@NotNull ByteBufMessageBuffer buffer) { + public int readInt(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readInt(); } @Override - public float readFloat(@NotNull ByteBufMessageBuffer buffer) { + public float readFloat(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readFloat(); } @Override - public double readDouble(@NotNull ByteBufMessageBuffer buffer) { + public double readDouble(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readDouble(); } @Override - public long readLong(@NotNull ByteBufMessageBuffer buffer) { + public long readLong(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readLong(); } @Override - public int readVarInt(@NotNull ByteBufMessageBuffer buffer) { + public int readVarInt(@NonNull ByteBufMessageBuffer buffer) { return VarInts.readInt(buffer.buffer()); } @Override - public int readUnsignedVarInt(@NotNull ByteBufMessageBuffer buffer) { + public int readUnsignedVarInt(@NonNull ByteBufMessageBuffer buffer) { return VarInts.readUnsignedInt(buffer.buffer()); } @Override - public long readVarLong(@NotNull ByteBufMessageBuffer buffer) { + public long readVarLong(@NonNull ByteBufMessageBuffer buffer) { return VarInts.readLong(buffer.buffer()); } @Override - public long readUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer) { + public long readUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer) { return VarInts.readUnsignedLong(buffer.buffer()); } @Override - public @NotNull String readString(@NotNull ByteBufMessageBuffer buffer) { + public @NonNull String readString(@NonNull ByteBufMessageBuffer buffer) { int size = VarInts.readUnsignedInt(buffer.buffer()); byte[] bytes = new byte[size]; buffer.buffer().readBytes(bytes); @@ -105,74 +104,74 @@ public long readUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer) { } @Override - public void writeBoolean(@NotNull ByteBufMessageBuffer buffer, boolean value) { + public void writeBoolean(@NonNull ByteBufMessageBuffer buffer, boolean value) { buffer.buffer().writeBoolean(value); } @Override - public void writeByte(@NotNull ByteBufMessageBuffer buffer, byte value) { + public void writeByte(@NonNull ByteBufMessageBuffer buffer, byte value) { buffer.buffer().writeByte(value); } @Override - public void writeShort(@NotNull ByteBufMessageBuffer buffer, short value) { + public void writeShort(@NonNull ByteBufMessageBuffer buffer, short value) { buffer.buffer().writeShort(value); } @Override - public void writeInt(@NotNull ByteBufMessageBuffer buffer, int value) { + public void writeInt(@NonNull ByteBufMessageBuffer buffer, int value) { buffer.buffer().writeInt(value); } @Override - public void writeFloat(@NotNull ByteBufMessageBuffer buffer, float value) { + public void writeFloat(@NonNull ByteBufMessageBuffer buffer, float value) { buffer.buffer().writeFloat(value); } @Override - public void writeDouble(@NotNull ByteBufMessageBuffer buffer, double value) { + public void writeDouble(@NonNull ByteBufMessageBuffer buffer, double value) { buffer.buffer().writeDouble(value); } @Override - public void writeLong(@NotNull ByteBufMessageBuffer buffer, long value) { + public void writeLong(@NonNull ByteBufMessageBuffer buffer, long value) { buffer.buffer().writeLong(value); } @Override - public void writeVarInt(@NotNull ByteBufMessageBuffer buffer, int value) { + public void writeVarInt(@NonNull ByteBufMessageBuffer buffer, int value) { VarInts.writeInt(buffer.buffer(), value); } @Override - public void writeUnsignedVarInt(@NotNull ByteBufMessageBuffer buffer, int value) { + public void writeUnsignedVarInt(@NonNull ByteBufMessageBuffer buffer, int value) { VarInts.writeUnsignedInt(buffer.buffer(), value); } @Override - public void writeVarLong(@NotNull ByteBufMessageBuffer buffer, long value) { + public void writeVarLong(@NonNull ByteBufMessageBuffer buffer, long value) { VarInts.writeLong(buffer.buffer(), value); } @Override - public void writeUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer, long value) { + public void writeUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer, long value) { VarInts.writeUnsignedLong(buffer.buffer(), value); } @Override - public void writeString(@NotNull ByteBufMessageBuffer buffer, @NonNull String value) { + public void writeString(@NonNull ByteBufMessageBuffer buffer, @NonNull String value) { byte[] bytes = value.getBytes(StandardCharsets.UTF_8); VarInts.writeUnsignedInt(buffer.buffer(), bytes.length); buffer.buffer().writeBytes(bytes); } @Override - public @NotNull ByteBufMessageBuffer createBuffer() { + public @NonNull ByteBufMessageBuffer createBuffer() { return new ByteBufMessageBuffer(this); } @Override - public @NotNull ByteBufMessageBuffer createBuffer(byte @NotNull [] data) { + public @NonNull ByteBufMessageBuffer createBuffer(byte @NonNull [] data) { return new ByteBufMessageBuffer(this, Unpooled.wrappedBuffer(data)); } } diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java index 98e401e7344..c409b1a02d2 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java @@ -29,7 +29,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.common.util.VarInts; import org.geysermc.geyser.api.network.message.MessageCodec; -import org.jetbrains.annotations.NotNull; import java.nio.charset.StandardCharsets; @@ -39,62 +38,62 @@ public class ByteBufCodecLE implements MessageCodec { } @Override - public boolean readBoolean(@NotNull ByteBufMessageBuffer buffer) { + public boolean readBoolean(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readBoolean(); } @Override - public byte readByte(@NotNull ByteBufMessageBuffer buffer) { + public byte readByte(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readByte(); } @Override - public short readShort(@NotNull ByteBufMessageBuffer buffer) { + public short readShort(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readShortLE(); } @Override - public int readInt(@NotNull ByteBufMessageBuffer buffer) { + public int readInt(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readIntLE(); } @Override - public float readFloat(@NotNull ByteBufMessageBuffer buffer) { + public float readFloat(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readFloatLE(); } @Override - public double readDouble(@NotNull ByteBufMessageBuffer buffer) { + public double readDouble(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readDoubleLE(); } @Override - public long readLong(@NotNull ByteBufMessageBuffer buffer) { + public long readLong(@NonNull ByteBufMessageBuffer buffer) { return buffer.buffer().readLongLE(); } @Override - public int readVarInt(@NotNull ByteBufMessageBuffer buffer) { + public int readVarInt(@NonNull ByteBufMessageBuffer buffer) { return VarInts.readInt(buffer.buffer()); } @Override - public int readUnsignedVarInt(@NotNull ByteBufMessageBuffer buffer) { + public int readUnsignedVarInt(@NonNull ByteBufMessageBuffer buffer) { return VarInts.readUnsignedInt(buffer.buffer()); } @Override - public long readVarLong(@NotNull ByteBufMessageBuffer buffer) { + public long readVarLong(@NonNull ByteBufMessageBuffer buffer) { return VarInts.readLong(buffer.buffer()); } @Override - public long readUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer) { + public long readUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer) { return VarInts.readUnsignedLong(buffer.buffer()); } @Override - public @NotNull String readString(@NotNull ByteBufMessageBuffer buffer) { + public @NonNull String readString(@NonNull ByteBufMessageBuffer buffer) { int size = VarInts.readUnsignedInt(buffer.buffer()); byte[] bytes = new byte[size]; buffer.buffer().readBytes(bytes); @@ -103,74 +102,74 @@ public long readUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer) { } @Override - public void writeBoolean(@NotNull ByteBufMessageBuffer buffer, boolean value) { + public void writeBoolean(@NonNull ByteBufMessageBuffer buffer, boolean value) { buffer.buffer().writeBoolean(value); } @Override - public void writeByte(@NotNull ByteBufMessageBuffer buffer, byte value) { + public void writeByte(@NonNull ByteBufMessageBuffer buffer, byte value) { buffer.buffer().writeByte(value); } @Override - public void writeShort(@NotNull ByteBufMessageBuffer buffer, short value) { + public void writeShort(@NonNull ByteBufMessageBuffer buffer, short value) { buffer.buffer().writeShortLE(value); } @Override - public void writeInt(@NotNull ByteBufMessageBuffer buffer, int value) { + public void writeInt(@NonNull ByteBufMessageBuffer buffer, int value) { buffer.buffer().writeIntLE(value); } @Override - public void writeFloat(@NotNull ByteBufMessageBuffer buffer, float value) { + public void writeFloat(@NonNull ByteBufMessageBuffer buffer, float value) { buffer.buffer().writeFloatLE(value); } @Override - public void writeDouble(@NotNull ByteBufMessageBuffer buffer, double value) { + public void writeDouble(@NonNull ByteBufMessageBuffer buffer, double value) { buffer.buffer().writeDoubleLE(value); } @Override - public void writeLong(@NotNull ByteBufMessageBuffer buffer, long value) { + public void writeLong(@NonNull ByteBufMessageBuffer buffer, long value) { buffer.buffer().writeLongLE(value); } @Override - public void writeVarInt(@NotNull ByteBufMessageBuffer buffer, int value) { + public void writeVarInt(@NonNull ByteBufMessageBuffer buffer, int value) { VarInts.writeInt(buffer.buffer(), value); } @Override - public void writeUnsignedVarInt(@NotNull ByteBufMessageBuffer buffer, int value) { + public void writeUnsignedVarInt(@NonNull ByteBufMessageBuffer buffer, int value) { VarInts.writeUnsignedInt(buffer.buffer(), value); } @Override - public void writeVarLong(@NotNull ByteBufMessageBuffer buffer, long value) { + public void writeVarLong(@NonNull ByteBufMessageBuffer buffer, long value) { VarInts.writeLong(buffer.buffer(), value); } @Override - public void writeUnsignedVarLong(@NotNull ByteBufMessageBuffer buffer, long value) { + public void writeUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer, long value) { VarInts.writeUnsignedLong(buffer.buffer(), value); } @Override - public void writeString(@NotNull ByteBufMessageBuffer buffer, @NonNull String value) { + public void writeString(@NonNull ByteBufMessageBuffer buffer, @NonNull String value) { byte[] bytes = value.getBytes(StandardCharsets.UTF_8); VarInts.writeUnsignedInt(buffer.buffer(), bytes.length); buffer.buffer().writeBytes(bytes); } @Override - public @NotNull ByteBufMessageBuffer createBuffer() { + public @NonNull ByteBufMessageBuffer createBuffer() { return new ByteBufMessageBuffer(this); } @Override - public @NotNull ByteBufMessageBuffer createBuffer(byte @NotNull [] data) { + public @NonNull ByteBufMessageBuffer createBuffer(byte @NonNull [] data) { return new ByteBufMessageBuffer(this, Unpooled.wrappedBuffer(data)); } } diff --git a/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java b/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java index 6a2fd336f5b..51ff19420e2 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/JavaPacketMessage.java @@ -29,7 +29,7 @@ import org.geysermc.geyser.api.network.message.Message; import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket; -public record JavaPacketMessage(MinecraftPacket packet) implements Message.PacketWrapped { +public record JavaPacketMessage(T packet) implements Message.PacketWrapped { @Override public void encode(@NonNull ByteBufMessageBuffer buffer) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index 441fabb99c5..11e0db267f7 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -40,6 +40,7 @@ import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; +import org.geysermc.geyser.api.network.NetworkChannel; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.UrlPackCodec; @@ -61,6 +62,9 @@ import org.geysermc.geyser.level.block.GeyserJavaBlockState; import org.geysermc.geyser.level.block.GeyserMaterialInstance; import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData; +import org.geysermc.geyser.network.ExtensionNetworkChannel; +import org.geysermc.geyser.network.ExternalNetworkChannel; +import org.geysermc.geyser.network.PacketChannel; import org.geysermc.geyser.network.message.BedrockPacketMessage; import org.geysermc.geyser.network.message.JavaPacketMessage; import org.geysermc.geyser.pack.option.GeyserPriorityOption; @@ -72,6 +76,7 @@ import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket; import java.nio.file.Path; +import java.util.Arrays; import java.util.Map; /** @@ -121,14 +126,29 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov } if (args[0] instanceof BedrockPacket bedrockPacket) { - return new BedrockPacketMessage(bedrockPacket); + return new BedrockPacketMessage<>(bedrockPacket); } else if (args[0] instanceof MinecraftPacket javaPacket) { - return new JavaPacketMessage(javaPacket); + return new JavaPacketMessage<>(javaPacket); } else { throw new IllegalArgumentException("Unsupported packet type: " + args[0].getClass().getName()); } }); + providers.put(NetworkChannel.class, args -> { + // Extension network channel + if (args.length == 3 && args[0] instanceof Extension extension && args[1] instanceof String channel && args[2] instanceof Class messageType) { + return new ExtensionNetworkChannel(extension, channel, messageType); + } else if (args.length == 3 && args[0] instanceof String key && args[1] instanceof Integer packetId && args[2] instanceof Class packetType) { + // Packet channel + return new PacketChannel(key, packetId, packetType); + } else if (args.length == 2 && args[0] instanceof Identifier identifier && args[1] instanceof Class messageType) { + // External network channel + return new ExternalNetworkChannel(identifier, messageType); + } else { + throw new IllegalArgumentException("Unknown arguments provided for NetworkChannel provider. Could not create a channel given the arguments: " + Arrays.toString(args)); + } + }); + return providers; } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java index 64a46c89e69..0ff8d59a04e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java @@ -40,8 +40,6 @@ import org.geysermc.floodgate.pluginmessage.PluginMessageChannels; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; -import org.geysermc.geyser.api.event.EventBus; -import org.geysermc.geyser.api.event.java.ServerReceiveNetworkMessageEvent; import org.geysermc.geyser.api.network.MessageDirection; import org.geysermc.geyser.api.network.NetworkChannel; import org.geysermc.geyser.api.network.message.Message; @@ -54,6 +52,9 @@ import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; @Translator(packet = ClientboundCustomPayloadPacket.class) public class JavaCustomPayloadTranslator extends PacketTranslator { @@ -153,16 +154,28 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack } else { session.ensureInEventLoop(() -> { GeyserNetworkManager networkManager = session.getNetworkManager(); - NetworkChannel networkChannel = NetworkChannel.of(packet.getChannel().namespace(), packet.getChannel().value()); - if (!networkManager.registeredChannels().contains(networkChannel)) { - logger.debug("Received a custom payload for an unregistered channel: " + networkChannel.channel()); + Set channels = networkManager.registeredChannels(); + if (channels.isEmpty()) { + this.logger.debug("Received a custom payload for an unregistered channel: " + channel); return; } - Message message = networkManager.createMessage(networkChannel, packet.getData()); + List identifiedChannels = new ArrayList<>(); + for (NetworkChannel registeredChannel : channels) { + if (!registeredChannel.isPacket() && registeredChannel.identifier().toString().equals(channel)) { + identifiedChannels.add(registeredChannel); + } + } + + if (identifiedChannels.isEmpty()) { + this.logger.debug("Received a custom payload for an unregistered channel: " + channel); + return; + } - EventBus eventBus = session.getGeyser().getEventBus(); - eventBus.fire(new ServerReceiveNetworkMessageEvent(session, networkChannel, message, MessageDirection.CLIENTBOUND)); + for (NetworkChannel networkChannel : identifiedChannels) { + List> message = networkManager.createMessages(networkChannel, packet.getData()); + networkManager.handleMessages(networkChannel, message, MessageDirection.CLIENTBOUND); + } }); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index eb94afdb8ec..741911f1d5f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -137,7 +137,8 @@ public void translate(GeyserSession session, ClientboundLoginPacket packet) { if (!registeredChannels.isEmpty()) { String channels = registeredChannels .stream() - .map(channel -> channel.key() + ":" + channel.channel()) + .filter(channel -> !channel.isPacket()) + .map(channel -> channel.identifier().namespace() + ":" + channel.identifier().path()) .collect(Collectors.joining("\0")); session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, channels.getBytes(StandardCharsets.UTF_8))); diff --git a/core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java b/core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java new file mode 100644 index 00000000000..78d102966e0 --- /dev/null +++ b/core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network; + +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.MessageDirection; +import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.message.DataType; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.api.network.message.MessageBuffer; +import org.geysermc.geyser.api.network.message.MessageHandler; +import org.geysermc.geyser.api.network.message.MessagePriority; +import org.geysermc.geyser.impl.IdentifierImpl; +import org.geysermc.geyser.session.GeyserSession; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.geysermc.geyser.util.GeyserMockContext.mockContext; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class MessageRegistrationOrderTest { + private final NetworkChannel xuidChannel = new ExternalNetworkChannel(new IdentifierImpl(Key.key("geyser_test", "xuid")), XuidMessage.class); + + @Test + void testRegistrationOrder() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetworkManager manager = new GeyserNetworkManager(session); + + AtomicInteger state = new AtomicInteger(0); + + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.EARLY, message -> { + assertEquals(0, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.LATE, message -> { + assertEquals(2, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.NORMAL, message -> { + assertEquals(1, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + manager.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); + }); + } + + @Test + void testPipelineTags() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetworkManager manager = new GeyserNetworkManager(session); + + AtomicInteger state = new AtomicInteger(0); + + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(message -> { + assertEquals(2, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .pipeline(pipeline -> { + pipeline.tag("monitor"); + }) + .register(); + + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(message -> { + assertEquals(1, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .pipeline(pipeline -> { + pipeline.tag("initial-handler"); + pipeline.before("monitor"); + }) + .register(); + + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(message -> { + assertEquals(3, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .pipeline(pipeline -> { + pipeline.tag("tail"); + pipeline.after("monitor"); + }) + .register(); + + // No pipeline, so should automatically be added to the tail + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(message -> { + assertEquals(4, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + // Early priority - should come first regardless of tail structure + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.EARLY, message -> { + assertEquals(0, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + manager.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); + }); + } + + @Test + void testMixedHandlers() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetworkManager manager = new GeyserNetworkManager(session); + + AtomicInteger state = new AtomicInteger(0); + + // Simple early clientbound + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.EARLY, message -> { + assertEquals(0, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + // Late clientbound but first serverbound + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.LATE, message -> { + assertEquals(2, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .serverbound(MessagePriority.FIRST, message -> { + fail("Serverbound handler should not be called in this test"); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + // Normal (default) bidirectional + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .bidirectional((message, direction) -> { + if (direction == MessageDirection.SERVERBOUND) { + fail("Serverbound handler should not be called in this test"); + return MessageHandler.State.UNHANDLED; + } + + assertEquals(1, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + // Serverbound only - should never be called + new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .serverbound(MessagePriority.NORMAL, message -> { + fail("Serverbound handler should not be called in this test"); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + manager.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); + }); + } + + public record XuidMessage(String xuid) implements Message.Simple { + + public XuidMessage(MessageBuffer buffer) { + this(buffer.read(DataType.STRING)); + } + + @Override + public void encode(@NonNull MessageBuffer buffer) { + buffer.write(DataType.STRING, this.xuid); + } + } +} diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/NameVisibilityScoreboardTest.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/NameVisibilityScoreboardTest.java index 29882ca2ecc..05396746d96 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/NameVisibilityScoreboardTest.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/NameVisibilityScoreboardTest.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.scoreboard.network; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.spawnPlayerSilently; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java index fcb05c92d80..539766763d5 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java @@ -61,7 +61,7 @@ import java.util.Optional; import java.util.UUID; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.*; +import static org.geysermc.geyser.util.AssertUtils.*; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/belowname/BasicBelownameScoreboardTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/belowname/BasicBelownameScoreboardTests.java index dfe85a0eeb8..5e9cb6a34fd 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/belowname/BasicBelownameScoreboardTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/belowname/BasicBelownameScoreboardTests.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.scoreboard.network.belowname; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.spawnPlayerSilently; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/playerlist/BasicPlayerlistScoreboardTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/playerlist/BasicPlayerlistScoreboardTests.java index 4ac5ee0980f..e479f44f86b 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/playerlist/BasicPlayerlistScoreboardTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/playerlist/BasicPlayerlistScoreboardTests.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.scoreboard.network.playerlist; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; import java.util.List; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/server/CubecraftScoreboardTest.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/server/CubecraftScoreboardTest.java index 80f562fc370..60b881c80bd 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/server/CubecraftScoreboardTest.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/server/CubecraftScoreboardTest.java @@ -25,9 +25,9 @@ package org.geysermc.geyser.scoreboard.network.server; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacketMatch; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacketMatch; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.spawnPlayer; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/BasicSidebarScoreboardTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/BasicSidebarScoreboardTests.java index bd0d64c80fb..01b33f512d3 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/BasicSidebarScoreboardTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/BasicSidebarScoreboardTests.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.scoreboard.network.sidebar; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; import java.util.List; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/OrderAndLimitSidebarScoreboardTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/OrderAndLimitSidebarScoreboardTests.java index aab837456e8..c99b889c27d 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/OrderAndLimitSidebarScoreboardTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/OrderAndLimitSidebarScoreboardTests.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.scoreboard.network.sidebar; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; import java.util.List; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/VanillaSidebarScoreboardTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/VanillaSidebarScoreboardTests.java index f511f59c7ac..dcfd5242abc 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/VanillaSidebarScoreboardTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/sidebar/VanillaSidebarScoreboardTests.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.scoreboard.network.sidebar; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacket; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; import java.util.List; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContextScoreboard.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContextScoreboard.java index 8135039185d..1c00aa2f3ba 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContextScoreboard.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContextScoreboard.java @@ -25,9 +25,9 @@ package org.geysermc.geyser.scoreboard.network.util; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacketType; -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; -import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContext.mockContext; +import static org.geysermc.geyser.util.AssertUtils.assertNextPacketType; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.util.GeyserMockContext.mockContext; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; @@ -45,6 +45,7 @@ import org.geysermc.geyser.session.cache.EntityCache; import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.session.cache.waypoint.WaypointCache; +import org.geysermc.geyser.util.GeyserMockContext; import org.mockito.stubbing.Answer; public class GeyserMockContextScoreboard { diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/AssertUtils.java b/core/src/test/java/org/geysermc/geyser/util/AssertUtils.java similarity index 96% rename from core/src/test/java/org/geysermc/geyser/scoreboard/network/util/AssertUtils.java rename to core/src/test/java/org/geysermc/geyser/util/AssertUtils.java index 9177f205af4..d1517594ac8 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/AssertUtils.java +++ b/core/src/test/java/org/geysermc/geyser/util/AssertUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 GeyserMC. http://geysermc.org + * Copyright (c) 2024-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.scoreboard.network.util; +package org.geysermc.geyser.util; import java.util.Collections; import java.util.function.Consumer; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/EmptyGeyserLogger.java b/core/src/test/java/org/geysermc/geyser/util/EmptyGeyserLogger.java similarity index 94% rename from core/src/test/java/org/geysermc/geyser/scoreboard/network/util/EmptyGeyserLogger.java rename to core/src/test/java/org/geysermc/geyser/util/EmptyGeyserLogger.java index e033b7288cb..a95be4048e1 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/EmptyGeyserLogger.java +++ b/core/src/test/java/org/geysermc/geyser/util/EmptyGeyserLogger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 GeyserMC. http://geysermc.org + * Copyright (c) 2024-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.scoreboard.network.util; +package org.geysermc.geyser.util; import org.geysermc.geyser.GeyserLogger; diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContext.java b/core/src/test/java/org/geysermc/geyser/util/GeyserMockContext.java similarity index 93% rename from core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContext.java rename to core/src/test/java/org/geysermc/geyser/util/GeyserMockContext.java index 2b89867fb33..295a2cb6ebf 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContext.java +++ b/core/src/test/java/org/geysermc/geyser/util/GeyserMockContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 GeyserMC. http://geysermc.org + * Copyright (c) 2024-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,23 +23,24 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.scoreboard.network.util; +package org.geysermc.geyser.util; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.configuration.GeyserConfiguration; -import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.event.GeyserEventBus; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.mockito.Mockito; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + public class GeyserMockContext { private final List mocksAndSpies = new ArrayList<>(); private final List storedObjects = new ArrayList<>(); @@ -58,6 +59,9 @@ public static void mockContext(Consumer geyserContext) { var logger = context.storeObject(new EmptyGeyserLogger()); when(geyserImpl.getLogger()).thenReturn(logger); + var eventBus = context.mock(GeyserEventBus.class); + when(geyserImpl.getEventBus()).thenReturn(eventBus); + try (var geyserImplMock = mockStatic(GeyserImpl.class)) { geyserImplMock.when(GeyserImpl::getInstance).thenReturn(geyserImpl); @@ -113,7 +117,7 @@ public GeyserSession session() { return mockOrSpy(GeyserSession.class); } - void addPacket(BedrockPacket packet) { + public void addPacket(BedrockPacket packet) { packets.add(packet); } From fd17fb86f9c7e5fd4c675a86efe36c7f0a116623 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sun, 9 Nov 2025 18:30:12 +0000 Subject: [PATCH 06/22] Address MessagePriority issue --- .../geyser/api/network/message/MessagePriority.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java index de5dfe1a210..75bab260d8f 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java @@ -62,10 +62,10 @@ public int value() { */ @NonNull public static MessagePriority of(int value) { - if (value >= 75) return LAST; - if (value >= 25) return LATE; - if (value <= -75) return FIRST; - if (value <= -25) return EARLY; + if (value >= 75) return FIRST; + if (value >= 25) return EARLY; + if (value <= -75) return LAST; + if (value <= -25) return LATE; return NORMAL; } } From db931c1c2a98448bdbe7fee5c9a9ec8127a8a6b7 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sun, 9 Nov 2025 19:51:33 +0000 Subject: [PATCH 07/22] This should not be updated --- core/src/main/resources/languages | 2 +- core/src/main/resources/mappings | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index 40253e5f317..4ce8ad58ea7 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 40253e5f317ede6020aa563d0823c54c37380ebf +Subproject commit 4ce8ad58ea7ab779d613a64862956d6d0563a8e3 diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 8002ed6ed85..15398c1588f 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 8002ed6ed859686b9f544e626ad01995fc3ba7f1 +Subproject commit 15398c1588f5149824db8bd625e21f6886f98b10 From 75e60303c3ca3e900770afaafbc476ff276f8618 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sun, 9 Nov 2025 20:00:34 +0000 Subject: [PATCH 08/22] Add maximum length method for strings --- .../api/network/message/MessageCodec.java | 10 ++++++++++ .../geyser/network/message/ByteBufCodec.java | 19 +++++++++++++++++++ .../network/message/ByteBufCodecLE.java | 19 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java index 8e720b31381..e8805de59ff 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java @@ -132,6 +132,16 @@ public interface MessageCodec { @NonNull String readString(@NonNull T buffer); + /** + * Reads a string from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @param maxLength the maximum length of the string to read + * @return the string read + */ + @NonNull + String readString(@NonNull T buffer, int maxLength); + /** * Writes a boolean value to the {@link T buffer}. * diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java index 272170c4c65..f81d13fd23b 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.network.message; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.common.util.VarInts; @@ -103,6 +104,24 @@ public long readUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer) { return new String(bytes, StandardCharsets.UTF_8); } + @Override + public @NonNull String readString(@NonNull ByteBufMessageBuffer buffer, int maxLength) { + int maxBytes = ByteBufUtil.utf8MaxBytes(maxLength); + int size = VarInts.readUnsignedInt(buffer.buffer()); + if (size > maxBytes) { + throw new IllegalArgumentException("The received encoded string buffer length is longer than maximum allowed (" + size + " > " + maxBytes + ")"); + } + + byte[] bytes = new byte[size]; + buffer.buffer().readBytes(bytes); + String string = new String(bytes, StandardCharsets.UTF_8); + if (string.length() > maxLength) { + throw new IllegalArgumentException("The received string length is longer than maximum allowed (" + string.length() + " > " + maxLength + ")"); + } + + return string; + } + @Override public void writeBoolean(@NonNull ByteBufMessageBuffer buffer, boolean value) { buffer.buffer().writeBoolean(value); diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java index c409b1a02d2..dddb9c90a64 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.network.message; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.common.util.VarInts; @@ -101,6 +102,24 @@ public long readUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer) { return new String(bytes, StandardCharsets.UTF_8); } + @Override + public @NonNull String readString(@NonNull ByteBufMessageBuffer buffer, int maxLength) { + int maxBytes = ByteBufUtil.utf8MaxBytes(maxLength); + int size = VarInts.readUnsignedInt(buffer.buffer()); + if (size > maxBytes) { + throw new IllegalArgumentException("The received encoded string buffer length is longer than maximum allowed (" + size + " > " + maxBytes + ")"); + } + + byte[] bytes = new byte[size]; + buffer.buffer().readBytes(bytes); + String string = new String(bytes, StandardCharsets.UTF_8); + if (string.length() > maxLength) { + throw new IllegalArgumentException("The received string length is longer than maximum allowed (" + string.length() + " > " + maxLength + ")"); + } + + return string; + } + @Override public void writeBoolean(@NonNull ByteBufMessageBuffer buffer, boolean value) { buffer.buffer().writeBoolean(value); From ecdfd5a4f5644658e8420b6aeb888fd4f11d6245 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sun, 9 Nov 2025 20:11:05 +0000 Subject: [PATCH 09/22] Address more comments --- .../java/org/geysermc/geyser/api/network/NetworkChannel.java | 4 ++-- .../geysermc/geyser/api/network/message/MessagePriority.java | 1 - .../geysermc/geyser/network/NetworkDefinitionBuilder.java | 1 + .../geyser/translator/protocol/java/JavaLoginTranslator.java | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java index c383c378ddd..de7522343cd 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -45,13 +45,13 @@ * *
  * {@code
- *     private final NetworkChannel myChannel = NetworkChannel.of("example", "my_channel");
+ *     private final NetworkChannel myChannel = NetworkChannel.of("example", "my_channel", MyMessage.class);
  * }
  * 
* Or when inside an extension, with 'this' being the extension instance: *
  * {@code
- *     private final NetworkChannel myChannel = NetworkChannel.of(this, "my_channel");
+ *     private final NetworkChannel myChannel = NetworkChannel.of(this, "my_channel", MyMessage.class);
  * }
  * 
* diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java index 75bab260d8f..34b5231c760 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java @@ -58,7 +58,6 @@ public int value() { * * @param value the priority value * @return the priority - * @throws IllegalArgumentException if outside allowed range */ @NonNull public static MessagePriority of(int value) { diff --git a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java index be65967332e..f8a8cd53107 100644 --- a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java +++ b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java @@ -52,6 +52,7 @@ public NetworkDefinitionBuilder(@NonNull Consumer> registrat this.registrationCallback = registrationCallback; } + @Override public SessionDefineNetworkChannelsEvent.Builder.@NonNull Initial pipeline(@NonNull Consumer pipeline) { Objects.requireNonNull(pipeline, "pipeline"); PipelineImpl impl = new PipelineImpl(); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index 741911f1d5f..a91b3c8c684 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -139,9 +139,12 @@ public void translate(GeyserSession session, ClientboundLoginPacket packet) { .stream() .filter(channel -> !channel.isPacket()) .map(channel -> channel.identifier().namespace() + ":" + channel.identifier().path()) + .distinct() .collect(Collectors.joining("\0")); - session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, channels.getBytes(StandardCharsets.UTF_8))); + if (!channels.isEmpty()) { + session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, channels.getBytes(StandardCharsets.UTF_8))); + } } if (session.getBedrockDimension().bedrockId() != newDimension.bedrockId()) { From a4fa617ec0686b02d2b384451ceb6f657286aa46 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Mon, 10 Nov 2025 12:59:55 +0000 Subject: [PATCH 10/22] Fix wrapped packets and add a way to retrieve message codecs without relying on the impl --- .../api/network/message/MessageBuffer.java | 16 +++++++ .../api/network/message/MessageCodec.java | 23 +++++++++ .../geyser/network/GeyserNetworkManager.java | 47 ++++++++++++------- .../network/message/ByteBufMessageBuffer.java | 2 +- .../loader/ProviderRegistryLoader.java | 28 +++++++++++ 5 files changed, 99 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java index 678bbbbcceb..6a39603de7c 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java @@ -67,4 +67,20 @@ public interface MessageBuffer { * @return the length of the buffer */ int length(); + + /** + * Represents a buffer that wraps an internal buffer type. + * + * @param the internal buffer type + */ + interface Wrapped extends MessageBuffer { + + /** + * Gets the internal buffer. + * + * @return the internal buffer + */ + @NonNull + T buffer(); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java index e8805de59ff..9437d918ff9 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.api.network.message; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; /** * A codec for encoding and decoding messages. @@ -254,4 +255,26 @@ public interface MessageCodec { */ @NonNull T createBuffer(byte @NonNull [] data); + + /** + * Gets a provided {@link MessageCodec} for the given type. + * + * @param type the type of message codec to get + * @param options the encoder options + * @param the type of message codec to get + * @param the type of message codec to get + * @return the provided message codec + * @throws IllegalArgumentException if no provider is found + */ + @NonNull + static >> C provided(@NonNull Class type, @NonNull EncoderOptions... options) { + return GeyserApi.api().provider(MessageCodec.class, type, options); + } + + /** + * Options for the encoder. + */ + enum EncoderOptions { + LITTLE_ENDIAN + } } diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java index 5b795229fee..b0eeb784938 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java @@ -66,7 +66,7 @@ public class GeyserNetworkManager implements NetworkManager { private final GeyserSession session; private final Map>> definitions = new LinkedHashMap<>(); - private final Int2ObjectMap packetChannels = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap> packetChannels = new Int2ObjectOpenHashMap<>(); public GeyserNetworkManager(GeyserSession session) { this.session = session; @@ -218,9 +218,9 @@ private > List createMessages0( return messages; } - @Nullable - public PacketChannel getPacketChannel(int packetId) { - return this.packetChannels.get(packetId); + @NonNull + public List getPacketChannels(int packetId) { + return this.packetChannels.getOrDefault(packetId, List.of()); } @SuppressWarnings("unchecked") @@ -231,21 +231,30 @@ public boolean handlePacket(BedrockPacket packet, MessageDirection direction) { BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); BedrockPacketDefinition definition = codec.getPacketDefinition((Class) packet.getClass()); - PacketChannel channel = this.getPacketChannel(definition.getId()); - if (channel == null) { + List channels = this.getPacketChannels(definition.getId()); + if (channels.isEmpty()) { return true; } - List> messages; - if (channel.messageType().isInstance(packet)) { - messages = List.of(new BedrockPacketMessage<>(packet)); - } else { - ByteBuf buffer = Unpooled.buffer(); - definition.getSerializer().serialize(buffer, this.session.getUpstream().getCodecHelper(), packet); - messages = this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer)); + List> messages = new ArrayList<>(); + for (PacketChannel channel : channels) { + if (channel.messageType().isInstance(packet)) { + messages.add(new BedrockPacketMessage<>(packet)); + } else { + ByteBuf buffer = Unpooled.buffer(); + definition.getSerializer().serialize(buffer, this.session.getUpstream().getCodecHelper(), packet); + messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer))); + } + } + + for (NetworkChannel channel : channels) { + boolean handled = this.handleMessages(channel, messages, direction); + if (!handled) { + return false; + } } - return this.handleMessages(channel, messages, direction); + return true; } @SuppressWarnings("unchecked") @@ -286,7 +295,13 @@ public boolean handleMessages(@NonNull NetworkChannel for (Message message : messages) { for (MessageDefinition def : ordered) { - if (!(channel instanceof BaseNetworkChannel base) || !base.messageType().isInstance(message)) { + if (!(channel instanceof BaseNetworkChannel base)) { + continue; + } + + if (message instanceof Message.PacketWrapped wrapped && !base.messageType().isInstance(wrapped.packet())) { + continue; + } else if (!(message instanceof Message.PacketWrapped) && !base.messageType().isInstance(message)) { continue; } @@ -376,7 +391,7 @@ private > void registerMessage(@No if (channel.isPacket() && channel instanceof PacketChannel packetChannel) { int packetId = packetChannel.packetId(); - this.packetChannels.put(packetId, packetChannel); + this.packetChannels.computeIfAbsent(packetId, key -> new ArrayList<>()).add(packetChannel); } } diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java index 8b414d3033e..e436c3384d4 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java @@ -32,7 +32,7 @@ import org.geysermc.geyser.api.network.message.MessageBuffer; import org.geysermc.geyser.api.network.message.MessageCodec; -public record ByteBufMessageBuffer(MessageCodec codec, ByteBuf buffer) implements MessageBuffer { +public record ByteBufMessageBuffer(MessageCodec codec, ByteBuf buffer) implements MessageBuffer.Wrapped { public ByteBufMessageBuffer(MessageCodec codec) { this(codec, Unpooled.buffer()); diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index 11e0db267f7..a9d7cd44d3e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.registry.loader; +import io.netty.buffer.ByteBuf; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.api.bedrock.camera.CameraFade; import org.geysermc.geyser.api.bedrock.camera.CameraPosition; @@ -42,6 +43,7 @@ import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.api.network.NetworkChannel; import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.api.network.message.MessageCodec; import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.api.pack.option.PriorityOption; @@ -66,6 +68,7 @@ import org.geysermc.geyser.network.ExternalNetworkChannel; import org.geysermc.geyser.network.PacketChannel; import org.geysermc.geyser.network.message.BedrockPacketMessage; +import org.geysermc.geyser.network.message.ByteBufCodec; import org.geysermc.geyser.network.message.JavaPacketMessage; import org.geysermc.geyser.pack.option.GeyserPriorityOption; import org.geysermc.geyser.pack.option.GeyserSubpackOption; @@ -77,7 +80,9 @@ import java.nio.file.Path; import java.util.Arrays; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * Registers the provider data from the provider. @@ -134,6 +139,29 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov } }); + providers.put(MessageCodec.class, args -> { + if (args.length < 1) { + throw new IllegalArgumentException("MessageCodec requires at least one argument, got " + args.length); + } + + Set options = new HashSet<>(); + if (args.length > 1 && args[1] instanceof MessageCodec.EncoderOptions[] encoderOptions) { + options = new HashSet<>(Arrays.asList(encoderOptions)); + } + + if (args[0] instanceof Class wrapperType) { + if (wrapperType == ByteBuf.class) { + if (options.contains(MessageCodec.EncoderOptions.LITTLE_ENDIAN)) { + return ByteBufCodec.INSTANCE_LE; + } else { + return ByteBufCodec.INSTANCE; + } + } + } + + throw new IllegalArgumentException("Unsupported codec type: " + args[0]); + }); + providers.put(NetworkChannel.class, args -> { // Extension network channel if (args.length == 3 && args[0] instanceof Extension extension && args[1] instanceof String channel && args[2] instanceof Class messageType) { From 0979edfb07819de364d69946c3dfe45254e695cf Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Mon, 10 Nov 2025 17:44:10 +0000 Subject: [PATCH 11/22] Add UUID data type --- .../geyser/api/network/message/DataType.java | 5 +++ .../api/network/message/MessageCodec.java | 43 +++++++++++++------ .../geyser/network/message/ByteBufCodec.java | 12 ++++++ .../network/message/ByteBufCodecLE.java | 12 ++++++ 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java index a6933ed05a9..b6fa7f9bf9c 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.UUID; /** * Represents a data type that can be sent or received over the network. @@ -88,6 +89,10 @@ public final class DataType { * Note: Strings are encoded in UTF-8 format. */ public static final DataType STRING = of(MessageCodec::readString, MessageCodec::writeString); + /** + * A DataType for reading and writing UUIDs. + */ + public static final DataType UUID = of(MessageCodec::readUuid, MessageCodec::writeUuid); private final Reader reader; private final Writer writer; diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java index 9437d918ff9..8d3d2c79b59 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java @@ -28,6 +28,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; +import java.util.UUID; + /** * A codec for encoding and decoding messages. * @@ -143,11 +145,20 @@ public interface MessageCodec { @NonNull String readString(@NonNull T buffer, int maxLength); + /** + * Reads a UUID from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the UUID read + */ + @NonNull + UUID readUuid(@NonNull T buffer); + /** * Writes a boolean value to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the boolean value to write + * @param value the boolean value to write */ void writeBoolean(@NonNull T buffer, boolean value); @@ -155,7 +166,7 @@ public interface MessageCodec { * Writes a byte value to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the byte value to write + * @param value the byte value to write */ void writeByte(@NonNull T buffer, byte value); @@ -163,7 +174,7 @@ public interface MessageCodec { * Writes a short value to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the short value to write + * @param value the short value to write */ void writeShort(@NonNull T buffer, short value); @@ -171,7 +182,7 @@ public interface MessageCodec { * Writes an integer value to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the integer value to write + * @param value the integer value to write */ void writeInt(@NonNull T buffer, int value); @@ -179,7 +190,7 @@ public interface MessageCodec { * Writes a float value to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the float value to write + * @param value the float value to write */ void writeFloat(@NonNull T buffer, float value); @@ -187,7 +198,7 @@ public interface MessageCodec { * Writes a double value to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the double value to write + * @param value the double value to write */ void writeDouble(@NonNull T buffer, double value); @@ -195,7 +206,7 @@ public interface MessageCodec { * Writes a long value to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the long value to write + * @param value the long value to write */ void writeLong(@NonNull T buffer, long value); @@ -203,7 +214,7 @@ public interface MessageCodec { * Writes a variable-length integer to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the variable-length integer to write + * @param value the variable-length integer to write */ void writeVarInt(@NonNull T buffer, int value); @@ -211,7 +222,7 @@ public interface MessageCodec { * Writes an unsigned variable-length integer to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the unsigned variable-length integer to write + * @param value the unsigned variable-length integer to write */ void writeUnsignedVarInt(@NonNull T buffer, int value); @@ -219,7 +230,7 @@ public interface MessageCodec { * Writes a variable-length long to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the variable-length long to write + * @param value the variable-length long to write */ void writeVarLong(@NonNull T buffer, long value); @@ -227,7 +238,7 @@ public interface MessageCodec { * Writes an unsigned variable-length long to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the unsigned variable-length long to write + * @param value the unsigned variable-length long to write */ void writeUnsignedVarLong(@NonNull T buffer, long value); @@ -235,9 +246,17 @@ public interface MessageCodec { * Writes a string to the {@link T buffer}. * * @param buffer the buffer to write to - * @param value the string to write + * @param value the string to write */ void writeString(@NonNull T buffer, @NonNull String value); + + /** + * Writes a UUID to the {@link T buffer}. + * + * @param buffer the buffer to write to + * @param uuid the UUID to write + */ + void writeUuid(@NonNull T buffer, @NonNull UUID uuid); /** * Creates a new {@link T buffer} instance. diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java index f81d13fd23b..b00d9166f6c 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.java @@ -32,6 +32,7 @@ import org.geysermc.geyser.api.network.message.MessageCodec; import java.nio.charset.StandardCharsets; +import java.util.UUID; public class ByteBufCodec implements MessageCodec { public static final ByteBufCodec INSTANCE = new ByteBufCodec(); @@ -122,6 +123,11 @@ public long readUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer) { return string; } + @Override + public @NonNull UUID readUuid(@NonNull ByteBufMessageBuffer buffer) { + return new UUID(buffer.buffer().readLong(), buffer.buffer().readLong()); + } + @Override public void writeBoolean(@NonNull ByteBufMessageBuffer buffer, boolean value) { buffer.buffer().writeBoolean(value); @@ -184,6 +190,12 @@ public void writeString(@NonNull ByteBufMessageBuffer buffer, @NonNull String va buffer.buffer().writeBytes(bytes); } + @Override + public void writeUuid(@NonNull ByteBufMessageBuffer buffer, @NonNull UUID uuid) { + buffer.buffer().writeLong(uuid.getMostSignificantBits()); + buffer.buffer().writeLong(uuid.getLeastSignificantBits()); + } + @Override public @NonNull ByteBufMessageBuffer createBuffer() { return new ByteBufMessageBuffer(this); diff --git a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java index dddb9c90a64..6468fe27803 100644 --- a/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java @@ -32,6 +32,7 @@ import org.geysermc.geyser.api.network.message.MessageCodec; import java.nio.charset.StandardCharsets; +import java.util.UUID; public class ByteBufCodecLE implements MessageCodec { @@ -120,6 +121,11 @@ public long readUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer) { return string; } + @Override + public @NonNull UUID readUuid(@NonNull ByteBufMessageBuffer buffer) { + return new UUID(buffer.buffer().readLongLE(), buffer.buffer().readLongLE()); + } + @Override public void writeBoolean(@NonNull ByteBufMessageBuffer buffer, boolean value) { buffer.buffer().writeBoolean(value); @@ -182,6 +188,12 @@ public void writeString(@NonNull ByteBufMessageBuffer buffer, @NonNull String va buffer.buffer().writeBytes(bytes); } + @Override + public void writeUuid(@NonNull ByteBufMessageBuffer buffer, @NonNull UUID uuid) { + buffer.buffer().writeLongLE(uuid.getMostSignificantBits()); + buffer.buffer().writeLongLE(uuid.getLeastSignificantBits()); + } + @Override public @NonNull ByteBufMessageBuffer createBuffer() { return new ByteBufMessageBuffer(this); From ae24469c673d6bc2ee62b6514a17804734a57379 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Wed, 24 Dec 2025 14:09:19 -0600 Subject: [PATCH 12/22] Add support for Java packet handling --- .../api/connection/GeyserConnection.java | 6 +- .../SessionDefineNetworkChannelsEvent.java | 10 + .../geyser/api/network/JavaState.java | 50 +++++ .../geyser/api/network/MessageFlag.java | 32 ++++ .../{NetworkManager.java => Network.java} | 15 +- .../geyser/api/network/NetworkChannel.java | 13 -- .../geyser/api/network/PacketChannel.java | 77 ++++++++ .../geyser/api/network/PacketFlag.java | 37 ++++ .../geyser/api/network/ProtocolState.java | 52 ++++++ .../geyser/impl/ExtensionIdentifierImpl.java | 45 +++++ .../geyser/network/GeyserJavaState.java | 58 ++++++ ...NetworkManager.java => GeyserNetwork.java} | 174 ++++++++++++++---- .../network/NetworkDefinitionBuilder.java | 10 + ...ketChannel.java => PacketChannelImpl.java} | 32 ++-- .../geyser/network/UpstreamPacketHandler.java | 4 +- .../geyser/network/netty/LocalSession.java | 2 +- .../loader/ProviderRegistryLoader.java | 30 ++- .../geyser/session/DownstreamSession.java | 11 +- .../geyser/session/GeyserSession.java | 59 ++++-- .../geyser/session/UpstreamSession.java | 4 +- .../java/JavaCustomPayloadTranslator.java | 10 +- .../protocol/java/JavaLoginTranslator.java | 2 +- .../network/MessageRegistrationOrderTest.java | 36 ++-- 23 files changed, 647 insertions(+), 122 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/JavaState.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java rename api/src/main/java/org/geysermc/geyser/api/network/{NetworkManager.java => Network.java} (85%) create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java create mode 100644 core/src/main/java/org/geysermc/geyser/impl/ExtensionIdentifierImpl.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/GeyserJavaState.java rename core/src/main/java/org/geysermc/geyser/network/{GeyserNetworkManager.java => GeyserNetwork.java} (71%) rename core/src/main/java/org/geysermc/geyser/network/{PacketChannel.java => PacketChannelImpl.java} (73%) diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index fbc192ba1ff..18229942baf 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -36,7 +36,7 @@ import org.geysermc.geyser.api.entity.EntityData; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; -import org.geysermc.geyser.api.network.NetworkManager; +import org.geysermc.geyser.api.network.Network; import org.geysermc.geyser.api.skin.SkinData; import java.util.NoSuchElementException; @@ -124,13 +124,13 @@ public interface GeyserConnection extends Connection, CommandSource { void sendCommand(String command); /** - * Gets the {@link NetworkManager} used for handling + * Gets the {@link Network} used for handling * network channels and sending messages. * * @return the network manager */ @NonNull - NetworkManager networkManager(); + Network network(); /** * Gets the hostname or ip address the player used to join this Geyser instance. diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index 85107e26cd9..cd4a1d9053e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -29,6 +29,7 @@ import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.ProtocolState; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; import org.geysermc.geyser.api.network.message.MessageCodec; @@ -96,6 +97,15 @@ public interface Builder> { interface Initial> extends Sided, Bidirectional { + /** + * Sets the protocol state for this message. + * + * @param state the protocol state + * @return the initial builder instance + */ + @NonNull + Initial protocolState(@NonNull ProtocolState state); + /** * {@inheritDoc} */ diff --git a/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java b/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java new file mode 100644 index 00000000000..195b80819ac --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents the state of a Java connection. + */ +public interface JavaState { + + /** + * Gets the inbound protocol state. + * + * @return the inbound protocol state + */ + @NonNull + ProtocolState inbound(); + + /** + * Gets the outbound protocol state. + * + * @return the outbound protocol state + */ + @NonNull + ProtocolState outbound(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java b/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java new file mode 100644 index 00000000000..848f168b463 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +/** + * Represents flags that can be associated with network messages. + */ +public interface MessageFlag { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java b/api/src/main/java/org/geysermc/geyser/api/network/Network.java similarity index 85% rename from api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java rename to api/src/main/java/org/geysermc/geyser/api/network/Network.java index 76eec94fd21..6b5dbe5ade7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkManager.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/Network.java @@ -33,12 +33,20 @@ import java.util.Set; /** - * Represents the network manager responsible for handling network operations + * Represents the network handler responsible for handling network operations * for a {@link GeyserConnection}. * * @since 2.9.1 */ -public interface NetworkManager { +public interface Network { + + /** + * Gets the Java protocol state of the connection. + * + * @return the Java protocol state + */ + @NonNull + JavaState javaState(); /** * Gets the registered network channels. @@ -54,6 +62,7 @@ public interface NetworkManager { * @param channel the channel to send the message on * @param message the message to send * @param direction the direction of the message (clientbound or serverbound) + * @param flags the message flags to use when sending the message */ - void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction); + void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction, @NonNull MessageFlag... flags); } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java index de7522343cd..f995ab10a89 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.api.network; -import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.extension.Extension; @@ -137,16 +136,4 @@ static NetworkChannel of(@NonNull String id, @NonNull String channel, @NonNull C static NetworkChannel of(@NonNull Identifier identifier, @NonNull Class messageType) { return GeyserApi.api().provider(NetworkChannel.class, identifier, messageType); } - - /** - * Creates a new packet {@link NetworkChannel} instance for a packet channel. - * - * @param key the packet key - * @param packetId the packet ID - * @param packetType the type of the packet - * @return a new packet {@link NetworkChannel} instance for a packet channel - */ - static NetworkChannel packet(@NonNull String key, @NonNegative int packetId, @NonNull Class packetType) { - return GeyserApi.api().provider(NetworkChannel.class, key, packetId, packetType); - } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java new file mode 100644 index 00000000000..d372721f90a --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.extension.Extension; + +/** + * Represents a channel for network communication associated with a packet. + *

+ * This channel is used for listening to communication over + * packets between the server and client and can be used to + * send or receive packets. + * + * @since 2.9.1 + */ +public interface PacketChannel extends NetworkChannel { + + /** + * Gets the packet ID associated with this channel. + * + * @return the packet ID + */ + @NonNegative + int packetId(); + + /** + * Creates a new Bedrock packet {@link NetworkChannel} instance for a packet channel. + * + * @param extension the extension creating the channel + * @param packetId the packet ID + * @param packetType the type of the packet + * @return a new packet {@link NetworkChannel} instance for a packet channel + */ + @NonNull + static PacketChannel bedrock(@NonNull Extension extension, @NonNegative int packetId, @NonNull Class packetType) { + return GeyserApi.api().provider(PacketChannel.class, "bedrock", extension, packetId, packetType); + } + + /** + * Creates a new Java packet {@link NetworkChannel} instance for a packet channel. + * + * @param extension the extension creating the channel + * @param packetId the packet ID + * @param packetType the type of the packet + * @return a new packet {@link NetworkChannel} instance for a packet channel + */ + @NonNull + static PacketChannel java(@NonNull Extension extension, @NonNegative int packetId, @NonNull Class packetType) { + return GeyserApi.api().provider(PacketChannel.class, extension, "java", packetId, packetType); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java new file mode 100644 index 00000000000..fc4f6f23293 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +/** + * Represents flags that can be applied to packets + * to modify their behavior. + */ +public enum PacketFlag implements MessageFlag { + /** + * Indicates that the packet should be sent immediately. + */ + IMMEDIATE; +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java b/api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java new file mode 100644 index 00000000000..544e9aa3fae --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +/** + * Represents the protocol state of a Java network connection. + */ +public enum ProtocolState { + /** + * The handshake state. + */ + HANDSHAKE, + /** + * The game state. + */ + GAME, + /** + * The status state. + */ + STATUS, + /** + * The login state. + */ + LOGIN, + /** + * The configuration state. + */ + CONFIGURATION; +} diff --git a/core/src/main/java/org/geysermc/geyser/impl/ExtensionIdentifierImpl.java b/core/src/main/java/org/geysermc/geyser/impl/ExtensionIdentifierImpl.java new file mode 100644 index 00000000000..22315d58316 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/impl/ExtensionIdentifierImpl.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.impl; + +import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.Identifier; + +/** + * An identifier which redirects back to an extension. + *

+ * This primarily serves to back an {@link Extension} interface + * without an explicit call to the extension ID right away. + * Useful in instances where the extension is still in an early + * loading phase, but an identifier is required. + */ +public record ExtensionIdentifierImpl(Extension extension, String path) implements Identifier { + + @Override + public String namespace() { + return this.extension.description().id(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserJavaState.java b/core/src/main/java/org/geysermc/geyser/network/GeyserJavaState.java new file mode 100644 index 00000000000..723f7cd6ab3 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserJavaState.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.JavaState; +import org.geysermc.geyser.api.network.ProtocolState; +import org.geysermc.geyser.session.GeyserSession; + +public record GeyserJavaState(GeyserSession session) implements JavaState { + + @Override + public @NonNull ProtocolState inbound() { + org.geysermc.mcprotocollib.protocol.data.ProtocolState implState = this.session.getDownstream().getSession().getPacketProtocol().getInboundState(); + return switch (implState) { + case HANDSHAKE -> ProtocolState.HANDSHAKE; + case STATUS -> ProtocolState.STATUS; + case LOGIN -> ProtocolState.LOGIN; + case GAME -> ProtocolState.GAME; + case CONFIGURATION -> ProtocolState.CONFIGURATION; + }; + } + + @Override + public @NonNull ProtocolState outbound() { + org.geysermc.mcprotocollib.protocol.data.ProtocolState implState = this.session.getDownstream().getSession().getPacketProtocol().getOutboundState(); + return switch (implState) { + case HANDSHAKE -> ProtocolState.HANDSHAKE; + case STATUS -> ProtocolState.STATUS; + case LOGIN -> ProtocolState.LOGIN; + case GAME -> ProtocolState.GAME; + case CONFIGURATION -> ProtocolState.CONFIGURATION; + }; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java similarity index 71% rename from core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java rename to core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java index b0eeb784938..dfb93c40fc5 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java @@ -27,8 +27,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -38,9 +36,14 @@ import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; +import org.geysermc.geyser.api.network.JavaState; import org.geysermc.geyser.api.network.MessageDirection; +import org.geysermc.geyser.api.network.MessageFlag; +import org.geysermc.geyser.api.network.Network; import org.geysermc.geyser.api.network.NetworkChannel; -import org.geysermc.geyser.api.network.NetworkManager; +import org.geysermc.geyser.api.network.PacketChannel; +import org.geysermc.geyser.api.network.PacketFlag; +import org.geysermc.geyser.api.network.ProtocolState; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; import org.geysermc.geyser.api.network.message.MessageCodec; @@ -50,26 +53,33 @@ import org.geysermc.geyser.network.message.ByteBufCodec; import org.geysermc.geyser.network.message.ByteBufMessageBuffer; import org.geysermc.geyser.network.message.JavaPacketMessage; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.network.packet.Packet; +import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; +import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; import org.jetbrains.annotations.VisibleForTesting; import javax.annotation.Nonnull; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; -public class GeyserNetworkManager implements NetworkManager { +public class GeyserNetwork implements Network { private final GeyserSession session; + private final GeyserJavaState javaState; private final Map>> definitions = new LinkedHashMap<>(); - private final Int2ObjectMap> packetChannels = new Int2ObjectOpenHashMap<>(); + private final Map> packetChannels = new HashMap<>(); - public GeyserNetworkManager(GeyserSession session) { + public GeyserNetwork(GeyserSession session) { this.session = session; + this.javaState = new GeyserJavaState(session); SessionDefineNetworkChannelsEvent event = new SessionDefineNetworkChannelsEvent(session) { @@ -95,6 +105,10 @@ > void onRegister(@NonNull NetworkChannel chann @SuppressWarnings("unchecked") private > void onRegister(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory, NetworkDefinitionBuilder.@NonNull RegistrationImpl registration) { + if (channel instanceof PacketChannelImpl packetChannel && packetChannel.isJava() && registration.protocolState() == null) { + throw new IllegalArgumentException("Java packet channels must specify a protocol state"); + } + MessageHandler handler; int priority; @@ -125,6 +139,7 @@ private > void onRegister(@NonNull priority, clientbound != null && clientbound.priority() != null ? clientbound.priority().value() : null, serverbound != null && serverbound.priority() != null ? serverbound.priority().value() : null, + registration.protocolState(), registration.tag(), registration.beforeTag(), registration.afterTag() @@ -133,6 +148,11 @@ private > void onRegister(@NonNull this.registerMessage(channel, definition); } + @Override + public @NonNull JavaState javaState() { + return this.javaState; + } + @Override @Nonnull public Set registeredChannels() { @@ -140,32 +160,73 @@ public Set registeredChannels() { } @Override - public void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction) { + public void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction, @NonNull MessageFlag... flags) { + Set flagSet = Set.of(flags); if (channel.isPacket() && message instanceof Message.PacketBase packetBase) { if (packetBase instanceof BedrockPacketMessage packetMessage) { - this.session.sendUpstreamPacket(packetMessage.packet()); + if (direction == MessageDirection.CLIENTBOUND) { + if (flagSet.contains(PacketFlag.IMMEDIATE)) { + this.session.sendUpstreamPacketImmediately(packetMessage.packet()); + } else { + this.session.sendUpstreamPacket(packetMessage.packet()); + } + } else { + this.session.getUpstream().getSession().getPacketHandler().handlePacket(packetMessage.packet()); + } } else if (packetBase instanceof JavaPacketMessage packetMessage) { - this.session.sendDownstreamPacket(packetMessage.packet()); + MinecraftPacket javaPacket = packetMessage.packet(); + if (direction == MessageDirection.CLIENTBOUND) { + Registries.JAVA_PACKET_TRANSLATORS.translate(javaPacket.getClass(), javaPacket, this.session, true); + } else { + this.session.sendDownstreamPacket(javaPacket); + } } else if (packetBase instanceof Message.Packet packet) { - PacketChannel packetChannel = (PacketChannel) channel; + PacketChannelImpl packetChannel = (PacketChannelImpl) channel; int packetId = packetChannel.packetId(); - ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE_LE.createBuffer(); - packet.encode(buffer); - - BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); - BedrockCodecHelper helper = this.session.getUpstream().getCodecHelper(); - - BedrockPacket bedrockPacket = codec.tryDecode(helper, buffer.buffer(), packetId); - if (bedrockPacket == null) { - throw new IllegalArgumentException("No Bedrock packet definition found for packet ID: " + packetId); - } - - // Clientbound packets are sent upstream, serverbound packets are sent downstream - if (direction == MessageDirection.CLIENTBOUND) { - this.session.sendUpstreamPacket(bedrockPacket); + if (packetChannel.isJava()) { + ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE.createBuffer(); + packet.encode(buffer); + + MinecraftProtocol protocol = this.session.getDownstream().getSession().getPacketProtocol(); + Packet javaPacket; + if (direction == MessageDirection.CLIENTBOUND) { + javaPacket = protocol.getInboundPacketRegistry().createClientboundPacket(packetId, buffer.buffer()); + } else { + javaPacket = protocol.getOutboundPacketRegistry().createServerboundPacket(packetId, buffer.buffer()); + } + + if (javaPacket == null) { + throw new IllegalArgumentException("No Java packet definition found for packet ID: " + packetId); + } + + if (direction == MessageDirection.CLIENTBOUND) { + Registries.JAVA_PACKET_TRANSLATORS.translate(javaPacket.getClass(), javaPacket, this.session, true); + } else { + this.session.sendDownstreamPacket(javaPacket); + } } else { - this.session.getUpstream().getSession().getPacketHandler().handlePacket(bedrockPacket); + ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE_LE.createBuffer(); + packet.encode(buffer); + + BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); + BedrockCodecHelper helper = this.session.getUpstream().getCodecHelper(); + + BedrockPacket bedrockPacket = codec.tryDecode(helper, buffer.buffer(), packetId); + if (bedrockPacket == null) { + throw new IllegalArgumentException("No Bedrock packet definition found for packet ID: " + packetId); + } + + // Clientbound packets are sent upstream, serverbound packets are sent downstream + if (direction == MessageDirection.CLIENTBOUND) { + if (flagSet.contains(PacketFlag.IMMEDIATE)) { + this.session.sendUpstreamPacketImmediately(bedrockPacket); + } else { + this.session.sendUpstreamPacket(bedrockPacket); + } + } else { + this.session.getUpstream().getSession().getPacketHandler().handlePacket(bedrockPacket); + } } } @@ -219,26 +280,26 @@ private > List createMessages0( } @NonNull - public List getPacketChannels(int packetId) { - return this.packetChannels.getOrDefault(packetId, List.of()); + public List getPacketChannels(int packetId, boolean java) { + return this.packetChannels.getOrDefault(new PacketEntry(packetId, java), List.of()); } @SuppressWarnings("unchecked") - public boolean handlePacket(BedrockPacket packet, MessageDirection direction) { + public boolean handleBedrockPacket(BedrockPacket packet, MessageDirection direction) { if (this.packetChannels.isEmpty()) { return true; // Avoid processing anything if we have nothing to handle } BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); BedrockPacketDefinition definition = codec.getPacketDefinition((Class) packet.getClass()); - List channels = this.getPacketChannels(definition.getId()); + List channels = this.getPacketChannels(definition.getId(), false); if (channels.isEmpty()) { return true; } List> messages = new ArrayList<>(); for (PacketChannel channel : channels) { - if (channel.messageType().isInstance(packet)) { + if (((PacketChannelImpl) channel).messageType().isInstance(packet)) { messages.add(new BedrockPacketMessage<>(packet)); } else { ByteBuf buffer = Unpooled.buffer(); @@ -257,6 +318,45 @@ public boolean handlePacket(BedrockPacket packet, MessageDirection direction) { return true; } + public boolean handleJavaPacket(MinecraftPacket packet, MessageDirection direction) { + if (this.packetChannels.isEmpty()) { + return true; // Avoid processing anything if we have nothing to handle + } + + MinecraftProtocol protocol = this.session.getDownstream().getSession().getPacketProtocol(); + int packetId; + if (direction == MessageDirection.CLIENTBOUND) { + packetId = protocol.getInboundPacketRegistry().getClientboundId(packet.getClass()); + } else { + packetId = protocol.getOutboundPacketRegistry().getServerboundId(packet.getClass()); + } + + List channels = this.getPacketChannels(packetId, true); + if (channels.isEmpty()) { + return true; + } + + List> messages = new ArrayList<>(); + for (PacketChannel channel : channels) { + if (((PacketChannelImpl) channel).messageType().isInstance(packet)) { + messages.add(new JavaPacketMessage<>(packet)); + } else { + ByteBuf buffer = Unpooled.buffer(); + packet.serialize(buffer); + messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE, buffer))); + } + } + + for (NetworkChannel channel : channels) { + boolean handled = this.handleMessages(channel, messages, direction); + if (!handled) { + return false; + } + } + + return true; + } + @SuppressWarnings("unchecked") public boolean handleMessages(@NonNull NetworkChannel channel, @NonNull List> messages, @NonNull MessageDirection direction) { List> rawList = this.definitions.get(channel); @@ -268,6 +368,14 @@ public boolean handleMessages(@NonNull NetworkChannel List> ordered = new ArrayList<>(); List> unpinnedBlock = new ArrayList<>(); for (MessageDefinition def : rawList) { + // Ensure the packet states match + ProtocolState state = def.state; + if (direction == MessageDirection.CLIENTBOUND && state != null && state != this.javaState.inbound()) { + continue; + } else if (direction == MessageDirection.SERVERBOUND && state != null && state != this.javaState.outbound()) { + continue; + } + boolean pinned = def.tag() != null || def.beforeTag() != null || def.afterTag() != null; if (pinned) { // flush any accumulated unpinned block sorted by effective priority for this direction @@ -391,9 +499,12 @@ private > void registerMessage(@No if (channel.isPacket() && channel instanceof PacketChannel packetChannel) { int packetId = packetChannel.packetId(); - this.packetChannels.computeIfAbsent(packetId, key -> new ArrayList<>()).add(packetChannel); + this.packetChannels.computeIfAbsent(new PacketEntry(packetId, ((PacketChannelImpl) packetChannel).isJava()), key -> new ArrayList<>()).add(packetChannel); } } + + public record PacketEntry(int packetId, boolean java) { + } public record MessageDefinition>( @NonNull MessageCodec codec, @@ -404,6 +515,7 @@ public record MessageDefinition>( int priority, @Nullable Integer clientboundPriority, @Nullable Integer serverboundPriority, + @Nullable ProtocolState state, @Nullable String tag, @Nullable String beforeTag, @Nullable String afterTag diff --git a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java index f8a8cd53107..e1c05fdc7e7 100644 --- a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java +++ b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java @@ -28,6 +28,7 @@ import com.google.common.base.Preconditions; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; +import org.geysermc.geyser.api.network.ProtocolState; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; import org.geysermc.geyser.api.network.message.MessageHandler; @@ -37,6 +38,7 @@ import java.util.function.Consumer; public class NetworkDefinitionBuilder> implements SessionDefineNetworkChannelsEvent.Builder.Initial { + private ProtocolState protocolState; private HandlerEntry handler; private SidedHandlerEntry clientbound; private SidedHandlerEntry serverbound; @@ -52,6 +54,12 @@ public NetworkDefinitionBuilder(@NonNull Consumer> registrat this.registrationCallback = registrationCallback; } + @Override + public @NonNull Initial protocolState(@NonNull ProtocolState state) { + this.protocolState = Objects.requireNonNull(state, "state"); + return this; + } + @Override public SessionDefineNetworkChannelsEvent.Builder.@NonNull Initial pipeline(@NonNull Consumer pipeline) { Objects.requireNonNull(pipeline, "pipeline"); @@ -107,6 +115,7 @@ public NetworkDefinitionBuilder(@NonNull Consumer> registrat Preconditions.checkState(!this.registered, "This message has already been registered"); Preconditions.checkState(this.handler == null || (this.clientbound == null && this.serverbound == null), "Cannot register both bidirectional and sided handlers for the same message"); RegistrationImpl registration = new RegistrationImpl<>( + this.protocolState, this.handler, this.clientbound, this.serverbound, @@ -145,6 +154,7 @@ private static final class PipelineImpl implements SessionDefineNetworkChannelsE } public record RegistrationImpl>( + ProtocolState protocolState, HandlerEntry handler, SidedHandlerEntry clientbound, SidedHandlerEntry serverbound, diff --git a/core/src/main/java/org/geysermc/geyser/network/PacketChannel.java b/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java similarity index 73% rename from core/src/main/java/org/geysermc/geyser/network/PacketChannel.java rename to core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java index 9c8fc25d999..4733f14e54e 100644 --- a/core/src/main/java/org/geysermc/geyser/network/PacketChannel.java +++ b/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java @@ -27,41 +27,31 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.PacketChannel; import org.geysermc.geyser.api.util.Identifier; import java.util.Objects; -/** - * Represents a network channel associated with a packet. - *

- * This channel is used for listening to communication over - * packets between the server and client and can be used to - * send or receive packets. - */ -public class PacketChannel extends ExternalNetworkChannel { - private static final String PACKET_CHANNEL_KEY = "packet"; - +public class PacketChannelImpl extends ExternalNetworkChannel implements PacketChannel { private final int packetId; + private final boolean java; - public PacketChannel(@NonNull String key, @NonNegative int packetId, @NonNull Class packetType) { - super(Identifier.of(PACKET_CHANNEL_KEY, key), packetType); + public PacketChannelImpl(@NonNull Identifier identifier, boolean java, @NonNegative int packetId, @NonNull Class packetType) { + super(identifier, packetType); + this.java = java; this.packetId = packetId; } - /** - * Gets the packet ID associated with this channel. - * - * @return the packet ID - */ @NonNegative public int packetId() { return this.packetId; } - /** - * {@inheritDoc} - */ + public boolean isJava() { + return this.java; + } + @Override public boolean isPacket() { return true; @@ -71,7 +61,7 @@ public boolean isPacket() { public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; - PacketChannel that = (PacketChannel) o; + PacketChannelImpl that = (PacketChannelImpl) o; return this.packetId == that.packetId; } diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index bac125de820..a7ff5e1a8b7 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -109,11 +109,11 @@ public UpstreamPacketHandler(GeyserImpl geyser, GeyserSession session) { } private PacketSignal translateAndDefault(BedrockPacket packet) { - if (!this.session.getNetworkManager().handlePacket(packet, MessageDirection.SERVERBOUND)) { + if (!this.session.getNetwork().handleBedrockPacket(packet, MessageDirection.SERVERBOUND)) { return PacketSignal.HANDLED; } - Registries.BEDROCK_PACKET_TRANSLATORS.translate(packet.getClass(), packet, session, false); + Registries.BEDROCK_PACKET_TRANSLATORS.translate(packet.getClass(), packet, this.session, false); return PacketSignal.HANDLED; // PacketSignal.UNHANDLED will log a WARN publicly } diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java b/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java index d283538e2b6..8a40ab4edad 100644 --- a/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java +++ b/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java @@ -51,7 +51,7 @@ /** * Manages a Minecraft Java session over our LocalChannel implementations. */ -public final class LocalSession extends ClientNetworkSession { +public class LocalSession extends ClientNetworkSession { private static EventLoopGroup DEFAULT_EVENT_LOOP_GROUP; private static PreferredDirectByteBufAllocator PREFERRED_DIRECT_BYTE_BUF_ALLOCATOR = null; diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index a9d7cd44d3e..b41f61fcde5 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -42,6 +42,7 @@ import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.PacketChannel; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageCodec; import org.geysermc.geyser.api.pack.PathPackCodec; @@ -52,6 +53,7 @@ import org.geysermc.geyser.api.util.Identifier; import org.geysermc.geyser.event.GeyserEventRegistrar; import org.geysermc.geyser.extension.command.GeyserExtensionCommand; +import org.geysermc.geyser.impl.ExtensionIdentifierImpl; import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.impl.camera.GeyserCameraFade; import org.geysermc.geyser.impl.camera.GeyserCameraPosition; @@ -66,7 +68,7 @@ import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData; import org.geysermc.geyser.network.ExtensionNetworkChannel; import org.geysermc.geyser.network.ExternalNetworkChannel; -import org.geysermc.geyser.network.PacketChannel; +import org.geysermc.geyser.network.PacketChannelImpl; import org.geysermc.geyser.network.message.BedrockPacketMessage; import org.geysermc.geyser.network.message.ByteBufCodec; import org.geysermc.geyser.network.message.JavaPacketMessage; @@ -166,17 +168,35 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov // Extension network channel if (args.length == 3 && args[0] instanceof Extension extension && args[1] instanceof String channel && args[2] instanceof Class messageType) { return new ExtensionNetworkChannel(extension, channel, messageType); - } else if (args.length == 3 && args[0] instanceof String key && args[1] instanceof Integer packetId && args[2] instanceof Class packetType) { - // Packet channel - return new PacketChannel(key, packetId, packetType); } else if (args.length == 2 && args[0] instanceof Identifier identifier && args[1] instanceof Class messageType) { // External network channel return new ExternalNetworkChannel(identifier, messageType); } else { - throw new IllegalArgumentException("Unknown arguments provided for NetworkChannel provider. Could not create a channel given the arguments: " + Arrays.toString(args)); + throw new IllegalArgumentException("Unknown arguments provided for NetworkChannel provider. " + + "Could not create a channel given the arguments: " + Arrays.toString(args)); } }); + providers.put(PacketChannel.class, args -> { + if (args.length < 4) { + throw new IllegalArgumentException("PacketChannel requires at least four arguments, got " + args.length); + } + + if (args[0] instanceof Extension extension && args[1] instanceof String platform && args[2] instanceof Integer packetId + && args[3] instanceof Class packetType) { + return switch (platform) { + case "java" -> + new PacketChannelImpl(new ExtensionIdentifierImpl(extension, "java_packet_" + packetId), true, packetId, packetType); + case "bedrock" -> + new PacketChannelImpl(new ExtensionIdentifierImpl(extension, "bedrock_packet_" + packetId), false, packetId, packetType); + default -> throw new IllegalArgumentException("Unknown platform type for PacketChannel: " + platform); + }; + } + + throw new IllegalArgumentException("Unknown arguments provided for PacketChannel provider. " + + "Cloud not create a channel given the arguments: " + Arrays.toString(args)); + }); + return providers; } } diff --git a/core/src/main/java/org/geysermc/geyser/session/DownstreamSession.java b/core/src/main/java/org/geysermc/geyser/session/DownstreamSession.java index a55f84ab23f..dca3776cc35 100644 --- a/core/src/main/java/org/geysermc/geyser/session/DownstreamSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/DownstreamSession.java @@ -29,15 +29,22 @@ import lombok.RequiredArgsConstructor; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.mcprotocollib.network.ClientSession; +import org.geysermc.geyser.api.network.MessageDirection; import org.geysermc.mcprotocollib.network.packet.Packet; +import org.geysermc.mcprotocollib.network.session.ClientNetworkSession; +import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket; @Getter @RequiredArgsConstructor public class DownstreamSession { - private final ClientSession session; + private final GeyserSession geyserSession; + private final ClientNetworkSession session; public void sendPacket(@NonNull Packet packet) { + if (!this.geyserSession.getNetwork().handleJavaPacket((MinecraftPacket) packet, MessageDirection.SERVERBOUND)) { + return; + } + this.session.send(packet); } diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 1e6826f243f..3100ddd9a14 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -28,6 +28,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; import io.netty.channel.EventLoop; import it.unimi.dsi.fastutil.Pair; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -113,14 +114,14 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.bedrock.camera.CameraData; import org.geysermc.geyser.api.bedrock.camera.CameraShake; -import org.geysermc.geyser.input.InputLocksFlag; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.entity.EntityData; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent; import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent; -import org.geysermc.geyser.api.network.NetworkManager; +import org.geysermc.geyser.api.network.MessageDirection; +import org.geysermc.geyser.api.network.Network; import org.geysermc.geyser.api.network.RemoteServer; import org.geysermc.geyser.api.skin.SkinData; import org.geysermc.geyser.api.util.PlatformType; @@ -144,6 +145,7 @@ import org.geysermc.geyser.event.type.SessionDisconnectEventImpl; import org.geysermc.geyser.impl.camera.CameraDefinitions; import org.geysermc.geyser.impl.camera.GeyserCameraData; +import org.geysermc.geyser.input.InputLocksFlag; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.InventoryHolder; import org.geysermc.geyser.inventory.LecternContainer; @@ -157,7 +159,7 @@ import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.network.GameProtocol; -import org.geysermc.geyser.network.GeyserNetworkManager; +import org.geysermc.geyser.network.GeyserNetwork; import org.geysermc.geyser.network.netty.LocalSession; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.BlockMappings; @@ -201,12 +203,12 @@ import org.geysermc.geyser.util.MinecraftAuthLogger; import org.geysermc.mcprotocollib.auth.GameProfile; import org.geysermc.mcprotocollib.network.BuiltinFlags; -import org.geysermc.mcprotocollib.network.ClientSession; import org.geysermc.mcprotocollib.network.packet.Packet; import org.geysermc.mcprotocollib.network.session.ClientNetworkSession; import org.geysermc.mcprotocollib.protocol.ClientListener; import org.geysermc.mcprotocollib.protocol.MinecraftConstants; import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; +import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket; import org.geysermc.mcprotocollib.protocol.data.ProtocolState; import org.geysermc.mcprotocollib.protocol.data.game.ServerLink; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; @@ -757,7 +759,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private final Set inputLocksSet = EnumSet.noneOf(InputLocksFlag.class); private boolean inputLockDirty; - private final GeyserNetworkManager networkManager; + private final GeyserNetwork network; public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop tickEventLoop) { this.geyser = geyser; @@ -810,7 +812,7 @@ public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSessio } this.remoteServer = geyser.defaultRemoteServer(); - this.networkManager = new GeyserNetworkManager(this); + this.network = new GeyserNetwork(this); } /** @@ -1066,18 +1068,45 @@ private void connectDownstream() { // Start ticking tickThread = tickEventLoop.scheduleAtFixedRate(this::tick, nanosecondsPerTick, nanosecondsPerTick, TimeUnit.NANOSECONDS); - ClientSession downstream; + ClientNetworkSession downstream; if (geyser.getBootstrap().getSocketAddress() != null) { // We're going to connect through the JVM and not through TCP - downstream = new LocalSession(geyser.getBootstrap().getSocketAddress(), - upstream.getAddress().getAddress().getHostAddress(), - this.protocol, this.tickEventLoop); + downstream = new LocalSession( + geyser.getBootstrap().getSocketAddress(), + upstream.getAddress().getAddress().getHostAddress(), + this.protocol, this.tickEventLoop + ) { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Packet packet) { + if (!network.handleJavaPacket((MinecraftPacket) packet, MessageDirection.CLIENTBOUND)) { + return; + } + + super.channelRead0(ctx, packet); + } + }; downstream.setFlag(MinecraftConstants.CLIENT_HOST, this.remoteServer.address()); downstream.setFlag(MinecraftConstants.CLIENT_PORT, this.remoteServer.port()); - this.downstream = new DownstreamSession(downstream); + this.downstream = new DownstreamSession(this, downstream); } else { - downstream = new ClientNetworkSession(new InetSocketAddress(this.remoteServer.address(), this.remoteServer.port()), this.protocol, tickEventLoop, null, null); - this.downstream = new DownstreamSession(downstream); + downstream = new ClientNetworkSession( + new InetSocketAddress(this.remoteServer.address(), this.remoteServer.port()), + this.protocol, tickEventLoop, + null, null + ) { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Packet packet) { + if (!network.handleJavaPacket((MinecraftPacket) packet, MessageDirection.CLIENTBOUND)) { + return; + } + + super.channelRead0(ctx, packet); + } + }; + + this.downstream = new DownstreamSession(this, downstream); boolean resolveSrv = false; try { @@ -2474,8 +2503,8 @@ public boolean hasFormOpen() { @Override @NonNull - public NetworkManager networkManager() { - return this.networkManager; + public Network network() { + return this.network; } @Override diff --git a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java index e6548358318..84cd96a7661 100644 --- a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java @@ -48,7 +48,7 @@ public class UpstreamSession { private Queue postStartGamePackets = new ArrayDeque<>(); public void sendPacket(@NonNull BedrockPacket packet) { - if (!this.geyserSession.getNetworkManager().handlePacket(packet, MessageDirection.CLIENTBOUND)) { + if (!this.geyserSession.getNetwork().handleBedrockPacket(packet, MessageDirection.CLIENTBOUND)) { return; } @@ -58,7 +58,7 @@ public void sendPacket(@NonNull BedrockPacket packet) { } public void sendPacketImmediately(@NonNull BedrockPacket packet) { - if (!this.geyserSession.getNetworkManager().handlePacket(packet, MessageDirection.CLIENTBOUND)) { + if (!this.geyserSession.getNetwork().handleBedrockPacket(packet, MessageDirection.CLIENTBOUND)) { return; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java index 0ff8d59a04e..d6cc1017c34 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java @@ -44,7 +44,7 @@ import org.geysermc.geyser.api.network.NetworkChannel; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; -import org.geysermc.geyser.network.GeyserNetworkManager; +import org.geysermc.geyser.network.GeyserNetwork; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -153,8 +153,8 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack }); } else { session.ensureInEventLoop(() -> { - GeyserNetworkManager networkManager = session.getNetworkManager(); - Set channels = networkManager.registeredChannels(); + GeyserNetwork network = session.getNetwork(); + Set channels = network.registeredChannels(); if (channels.isEmpty()) { this.logger.debug("Received a custom payload for an unregistered channel: " + channel); return; @@ -173,8 +173,8 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack } for (NetworkChannel networkChannel : identifiedChannels) { - List> message = networkManager.createMessages(networkChannel, packet.getData()); - networkManager.handleMessages(networkChannel, message, MessageDirection.CLIENTBOUND); + List> message = network.createMessages(networkChannel, packet.getData()); + network.handleMessages(networkChannel, message, MessageDirection.CLIENTBOUND); } }); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index a91b3c8c684..be2db5ba964 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -133,7 +133,7 @@ public void translate(GeyserSession session, ClientboundLoginPacket packet) { } session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8))); - Set registeredChannels = session.getNetworkManager().registeredChannels(); + Set registeredChannels = session.getNetwork().registeredChannels(); if (!registeredChannels.isEmpty()) { String channels = registeredChannels .stream() diff --git a/core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java b/core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java index 78d102966e0..258f7084e81 100644 --- a/core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java +++ b/core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java @@ -52,32 +52,32 @@ public class MessageRegistrationOrderTest { void testRegistrationOrder() { mockContext(context -> { GeyserSession session = context.mock(GeyserSession.class); - GeyserNetworkManager manager = new GeyserNetworkManager(session); + GeyserNetwork network = new GeyserNetwork(session); AtomicInteger state = new AtomicInteger(0); - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(MessagePriority.EARLY, message -> { assertEquals(0, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; }) .register(); - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(MessagePriority.LATE, message -> { assertEquals(2, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; }) .register(); - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(MessagePriority.NORMAL, message -> { assertEquals(1, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; }) .register(); - manager.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); + network.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); }); } @@ -85,11 +85,11 @@ void testRegistrationOrder() { void testPipelineTags() { mockContext(context -> { GeyserSession session = context.mock(GeyserSession.class); - GeyserNetworkManager manager = new GeyserNetworkManager(session); + GeyserNetwork network = new GeyserNetwork(session); AtomicInteger state = new AtomicInteger(0); - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(message -> { assertEquals(2, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; @@ -99,7 +99,7 @@ void testPipelineTags() { }) .register(); - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(message -> { assertEquals(1, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; @@ -110,7 +110,7 @@ void testPipelineTags() { }) .register(); - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(message -> { assertEquals(3, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; @@ -122,7 +122,7 @@ void testPipelineTags() { .register(); // No pipeline, so should automatically be added to the tail - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(message -> { assertEquals(4, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; @@ -130,14 +130,14 @@ void testPipelineTags() { .register(); // Early priority - should come first regardless of tail structure - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(MessagePriority.EARLY, message -> { assertEquals(0, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; }) .register(); - manager.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); + network.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); }); } @@ -145,12 +145,12 @@ void testPipelineTags() { void testMixedHandlers() { mockContext(context -> { GeyserSession session = context.mock(GeyserSession.class); - GeyserNetworkManager manager = new GeyserNetworkManager(session); + GeyserNetwork network = new GeyserNetwork(session); AtomicInteger state = new AtomicInteger(0); // Simple early clientbound - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(MessagePriority.EARLY, message -> { assertEquals(0, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; @@ -158,7 +158,7 @@ void testMixedHandlers() { .register(); // Late clientbound but first serverbound - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .clientbound(MessagePriority.LATE, message -> { assertEquals(2, state.getAndIncrement()); return MessageHandler.State.UNHANDLED; @@ -170,7 +170,7 @@ void testMixedHandlers() { .register(); // Normal (default) bidirectional - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .bidirectional((message, direction) -> { if (direction == MessageDirection.SERVERBOUND) { fail("Serverbound handler should not be called in this test"); @@ -183,14 +183,14 @@ void testMixedHandlers() { .register(); // Serverbound only - should never be called - new NetworkDefinitionBuilder(registration -> manager.onRegister(this.xuidChannel, XuidMessage::new, registration)) + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) .serverbound(MessagePriority.NORMAL, message -> { fail("Serverbound handler should not be called in this test"); return MessageHandler.State.UNHANDLED; }) .register(); - manager.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); + network.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); }); } From d681186b55e8678aef95bc99f49be37488911c1a Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Thu, 25 Dec 2025 09:05:40 -0600 Subject: [PATCH 13/22] Update mappings --- core/src/main/resources/mappings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 15398c1588f..366baa68a21 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 15398c1588f5149824db8bd625e21f6886f98b10 +Subproject commit 366baa68a217220f4134eb19a158cdb2f0008426 From 57a92a63b731cf376ef52c7993cb5e42ce5cbfc8 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Thu, 25 Dec 2025 09:12:15 -0600 Subject: [PATCH 14/22] Update API version since --- .../geyser/api/connection/GeyserConnection.java | 3 ++- .../bedrock/SessionDefineNetworkChannelsEvent.java | 3 ++- .../org/geysermc/geyser/api/network/JavaState.java | 2 ++ .../geysermc/geyser/api/network/MessageDirection.java | 3 ++- .../org/geysermc/geyser/api/network/MessageFlag.java | 2 ++ .../java/org/geysermc/geyser/api/network/Network.java | 2 +- .../geysermc/geyser/api/network/NetworkChannel.java | 10 +++++----- .../org/geysermc/geyser/api/network/PacketChannel.java | 2 +- .../org/geysermc/geyser/api/network/PacketFlag.java | 2 ++ .../org/geysermc/geyser/api/network/ProtocolState.java | 2 ++ .../geysermc/geyser/api/network/message/DataType.java | 2 +- .../geysermc/geyser/api/network/message/Message.java | 3 ++- .../geyser/api/network/message/MessageBuffer.java | 3 ++- .../geyser/api/network/message/MessageCodec.java | 2 +- .../geyser/api/network/message/MessageFactory.java | 2 +- .../geyser/api/network/message/MessageHandler.java | 1 + .../geyser/api/network/message/MessagePriority.java | 3 ++- 17 files changed, 31 insertions(+), 16 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index 18229942baf..307edcef748 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -127,7 +127,8 @@ public interface GeyserConnection extends Connection, CommandSource { * Gets the {@link Network} used for handling * network channels and sending messages. * - * @return the network manager + * @return the network + * @since 2.9.2 */ @NonNull Network network(); diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index cd4a1d9053e..28a6cb98d49 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -41,7 +41,8 @@ /** * Called whenever Geyser is registering network channels. - * @since 2.9.1 + * + * @since 2.9.2 */ public abstract class SessionDefineNetworkChannelsEvent extends ConnectionEvent { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java b/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java index 195b80819ac..2735ec44dee 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java @@ -29,6 +29,8 @@ /** * Represents the state of a Java connection. + * + * @since 2.9.2 */ public interface JavaState { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java index bcb569618d2..7ec85949a38 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java @@ -27,7 +27,8 @@ /** * Represents the direction of a message. - * @since 2.9.1 + * + * @since 2.9.2 */ public enum MessageDirection { /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java b/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java index 848f168b463..7e49fa55671 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java @@ -27,6 +27,8 @@ /** * Represents flags that can be associated with network messages. + * + * @since 2.9.2 */ public interface MessageFlag { } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/Network.java b/api/src/main/java/org/geysermc/geyser/api/network/Network.java index 6b5dbe5ade7..37dc3c22566 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/Network.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/Network.java @@ -36,7 +36,7 @@ * Represents the network handler responsible for handling network operations * for a {@link GeyserConnection}. * - * @since 2.9.1 + * @since 2.9.2 */ public interface Network { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java index f995ab10a89..b51d56dd07b 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -56,12 +56,12 @@ * *

* For packet channels, it can get slightly more complex as you need to - * know the packet ID alongside having a constructed message type. The - * following example demonstrates this with the animate packet, assuming - * the AnimateMessage class represents the correct packet structure: + * know the packet ID, understand the constructed message type and have an extension + * available. The following example demonstrates this with the animate packet, + * assuming the AnimateMessage class represents the correct packet structure: *

  * {@code
- *    private final NetworkChannel animateChannel = NetworkChannel.packet("animate", 44, AnimateMessage.class);
+ *    private final NetworkChannel animateChannel = PacketChannel.bedrock(this, 44, AnimateMessage.class);
  * }
  * 
* @@ -71,7 +71,7 @@ * an example on how to do this, please see the * Networking API documentation. * - * @since 2.9.1 + * @since 2.9.2 */ public interface NetworkChannel { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java index d372721f90a..50414db53b6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java @@ -37,7 +37,7 @@ * packets between the server and client and can be used to * send or receive packets. * - * @since 2.9.1 + * @since 2.9.2 */ public interface PacketChannel extends NetworkChannel { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java index fc4f6f23293..f0d439c26e3 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java @@ -28,6 +28,8 @@ /** * Represents flags that can be applied to packets * to modify their behavior. + * + * @since 2.9.2 */ public enum PacketFlag implements MessageFlag { /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java b/api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java index 544e9aa3fae..f53937b7d2a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java @@ -27,6 +27,8 @@ /** * Represents the protocol state of a Java network connection. + * + * @since 2.9.2 */ public enum ProtocolState { /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java index b6fa7f9bf9c..ebfdd6876a3 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java @@ -36,7 +36,7 @@ * Represents a data type that can be sent or received over the network. * * @param the type - * @since 2.9.1 + * @since 2.9.2 */ public final class DataType { /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java index 2cc81fd2c51..62f4e0fb0bb 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java @@ -33,7 +33,8 @@ /** * Represents a message that can be sent over the network. - * @since 2.9.1 + * + * @since 2.9.2 */ public interface Message { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java index 6a39603de7c..c63555899d6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java @@ -29,7 +29,8 @@ /** * A buffer for messages that can be sent over the network. - * @since 2.9.1 + * + * @since 2.9.2 */ public interface MessageBuffer { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java index 8d3d2c79b59..0d91c4576e7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java @@ -34,7 +34,7 @@ * A codec for encoding and decoding messages. * * @param the type of {@link MessageBuffer} used for encoding and decoding - * @since 2.9.1 + * @since 2.9.2 */ public interface MessageCodec { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java index 14c3c7bb3cf..fc0dc89d3d4 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java @@ -32,7 +32,7 @@ * * @param the type of the message buffer * @param the type of the message - * @since 2.9.1 + * @since 2.9.2 */ @FunctionalInterface public interface MessageFactory> { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java index c5fb760ee90..bf55665b4f3 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java @@ -32,6 +32,7 @@ * Represents a handler for processing messages. * * @param the type of message to handle + * @since 2.9.2 */ @FunctionalInterface public interface MessageHandler> { diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java index 34b5231c760..4b3ab8c1006 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java @@ -29,7 +29,8 @@ /** * Represents the priority of a message when being processed. - * @since 2.9.1 + * + * @since 2.9.2 */ public enum MessagePriority { FIRST(100), From b6ee70b8cae688e7891ef9c81d9783dd951cdbde Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Thu, 25 Dec 2025 09:26:44 -0600 Subject: [PATCH 15/22] Fix Bedrock channel --- .../java/org/geysermc/geyser/api/network/PacketChannel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java index 50414db53b6..2f681ca272b 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java @@ -59,7 +59,7 @@ public interface PacketChannel extends NetworkChannel { */ @NonNull static PacketChannel bedrock(@NonNull Extension extension, @NonNegative int packetId, @NonNull Class packetType) { - return GeyserApi.api().provider(PacketChannel.class, "bedrock", extension, packetId, packetType); + return GeyserApi.api().provider(PacketChannel.class, extension, "bedrock", packetId, packetType); } /** From dd68e5c3cea7ae5eeac9e3c6339fa385d0c06e13 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Thu, 25 Dec 2025 11:05:09 -0600 Subject: [PATCH 16/22] Address some review comments --- .../SessionDefineNetworkChannelsEvent.java | 20 +++++++++++-------- .../network/NetworkDefinitionBuilder.java | 5 ++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index 28a6cb98d49..410be0717cb 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -54,7 +54,7 @@ public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection) { * Defines the registration of a new network channel with a message factory. * * @param channel the channel to register - * @param messageFactory the factory to create messages from the buffer + * @param messageFactory the factory used to create messages from the buffer * @param the message type created by the factory * @return a registration builder to configure handlers */ @@ -65,7 +65,7 @@ public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection) { * * @param channel the channel to register * @param codec the codec to use to encode/decode the buffer - * @param messageFactory the factory to create messages from the buffer + * @param messageFactory the factory used to create messages from the buffer * @param the buffer type * @param the message type created by the factory * @return a registration builder to configure handlers @@ -100,6 +100,9 @@ interface Initial> extends Sided, /** * Sets the protocol state for this message. + *

+ * This is required for any message involving Java Edition + * packets that need to be restricted to certain states. * * @param state the protocol state * @return the initial builder instance @@ -118,25 +121,25 @@ interface Initial> extends Sided, interface Sided> extends Builder { /** - * Register a clientbound handler. + * Registers a clientbound handler. */ @NonNull Sided clientbound(MessageHandler.@NonNull Sided handler); /** - * Register a clientbound handler with a priority. + * Registers a clientbound handler with a priority. */ @NonNull Sided clientbound(@NonNull MessagePriority priority, MessageHandler.@NonNull Sided handler); /** - * Register a serverbound handler. + * Registers a serverbound handler. */ @NonNull Sided serverbound(MessageHandler.@NonNull Sided handler); /** - * Register a serverbound handler with a priority. + * Registers a serverbound handler with a priority. */ @NonNull Sided serverbound(@NonNull MessagePriority priority, MessageHandler.@NonNull Sided handler); @@ -152,13 +155,14 @@ interface Sided> extends Builder { interface Bidirectional> extends Builder { /** - * Register a bidirectional handler receiving the message and direction. + * Registers a bidirectional handler which receives the message and passes + * through the direction. */ @NonNull Bidirectional bidirectional(@NonNull MessageHandler handler); /** - * Register a bidirectional handler with a priority. + * Registers a bidirectional handler with a priority. */ @NonNull Bidirectional bidirectional(@NonNull MessagePriority priority, @NonNull MessageHandler handler); diff --git a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java index e1c05fdc7e7..b834a50a539 100644 --- a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java +++ b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java @@ -136,7 +136,10 @@ private static final class PipelineImpl implements SessionDefineNetworkChannelsE @Override public SessionDefineNetworkChannelsEvent.Builder.@NonNull Pipeline tag(@NonNull String tag) { - this.tag = Objects.requireNonNull(tag, "tag"); + Preconditions.checkNotNull(tag, "tag"); + Preconditions.checkState(!tag.isBlank(), "tag"); + + this.tag = tag; return this; } From e2fa270f9e2fb4ead793244ae618d5582052a2c3 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Thu, 25 Dec 2025 14:10:51 -0600 Subject: [PATCH 17/22] Fix these too --- .../java/org/geysermc/geyser/network/PacketChannelImpl.java | 1 + .../geysermc/geyser/registry/loader/ProviderRegistryLoader.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java b/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java index 4733f14e54e..a183ee56ab4 100644 --- a/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java +++ b/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java @@ -43,6 +43,7 @@ public PacketChannelImpl(@NonNull Identifier identifier, boolean java, @NonNegat this.packetId = packetId; } + @Override @NonNegative public int packetId() { return this.packetId; diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index b41f61fcde5..61843ef7caa 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -194,7 +194,7 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov } throw new IllegalArgumentException("Unknown arguments provided for PacketChannel provider. " + - "Cloud not create a channel given the arguments: " + Arrays.toString(args)); + "Could not create a channel given the arguments: " + Arrays.toString(args)); }); return providers; From 406a1064417197b09585046362139f91fe624606 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 7 Feb 2026 19:49:54 +0000 Subject: [PATCH 18/22] Fix some bugs and add a state to the network channels event --- .../SessionDefineNetworkChannelsEvent.java | 64 ++++++++++++++++++- .../geyser/network/GeyserNetwork.java | 37 ++++++++--- .../geyser/network/UpstreamPacketHandler.java | 2 + .../geyser/session/GeyserSession.java | 3 + .../java/JavaCustomPayloadTranslator.java | 2 +- .../java/JavaLoginFinishedTranslator.java | 2 + 6 files changed, 100 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index 410be0717cb..b8c2a41c10a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -41,13 +41,43 @@ /** * Called whenever Geyser is registering network channels. + *

+ * It is important to note that this event may be called multiple times during a + * session's lifecycle, as channels can be registered at different stages of the connection. + * It is always important to check the {@link State} of the connection through {@link #state()} + * before registering channels to avoid creating multiple listeners that are only intended + * to be registered once. + *

+ * It is typically advised to register channels at the {@link State#LOGGED_IN} state unless + * creating a developer tool, such as a packet logger. This is the latest point at which channels + * can be registered, and is when clients will be fully established. Registering earlier means + * certain client information is unavailable or potentially unverified (i.e., Bedrock usernames). + *

+ * Registering at earlier points also increases the intensity of an L7 attack as the client is in a + * far earlier stage of the connection and there more vulnerable to invalid clients early in the + * connection stage. However, for debug tools that will never be used in a production environment, + * registering at earlier stages can be useful to capture more information about the connection and + * the client's behavior during earlier stages of the connection. * * @since 2.9.2 */ public abstract class SessionDefineNetworkChannelsEvent extends ConnectionEvent { + private final State state; - public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection) { + public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection, @NonNull State state) { super(connection); + + this.state = state; + } + + /** + * Gets the state of the connection at the time of channel registration. + * + * @return the registration state + */ + @NonNull + public State state() { + return this.state; } /** @@ -219,4 +249,36 @@ interface Pipeline { public interface Registration> { } + + /** + * The state of the connection at the time of channel registration. + *

+ * This allows for checking what state the client is in before + * registering channels, which offers greater flexibility and control + * for extensions that need to register channels. + */ + public enum State { + /** + * Called when a session is FIRST initialized. The session + * may lack certain information at this point, such as the Java + * session information, or verifiable Bedrock client info. However, + * this is the earliest point at which channels can be registered, + * and when it is guaranteed a session will have a protocol version. + */ + INITIALIZED, + /** + * Called when a Bedrock session is fully authenticated and has + * completed the Bedrock login process. At this point, the session + * will not have yet logged into the Java server, so Java client + * information will still be unavailable. + */ + AUTHENTICATED, + /** + * Called once a session has logged into the Java server. At this point, + * the session can be considered fully established, and all information + * about the client and connection will be available. This is the latest + * point at which channels can be registered. + */ + LOGGED_IN + } } diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java index dfb93c40fc5..8a82b981b70 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java @@ -59,6 +59,7 @@ import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; import javax.annotation.Nonnull; @@ -80,8 +81,10 @@ public class GeyserNetwork implements Network { public GeyserNetwork(GeyserSession session) { this.session = session; this.javaState = new GeyserJavaState(session); + } - SessionDefineNetworkChannelsEvent event = new SessionDefineNetworkChannelsEvent(session) { + public void defineNetworkChannels(SessionDefineNetworkChannelsEvent.@NonNull State state) { + SessionDefineNetworkChannelsEvent event = new SessionDefineNetworkChannelsEvent(session, state) { @Override public > Builder.@NonNull Initial define(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory) { @@ -247,18 +250,18 @@ public void send(@NonNull NetworkChannel channel, @Non } @NonNull - public List> createMessages(@NonNull NetworkChannel channel, byte @NonNull[] data) { - return this.createMessages0(channel, definition -> definition.createBuffer(data)); + public List> createMessages(@NonNull NetworkChannel channel, byte @NonNull[] data, @NotNull MessageDirection direction) { + return this.createMessages0(channel, definition -> definition.createBuffer(data), direction); } @NonNull - public List> createMessages(@NonNull NetworkChannel channel, @NonNull T buffer) { - return this.createMessages0(channel, def -> buffer); + public List> createMessages(@NonNull NetworkChannel channel, @NonNull T buffer, @NotNull MessageDirection direction) { + return this.createMessages0(channel, def -> buffer, direction); } @SuppressWarnings("unchecked") @NonNull - private > List createMessages0(@NonNull NetworkChannel channel, @NonNull Function, T> creator) { + private > List createMessages0(@NonNull NetworkChannel channel, @NonNull Function, T> creator, @NotNull MessageDirection direction) { List> definitions = this.definitions.get(channel); if (definitions == null || definitions.isEmpty()) { throw new IllegalArgumentException("No message definition registered for channel: " + channel); @@ -266,6 +269,24 @@ private > List createMessages0( List messages = new ArrayList<>(); for (MessageDefinition def : definitions) { + // Ensure the packet states match + ProtocolState state = def.state; + if (direction == MessageDirection.CLIENTBOUND && state != null && state != this.javaState.inbound()) { + continue; + } else if (direction == MessageDirection.SERVERBOUND && state != null && state != this.javaState.outbound()) { + continue; + } + + // Skip if there's no appropriate handler for this direction + if (def.handler == null) { + if (direction == MessageDirection.CLIENTBOUND && def.clientboundHandler == null) { + continue; + } + if (direction == MessageDirection.SERVERBOUND && def.serverboundHandler == null) { + continue; + } + } + MessageDefinition definition = (MessageDefinition) def; T buffer = creator.apply(definition); M message = definition.createMessage(buffer); @@ -304,7 +325,7 @@ public boolean handleBedrockPacket(BedrockPacket packet, MessageDirection direct } else { ByteBuf buffer = Unpooled.buffer(); definition.getSerializer().serialize(buffer, this.session.getUpstream().getCodecHelper(), packet); - messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer))); + messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer), direction)); } } @@ -343,7 +364,7 @@ public boolean handleJavaPacket(MinecraftPacket packet, MessageDirection directi } else { ByteBuf buffer = Unpooled.buffer(); packet.serialize(buffer); - messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE, buffer))); + messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE, buffer), direction)); } } diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 0022410c209..1adb4be3a44 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -54,6 +54,7 @@ import org.geysermc.api.util.BedrockPlatform; import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; import org.geysermc.geyser.api.event.bedrock.SessionInitializeEvent; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.MessageDirection; @@ -229,6 +230,7 @@ public PacketSignal handle(LoginPacket loginPacket) { // Fire SessionInitializeEvent here as we now know the client data geyser.eventBus().fire(new SessionInitializeEvent(session)); + session.getNetwork().defineNetworkChannels(SessionDefineNetworkChannelsEvent.State.INITIALIZED); PlayStatusPacket playStatus = new PlayStatusPacket(); playStatus.setStatus(PlayStatusPacket.Status.LOGIN_SUCCESS); diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index b4a342e80f4..a9f23f867e0 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -119,6 +119,7 @@ import org.geysermc.geyser.api.entity.EntityData; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; +import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent; import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent; import org.geysermc.geyser.api.network.MessageDirection; @@ -1079,6 +1080,8 @@ private void connectDownstream() { return; } + this.network.defineNetworkChannels(SessionDefineNetworkChannelsEvent.State.AUTHENTICATED); + this.cookies = loginEvent.cookies(); // Don't allow changing the remote server when it's not up to us // Just imagine the chaos of using an integrated world manager for an external server :) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java index d6cc1017c34..4b412644ee2 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java @@ -173,7 +173,7 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack } for (NetworkChannel networkChannel : identifiedChannels) { - List> message = network.createMessages(networkChannel, packet.getData()); + List> message = network.createMessages(networkChannel, packet.getData(), MessageDirection.CLIENTBOUND); network.handleMessages(networkChannel, message, MessageDirection.CLIENTBOUND); } }); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java index 559e747f05c..63d6f499a42 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.translator.protocol.java; import net.kyori.adventure.key.Key; +import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.session.GeyserSession; @@ -55,6 +56,7 @@ public void translate(GeyserSession session, ClientboundLoginFinishedPacket pack playerEntity.uuid(profile.getId()); session.getGeyser().getSessionManager().addSession(playerEntity.uuid(), session); + session.getNetwork().defineNetworkChannels(SessionDefineNetworkChannelsEvent.State.LOGGED_IN); // Check if they are not using a linked account if (remoteAuthType == AuthType.OFFLINE || playerEntity.uuid().getMostSignificantBits() == 0) { From 85e7af9e7580ba27669ad79d06ea99f1dbbe825f Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 27 Jun 2026 12:44:10 +0100 Subject: [PATCH 19/22] Update annotations and address a few comments --- .../api/connection/GeyserConnection.java | 1 - .../SessionDefineNetworkChannelsEvent.java | 52 +++++--------- .../geyser/api/network/JavaState.java | 4 -- .../geysermc/geyser/api/network/Network.java | 5 +- .../geyser/api/network/NetworkChannel.java | 11 +-- .../geyser/api/network/PacketChannel.java | 7 +- .../geyser/api/network/message/DataType.java | 33 ++++----- .../geyser/api/network/message/Message.java | 19 ++---- .../api/network/message/MessageBuffer.java | 10 +-- .../api/network/message/MessageCodec.java | 68 +++++++++---------- .../api/network/message/MessageFactory.java | 5 +- .../api/network/message/MessageHandler.java | 7 +- .../api/network/message/MessagePriority.java | 9 +-- .../api/network/message/package-info.java | 29 ++++++++ 14 files changed, 115 insertions(+), 145 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/message/package-info.java diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index 447af609f4c..052ea40940e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -129,7 +129,6 @@ public interface GeyserConnection extends Connection, CommandSource { * @return the network * @since 2.9.2 */ - @NonNull Network network(); /** diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index b8c2a41c10a..59530eb1509 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.api.event.bedrock; -import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.network.NetworkChannel; @@ -64,7 +64,7 @@ public abstract class SessionDefineNetworkChannelsEvent extends ConnectionEvent { private final State state; - public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection, @NonNull State state) { + public SessionDefineNetworkChannelsEvent(GeyserConnection connection, State state) { super(connection); this.state = state; @@ -75,7 +75,6 @@ public SessionDefineNetworkChannelsEvent(@NonNull GeyserConnection connection, @ * * @return the registration state */ - @NonNull public State state() { return this.state; } @@ -88,7 +87,7 @@ public State state() { * @param the message type created by the factory * @return a registration builder to configure handlers */ - public abstract > Builder.@NonNull Initial define(@NonNull NetworkChannel channel, @NonNull MessageFactory messageFactory); + public abstract > Builder.Initial define(NetworkChannel channel, MessageFactory messageFactory); /** * Defines the registration of a new network channel with a codec and message factory. @@ -100,7 +99,7 @@ public State state() { * @param the message type created by the factory * @return a registration builder to configure handlers */ - public abstract > Builder.@NonNull Initial define(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory); + public abstract > Builder.Initial define(NetworkChannel channel, MessageCodec codec, MessageFactory messageFactory); /** * Registration builder for attaching handlers to a channel. @@ -115,15 +114,13 @@ public interface Builder> { * @param pipeline the pipeline consumer * @return the builder instance */ - @NonNull - Builder pipeline(@NonNull Consumer pipeline); + @This Builder pipeline(Consumer pipeline); /** * Finalizes the registration. * * @return the completed registration */ - @NonNull Registration register(); interface Initial> extends Sided, Bidirectional { @@ -137,15 +134,13 @@ interface Initial> extends Sided, * @param state the protocol state * @return the initial builder instance */ - @NonNull - Initial protocolState(@NonNull ProtocolState state); + @This Initial protocolState(ProtocolState state); /** * {@inheritDoc} */ @Override - @NonNull - Initial pipeline(@NonNull Consumer pipeline); + @This Initial pipeline(Consumer pipeline); } interface Sided> extends Builder { @@ -153,33 +148,28 @@ interface Sided> extends Builder { /** * Registers a clientbound handler. */ - @NonNull - Sided clientbound(MessageHandler.@NonNull Sided handler); + @This Sided clientbound(MessageHandler.Sided handler); /** * Registers a clientbound handler with a priority. */ - @NonNull - Sided clientbound(@NonNull MessagePriority priority, MessageHandler.@NonNull Sided handler); + @This Sided clientbound(MessagePriority priority, MessageHandler.Sided handler); /** * Registers a serverbound handler. */ - @NonNull - Sided serverbound(MessageHandler.@NonNull Sided handler); + @This Sided serverbound(MessageHandler.Sided handler); /** * Registers a serverbound handler with a priority. */ - @NonNull - Sided serverbound(@NonNull MessagePriority priority, MessageHandler.@NonNull Sided handler); + @This Sided serverbound(MessagePriority priority, MessageHandler.Sided handler); /** * {@inheritDoc} */ @Override - @NonNull - Sided pipeline(@NonNull Consumer pipeline); + @This Sided pipeline(Consumer pipeline); } interface Bidirectional> extends Builder { @@ -188,21 +178,18 @@ interface Bidirectional> extends Buil * Registers a bidirectional handler which receives the message and passes * through the direction. */ - @NonNull - Bidirectional bidirectional(@NonNull MessageHandler handler); + @This Bidirectional bidirectional(MessageHandler handler); /** * Registers a bidirectional handler with a priority. */ - @NonNull - Bidirectional bidirectional(@NonNull MessagePriority priority, @NonNull MessageHandler handler); + @This Bidirectional bidirectional(MessagePriority priority, MessageHandler handler); /** * {@inheritDoc} */ @Override - @NonNull - Bidirectional pipeline(@NonNull Consumer pipeline); + @This Bidirectional pipeline(Consumer pipeline); } /** @@ -216,8 +203,7 @@ interface Pipeline { * @param tag the tag to apply * @return the pipeline instance */ - @NonNull - Pipeline tag(@NonNull String tag); + @This Pipeline tag(String tag); /** * Places this handler before the handler with the given tag. @@ -229,8 +215,7 @@ interface Pipeline { * @param tag the tag to place before * @return the pipeline instance */ - @NonNull - Pipeline before(@NonNull String tag); + @This Pipeline before(String tag); /** * Places this handler after the handler with the given tag. @@ -242,8 +227,7 @@ interface Pipeline { * @param tag the tag to place after * @return the pipeline instance */ - @NonNull - Pipeline after(@NonNull String tag); + @This Pipeline after(String tag); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java b/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java index 2735ec44dee..57b6e1824a7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.api.network; -import org.checkerframework.checker.nullness.qual.NonNull; - /** * Represents the state of a Java connection. * @@ -39,7 +37,6 @@ public interface JavaState { * * @return the inbound protocol state */ - @NonNull ProtocolState inbound(); /** @@ -47,6 +44,5 @@ public interface JavaState { * * @return the outbound protocol state */ - @NonNull ProtocolState outbound(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/Network.java b/api/src/main/java/org/geysermc/geyser/api/network/Network.java index 37dc3c22566..35233e163ca 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/Network.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/Network.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.api.network; -import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; @@ -45,7 +44,6 @@ public interface Network { * * @return the Java protocol state */ - @NonNull JavaState javaState(); /** @@ -53,7 +51,6 @@ public interface Network { * * @return the registered network channels */ - @NonNull Set registeredChannels(); /** @@ -64,5 +61,5 @@ public interface Network { * @param direction the direction of the message (clientbound or serverbound) * @param flags the message flags to use when sending the message */ - void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction, @NonNull MessageFlag... flags); + void send(NetworkChannel channel, Message message, MessageDirection direction, MessageFlag... flags); } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java index b51d56dd07b..23a10ee7290 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.api.network; -import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.util.Identifier; @@ -80,7 +79,6 @@ public interface NetworkChannel { * * @return the identifier that owns this channel */ - @NonNull Identifier identifier(); /** @@ -101,8 +99,7 @@ public interface NetworkChannel { * @param messageType the type of the message sent over this channel * @return a new external {@link NetworkChannel} instance */ - @NonNull - static NetworkChannel of(@NonNull Extension extension, @NonNull String channel, @NonNull Class messageType) { + static NetworkChannel of(Extension extension, String channel, Class messageType) { return GeyserApi.api().provider(NetworkChannel.class, extension, channel, messageType); } @@ -117,8 +114,7 @@ static NetworkChannel of(@NonNull Extension extension, @NonNull String channel, * @param messageType the type of the message sent over this channel * @return a new external {@link NetworkChannel} instance */ - @NonNull - static NetworkChannel of(@NonNull String id, @NonNull String channel, @NonNull Class messageType) { + static NetworkChannel of(String id, String channel, Class messageType) { return of(Identifier.of(id, channel), messageType); } @@ -132,8 +128,7 @@ static NetworkChannel of(@NonNull String id, @NonNull String channel, @NonNull C * @param messageType the type of the message sent over this channel * @return a new external {@link NetworkChannel} instance */ - @NonNull - static NetworkChannel of(@NonNull Identifier identifier, @NonNull Class messageType) { + static NetworkChannel of(Identifier identifier, Class messageType) { return GeyserApi.api().provider(NetworkChannel.class, identifier, messageType); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java index 2f681ca272b..88613484f13 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java @@ -26,7 +26,6 @@ package org.geysermc.geyser.api.network; import org.checkerframework.checker.index.qual.NonNegative; -import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.extension.Extension; @@ -57,8 +56,7 @@ public interface PacketChannel extends NetworkChannel { * @param packetType the type of the packet * @return a new packet {@link NetworkChannel} instance for a packet channel */ - @NonNull - static PacketChannel bedrock(@NonNull Extension extension, @NonNegative int packetId, @NonNull Class packetType) { + static PacketChannel bedrock(Extension extension, @NonNegative int packetId, Class packetType) { return GeyserApi.api().provider(PacketChannel.class, extension, "bedrock", packetId, packetType); } @@ -70,8 +68,7 @@ static PacketChannel bedrock(@NonNull Extension extension, @NonNegative int pack * @param packetType the type of the packet * @return a new packet {@link NetworkChannel} instance for a packet channel */ - @NonNull - static PacketChannel java(@NonNull Extension extension, @NonNegative int packetId, @NonNull Class packetType) { + static PacketChannel java(Extension extension, @NonNegative int packetId, Class packetType) { return GeyserApi.api().provider(PacketChannel.class, extension, "java", packetId, packetType); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java index ebfdd6876a3..89d51514965 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.api.network.message; -import org.checkerframework.checker.nullness.qual.NonNull; - import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -97,7 +95,7 @@ public final class DataType { private final Reader reader; private final Writer writer; - private DataType(@NonNull Reader reader, @NonNull Writer writer) { + private DataType(Reader reader, Writer writer) { this.reader = reader; this.writer = writer; } @@ -109,8 +107,7 @@ private DataType(@NonNull Reader reader, @NonNull Writer writer) { * @param buffer the buffer to read from * @return the read value */ - @NonNull - public > T read(@NonNull C codec, @NonNull B buffer) { + public > T read(C codec, B buffer) { return this.reader.read(codec, buffer); } @@ -121,7 +118,7 @@ public > T read(@NonNull C co * @param buffer the buffer to write to * @param value the value to write */ - public > void write(@NonNull C codec, @NonNull B buffer, @NonNull T value) { + public > void write(C codec, B buffer, T value) { this.writer.write(codec, buffer, value); } @@ -133,8 +130,7 @@ public > void write(@NonNull * @param the type of values this DataType handles * @return a new DataType instance */ - @NonNull - public static DataType of(@NonNull Reader reader, @NonNull Writer writer) { + public static DataType of(Reader reader, Writer writer) { return new DataType<>(reader, writer); } @@ -145,13 +141,11 @@ public static DataType of(@NonNull Reader reader, @NonNull Writer w * @return a DataType that reads and writes Optional values * @param the type of the value contained in the Optional */ - @NonNull - public static DataType> optional(@NonNull DataType type) { + public static DataType> optional(DataType type) { return new DataType<>(new Reader<>() { @Override - @NonNull - public > Optional read(@NonNull C codec, @NonNull B buffer) { + public > Optional read(C codec, B buffer) { if (codec.readBoolean(buffer)) { return Optional.of(type.read(codec, buffer)); } else { @@ -161,7 +155,7 @@ public > Optional read(@No }, new Writer<>() { @Override - public > void write(@NonNull C codec, @NonNull B buffer, @NonNull Optional value) { + public > void write(C codec, B buffer, Optional value) { codec.writeBoolean(buffer, value.isPresent()); value.ifPresent(t -> type.write(codec, buffer, t)); } @@ -175,13 +169,11 @@ public > void write(@NonNull * @return a DataType that reads and writes lists of the specified type * @param the type of the elements in the list */ - @NonNull - public static DataType> list(@NonNull DataType type) { + public static DataType> list(DataType type) { return new DataType<>(new Reader<>() { @Override - @NonNull - public > List read(@NonNull C codec, @NonNull B buffer) { + public > List read(C codec, B buffer) { int size = codec.readUnsignedVarInt(buffer); List list = new ArrayList<>(size); for (int i = 0; i < size; i++) { @@ -193,7 +185,7 @@ public > List read(@NonNul }, new Writer<>() { @Override - public > void write(@NonNull C codec, @NonNull B buffer, @NonNull List value) { + public > void write(C codec, B buffer, List value) { codec.writeUnsignedVarInt(buffer, value.size()); for (T element : value) { type.write(codec, buffer, element); @@ -216,8 +208,7 @@ public interface Reader { * @param buffer the buffer to read from * @return the read value */ - @NonNull - > T read(@NonNull C codec, @NonNull B buffer); + > T read(C codec, B buffer); } /** @@ -234,6 +225,6 @@ public interface Writer { * @param buffer the buffer to write to * @param value the value to write */ - > void write(@NonNull C codec, @NonNull B buffer, @NonNull T value); + > void write(C codec, B buffer, T value); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java index 62f4e0fb0bb..d60277673e5 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.api.network.message; -import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; import java.util.function.Function; @@ -39,11 +38,11 @@ public interface Message { /** - * Reads the message from the provided buffer. + * Encodes the message to the given buffer. * - * @param buffer the buffer to read from + * @param buffer the buffer to write to */ - void encode(@NonNull T buffer); + void encode(T buffer); /** * Represents a simple message using the built-in {@link MessageBuffer} implementation. @@ -71,19 +70,17 @@ interface Packet extends PacketBase { * @return a new packet message */ @SuppressWarnings("unchecked") - @NonNull - static PacketWrapped of(@NonNull Object packet) { + static PacketWrapped of(Object packet) { return (PacketWrapped) GeyserApi.api().provider(PacketWrapped.class, packet); } /** - * Creates a new packet message from the given packet object and direction. + * Creates a new packet message from the given packet supplier. * * @param packetSupplier a supplier that provides the packet object to create the message from * @return a new packet message */ - @NonNull - static MessageFactory> of(@NonNull Supplier

packetSupplier) { + static MessageFactory> of(Supplier

packetSupplier) { return buffer -> of(packetSupplier.get()); } @@ -94,8 +91,7 @@ static MessageFactory> of(@N * @param packetSupplier a function that provides the packet object * @return a new packet message factory */ - @NonNull - static MessageFactory> of(@NonNull Function substitutor, @NonNull Function packetSupplier) { + static MessageFactory> of(Function substitutor, Function packetSupplier) { return buffer -> of(packetSupplier.apply(substitutor.apply(buffer))); } } @@ -112,7 +108,6 @@ interface PacketWrapped extends PacketBase { * * @return the packet */ - @NonNull P packet(); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java index c63555899d6..d4138a6ce63 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.api.network.message; -import org.checkerframework.checker.nullness.qual.NonNull; - /** * A buffer for messages that can be sent over the network. * @@ -42,8 +40,7 @@ public interface MessageBuffer { * @return the read message * @param the type of message to read */ - @NonNull - T read(@NonNull DataType type); + T read(DataType type); /** * Writes a {@link T value} to the buffer using the @@ -53,14 +50,14 @@ public interface MessageBuffer { * @param value the value to write * @param the type of message to write */ - void write(@NonNull DataType type, @NonNull T value); + void write(DataType type, T value); /** * Serializes the buffer to a byte array. * * @return the serialized byte array */ - byte @NonNull [] serialize(); + byte[] serialize(); /** * Gets the length of the buffer. @@ -81,7 +78,6 @@ interface Wrapped extends MessageBuffer { * * @return the internal buffer */ - @NonNull T buffer(); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java index 0d91c4576e7..61f2d4022e6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.api.network.message; -import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; import java.util.UUID; @@ -44,7 +43,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the boolean value read */ - boolean readBoolean(@NonNull T buffer); + boolean readBoolean(T buffer); /** * Reads a byte value from the {@link T buffer}. @@ -52,7 +51,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the byte value read */ - byte readByte(@NonNull T buffer); + byte readByte(T buffer); /** * Reads a short value from the {@link T buffer}. @@ -60,7 +59,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the short value read */ - short readShort(@NonNull T buffer); + short readShort(T buffer); /** * Reads an integer value from the {@link T buffer}. @@ -68,7 +67,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the integer value read */ - int readInt(@NonNull T buffer); + int readInt(T buffer); /** * Reads a float value from the {@link T buffer}. @@ -76,7 +75,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the float value read */ - float readFloat(@NonNull T buffer); + float readFloat(T buffer); /** * Reads a double value from the {@link T buffer}. @@ -84,7 +83,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the double value read */ - double readDouble(@NonNull T buffer); + double readDouble(T buffer); /** * Reads a long value from the {@link T buffer}. @@ -92,7 +91,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the long value read */ - long readLong(@NonNull T buffer); + long readLong(T buffer); /** * Reads a variable-length integer from the {@link T buffer}. @@ -100,7 +99,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the variable-length integer read */ - int readVarInt(@NonNull T buffer); + int readVarInt(T buffer); /** * Reads an unsigned variable-length integer from the {@link T buffer}. @@ -108,7 +107,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the unsigned variable-length integer read */ - int readUnsignedVarInt(@NonNull T buffer); + int readUnsignedVarInt(T buffer); /** * Reads a variable-length long from the {@link T buffer}. @@ -116,7 +115,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the variable-length long read */ - long readVarLong(@NonNull T buffer); + long readVarLong(T buffer); /** * Reads an unsigned variable-length long from the {@link T buffer}. @@ -124,7 +123,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the unsigned variable-length long read */ - long readUnsignedVarLong(@NonNull T buffer); + long readUnsignedVarLong(T buffer); /** * Reads a string from the {@link T buffer}. @@ -132,8 +131,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the string read */ - @NonNull - String readString(@NonNull T buffer); + String readString(T buffer); /** * Reads a string from the {@link T buffer}. @@ -142,8 +140,7 @@ public interface MessageCodec { * @param maxLength the maximum length of the string to read * @return the string read */ - @NonNull - String readString(@NonNull T buffer, int maxLength); + String readString(T buffer, int maxLength); /** * Reads a UUID from the {@link T buffer}. @@ -151,8 +148,7 @@ public interface MessageCodec { * @param buffer the buffer to read from * @return the UUID read */ - @NonNull - UUID readUuid(@NonNull T buffer); + UUID readUuid(T buffer); /** * Writes a boolean value to the {@link T buffer}. @@ -160,7 +156,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the boolean value to write */ - void writeBoolean(@NonNull T buffer, boolean value); + void writeBoolean(T buffer, boolean value); /** * Writes a byte value to the {@link T buffer}. @@ -168,7 +164,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the byte value to write */ - void writeByte(@NonNull T buffer, byte value); + void writeByte(T buffer, byte value); /** * Writes a short value to the {@link T buffer}. @@ -176,7 +172,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the short value to write */ - void writeShort(@NonNull T buffer, short value); + void writeShort(T buffer, short value); /** * Writes an integer value to the {@link T buffer}. @@ -184,7 +180,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the integer value to write */ - void writeInt(@NonNull T buffer, int value); + void writeInt(T buffer, int value); /** * Writes a float value to the {@link T buffer}. @@ -192,7 +188,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the float value to write */ - void writeFloat(@NonNull T buffer, float value); + void writeFloat(T buffer, float value); /** * Writes a double value to the {@link T buffer}. @@ -200,7 +196,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the double value to write */ - void writeDouble(@NonNull T buffer, double value); + void writeDouble(T buffer, double value); /** * Writes a long value to the {@link T buffer}. @@ -208,7 +204,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the long value to write */ - void writeLong(@NonNull T buffer, long value); + void writeLong(T buffer, long value); /** * Writes a variable-length integer to the {@link T buffer}. @@ -216,7 +212,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the variable-length integer to write */ - void writeVarInt(@NonNull T buffer, int value); + void writeVarInt(T buffer, int value); /** * Writes an unsigned variable-length integer to the {@link T buffer}. @@ -224,7 +220,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the unsigned variable-length integer to write */ - void writeUnsignedVarInt(@NonNull T buffer, int value); + void writeUnsignedVarInt(T buffer, int value); /** * Writes a variable-length long to the {@link T buffer}. @@ -232,7 +228,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the variable-length long to write */ - void writeVarLong(@NonNull T buffer, long value); + void writeVarLong(T buffer, long value); /** * Writes an unsigned variable-length long to the {@link T buffer}. @@ -240,7 +236,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the unsigned variable-length long to write */ - void writeUnsignedVarLong(@NonNull T buffer, long value); + void writeUnsignedVarLong(T buffer, long value); /** * Writes a string to the {@link T buffer}. @@ -248,7 +244,7 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param value the string to write */ - void writeString(@NonNull T buffer, @NonNull String value); + void writeString(T buffer, String value); /** * Writes a UUID to the {@link T buffer}. @@ -256,14 +252,13 @@ public interface MessageCodec { * @param buffer the buffer to write to * @param uuid the UUID to write */ - void writeUuid(@NonNull T buffer, @NonNull UUID uuid); + void writeUuid(T buffer, UUID uuid); /** * Creates a new {@link T buffer} instance. * * @return a new instance of {@link MessageBuffer} */ - @NonNull T createBuffer(); /** @@ -272,8 +267,7 @@ public interface MessageCodec { * @param data the byte array to initialize the buffer with * @return a new instance of {@link MessageBuffer} initialized with the provided data */ - @NonNull - T createBuffer(byte @NonNull [] data); + T createBuffer(byte[] data); /** * Gets a provided {@link MessageCodec} for the given type. @@ -285,8 +279,7 @@ public interface MessageCodec { * @return the provided message codec * @throws IllegalArgumentException if no provider is found */ - @NonNull - static >> C provided(@NonNull Class type, @NonNull EncoderOptions... options) { + static >> C provided(Class type, EncoderOptions... options) { return GeyserApi.api().provider(MessageCodec.class, type, options); } @@ -294,6 +287,9 @@ static >> C provided(@NonNull * Options for the encoder. */ enum EncoderOptions { + /** + * Little endian encoding. + */ LITTLE_ENDIAN } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java index fc0dc89d3d4..7898c1677a2 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.api.network.message; -import org.checkerframework.checker.nullness.qual.NonNull; - /** * A factory interface for creating messages from a given message buffer. * @@ -43,6 +41,5 @@ public interface MessageFactory> { * @param buffer the buffer to create the message from * @return a new message created from the buffer */ - @NonNull - M create(@NonNull T buffer); + M create(T buffer); } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java index bf55665b4f3..b7caf6f1938 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.api.network.message; -import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.network.MessageDirection; /** @@ -44,8 +43,7 @@ public interface MessageHandler> { * @param direction the direction of the message * @return the state after handling the message */ - @NonNull - State handle(@NonNull T message, @NonNull MessageDirection direction); + State handle(T message, MessageDirection direction); /** * A message handler that belongs to a specific side (clientbound or serverbound). @@ -60,8 +58,7 @@ interface Sided> { * @param message the message to handle * @return the state after handling the message */ - @NonNull - State handle(@NonNull T message); + State handle(T message); } /** diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java index 4b3ab8c1006..6b55ebb3f54 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.api.network.message; -import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.value.qual.IntRange; /** * Represents the priority of a message when being processed. @@ -55,13 +55,14 @@ public int value() { } /** - * Creates a custom priority in the range [-100, 100]. + * Returns a {@link MessagePriority} based on the given value. + *

+ * If the value is out of range, it will return {@link #NORMAL}. * * @param value the priority value * @return the priority */ - @NonNull - public static MessagePriority of(int value) { + public static MessagePriority of(@IntRange(from = -100, to = 100) int value) { if (value >= 75) return FIRST; if (value >= 25) return EARLY; if (value <= -75) return LAST; diff --git a/api/src/main/java/org/geysermc/geyser/api/network/message/package-info.java b/api/src/main/java/org/geysermc/geyser/api/network/message/package-info.java new file mode 100644 index 00000000000..173963009a7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +@NullMarked +package org.geysermc.geyser.api.network.message; + +import org.jspecify.annotations.NullMarked; From 9a679a968509b3033f8e485b77fe3a8718cda888 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 27 Jun 2026 14:04:09 +0100 Subject: [PATCH 20/22] Introduce an earlier state for network channels --- .../SessionDefineNetworkChannelsEvent.java | 19 +++++++++++++++---- .../network/GeyserServerInitializer.java | 3 +++ .../java/JavaCustomPayloadTranslator.java | 15 +++++++++------ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index 59530eb1509..a86bf05882d 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -54,7 +54,7 @@ * certain client information is unavailable or potentially unverified (i.e., Bedrock usernames). *

* Registering at earlier points also increases the intensity of an L7 attack as the client is in a - * far earlier stage of the connection and there more vulnerable to invalid clients early in the + * far earlier stage of the connection and therefore more vulnerable to invalid clients early in the * connection stage. However, for debug tools that will never be used in a production environment, * registering at earlier stages can be useful to capture more information about the connection and * the client's behavior during earlier stages of the connection. @@ -242,12 +242,23 @@ public interface Registration> { * for extensions that need to register channels. */ public enum State { + /** + * Called when a session is created. This is the earliest point at which + * channels can be registered. This is a VERY early point in the connection, + * meaning very little to no information about the client is available yet. + *

+ * It is only recommended to listen for this state if you are creating a developer + * tool such as a packet logger, or creating proxy software where the Geyser instance + * is not exposed to the public internet. It is assumed that in this scenario, the + * proxy software has taken all precautions to ensure that the client is not malicious. + */ + CREATED, /** * Called when a session is FIRST initialized. The session * may lack certain information at this point, such as the Java - * session information, or verifiable Bedrock client info. However, - * this is the earliest point at which channels can be registered, - * and when it is guaranteed a session will have a protocol version. + * session information, or verifiable Bedrock client info. This + * is the earliest point when it is guaranteed a session will have + * a protocol version. */ INITIALIZED, /** diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserServerInitializer.java b/core/src/main/java/org/geysermc/geyser/network/GeyserServerInitializer.java index 7ccab594f4c..520c3e0061c 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GeyserServerInitializer.java +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserServerInitializer.java @@ -36,6 +36,7 @@ import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; import org.cloudburstmc.protocol.bedrock.netty.initializer.BedrockServerInitializer; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; import org.geysermc.geyser.session.GeyserSession; public class GeyserServerInitializer extends BedrockServerInitializer { @@ -69,6 +70,8 @@ public void initSession(@NonNull BedrockServerSession bedrockServerSession) { channel.pipeline().addAfter(BedrockPacketCodec.NAME, InvalidPacketHandler.NAME, new InvalidPacketHandler(session)); } + session.getNetwork().defineNetworkChannels(SessionDefineNetworkChannelsEvent.State.CREATED); + bedrockServerSession.setPacketHandler(new UpstreamPacketHandler(this.geyser, session)); } catch (Throwable e) { // Error must be caught or it will be swallowed diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java index b5b6a56451e..1ce71468a1b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java @@ -54,7 +54,9 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; @Translator(packet = ClientboundCustomPayloadPacket.class) @@ -154,8 +156,7 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack session.sendUpstreamPacket(toSend); }); - } else { - session.ensureInEventLoop(() -> { + default -> session.ensureInEventLoop(() -> { GeyserNetwork network = session.getNetwork(); Set channels = network.registeredChannels(); if (channels.isEmpty()) { @@ -163,14 +164,16 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack return; } - List identifiedChannels = new ArrayList<>(); + Map> channelMap = new HashMap<>(); for (NetworkChannel registeredChannel : channels) { - if (!registeredChannel.isPacket() && registeredChannel.identifier().toString().equals(channel)) { - identifiedChannels.add(registeredChannel); + if (!registeredChannel.isPacket()) { + String identifier = registeredChannel.identifier().toString(); + channelMap.computeIfAbsent(identifier, k -> new ArrayList<>()).add(registeredChannel); } } - if (identifiedChannels.isEmpty()) { + List identifiedChannels = channelMap.get(channel); + if (identifiedChannels == null || identifiedChannels.isEmpty()) { this.logger.debug("Received a custom payload for an unregistered channel: " + channel); return; } From 7b0a29bcd285266491158e569f2e0a7402ab0fdc Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 27 Jun 2026 17:18:30 +0100 Subject: [PATCH 21/22] Address more comments and add a test for message dispatches --- .../geyser/network/GeyserNetwork.java | 101 ++++++---- .../java/JavaCustomPayloadTranslator.java | 6 +- .../geyser/network/MessageDispatchTest.java | 175 ++++++++++++++++++ 3 files changed, 243 insertions(+), 39 deletions(-) create mode 100644 core/src/test/java/org/geysermc/geyser/network/MessageDispatchTest.java diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java index 8a82b981b70..e94e19213ad 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java @@ -64,7 +64,9 @@ import javax.annotation.Nonnull; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -107,7 +109,7 @@ > void onRegister(@NonNull NetworkChannel chann @SuppressWarnings("unchecked") private > void onRegister(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, - @NonNull MessageFactory messageFactory, NetworkDefinitionBuilder.@NonNull RegistrationImpl registration) { + @NonNull MessageFactory messageFactory, NetworkDefinitionBuilder.@NonNull RegistrationImpl registration) { if (channel instanceof PacketChannelImpl packetChannel && packetChannel.isJava() && registration.protocolState() == null) { throw new IllegalArgumentException("Java packet channels must specify a protocol state"); } @@ -164,7 +166,7 @@ public Set registeredChannels() { @Override public void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction, @NonNull MessageFlag... flags) { - Set flagSet = Set.of(flags); + Set flagSet = flags.length == 0 ? Set.of() : new HashSet<>(Arrays.asList(flags)); if (channel.isPacket() && message instanceof Message.PacketBase packetBase) { if (packetBase instanceof BedrockPacketMessage packetMessage) { if (direction == MessageDirection.CLIENTBOUND) { @@ -236,40 +238,43 @@ public void send(@NonNull NetworkChannel channel, @Non return; } + // TODO: Proxy level-support for sending custom payloads up to the proxy if Geyser is on + // the backend + if (direction != MessageDirection.SERVERBOUND) { + throw new IllegalArgumentException("Non-packet network channels only support SERVERBOUND sending (custom payload to the Java server)"); + } + MessageDefinition> definition = this.findMessageDefinition(channel, message); T buffer = definition.codec.createBuffer(); message.encode(buffer); - ServerboundCustomPayloadPacket packet = new ServerboundCustomPayloadPacket( - Key.key(channel.identifier().toString()), - buffer.serialize() - ); + Key channelKey = Key.key(channel.identifier().toString()); + byte[] data = buffer.serialize(); + ServerboundCustomPayloadPacket packet = new ServerboundCustomPayloadPacket(channelKey, data); this.session.sendDownstreamPacket(packet); } - @NonNull - public List> createMessages(@NonNull NetworkChannel channel, byte @NonNull[] data, @NotNull MessageDirection direction) { - return this.createMessages0(channel, definition -> definition.createBuffer(data), direction); + @Nullable + public Message createMessage(@NonNull NetworkChannel channel, byte @NonNull[] data, @NotNull MessageDirection direction) { + return this.createMessage0(channel, definition -> definition.createBuffer(data), direction); } - @NonNull - public List> createMessages(@NonNull NetworkChannel channel, @NonNull T buffer, @NotNull MessageDirection direction) { - return this.createMessages0(channel, def -> buffer, direction); + @Nullable + public Message createMessage(@NonNull NetworkChannel channel, @NonNull T buffer, @NotNull MessageDirection direction) { + return this.createMessage0(channel, def -> buffer, direction); } @SuppressWarnings("unchecked") - @NonNull - private > List createMessages0(@NonNull NetworkChannel channel, @NonNull Function, T> creator, @NotNull MessageDirection direction) { + @Nullable + private > M createMessage0(@NonNull NetworkChannel channel, @NonNull Function, T> creator, @NotNull MessageDirection direction) { List> definitions = this.definitions.get(channel); if (definitions == null || definitions.isEmpty()) { throw new IllegalArgumentException("No message definition registered for channel: " + channel); } - List messages = new ArrayList<>(); for (MessageDefinition def : definitions) { - // Ensure the packet states match ProtocolState state = def.state; if (direction == MessageDirection.CLIENTBOUND && state != null && state != this.javaState.inbound()) { continue; @@ -277,7 +282,6 @@ private > List createMessages0( continue; } - // Skip if there's no appropriate handler for this direction if (def.handler == null) { if (direction == MessageDirection.CLIENTBOUND && def.clientboundHandler == null) { continue; @@ -294,10 +298,10 @@ private > List createMessages0( packetMessage.postProcess(this.session, (ByteBufMessageBuffer) buffer); } - messages.add(message); + return message; } - return messages; + return null; } @NonNull @@ -318,18 +322,27 @@ public boolean handleBedrockPacket(BedrockPacket packet, MessageDirection direct return true; } - List> messages = new ArrayList<>(); for (PacketChannel channel : channels) { + List> messages = new ArrayList<>(); if (((PacketChannelImpl) channel).messageType().isInstance(packet)) { messages.add(new BedrockPacketMessage<>(packet)); } else { ByteBuf buffer = Unpooled.buffer(); - definition.getSerializer().serialize(buffer, this.session.getUpstream().getCodecHelper(), packet); - messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer), direction)); + try { + definition.getSerializer().serialize(buffer, this.session.getUpstream().getCodecHelper(), packet); + + byte[] data = new byte[buffer.readableBytes()]; + buffer.getBytes(buffer.readerIndex(), data); + + Message message = this.createMessage(channel, data, direction); + if (message != null) { + messages.add(message); + } + } finally { + buffer.release(); + } } - } - for (NetworkChannel channel : channels) { boolean handled = this.handleMessages(channel, messages, direction); if (!handled) { return false; @@ -357,18 +370,27 @@ public boolean handleJavaPacket(MinecraftPacket packet, MessageDirection directi return true; } - List> messages = new ArrayList<>(); for (PacketChannel channel : channels) { + List> messages = new ArrayList<>(); if (((PacketChannelImpl) channel).messageType().isInstance(packet)) { messages.add(new JavaPacketMessage<>(packet)); } else { ByteBuf buffer = Unpooled.buffer(); - packet.serialize(buffer); - messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE, buffer), direction)); + try { + packet.serialize(buffer); + + byte[] data = new byte[buffer.readableBytes()]; + buffer.getBytes(buffer.readerIndex(), data); + + Message message = this.createMessage(channel, data, direction); + if (message != null) { + messages.add(message); + } + } finally { + buffer.release(); + } } - } - for (NetworkChannel channel : channels) { boolean handled = this.handleMessages(channel, messages, direction); if (!handled) { return false; @@ -425,7 +447,7 @@ public boolean handleMessages(@NonNull NetworkChannel for (Message message : messages) { for (MessageDefinition def : ordered) { if (!(channel instanceof BaseNetworkChannel base)) { - continue; + throw new IllegalArgumentException("Unsupported NetworkChannel implementation: " + channel.getClass().getName() + "; expected BaseNetworkChannel"); } if (message instanceof Message.PacketWrapped wrapped && !base.messageType().isInstance(wrapped.packet())) { @@ -505,13 +527,18 @@ private > void registerMessage(@No } if (insertIndex == -1) { - // Fallback: insert by descending priority - insertIndex = list.size(); - for (int i = 0; i < list.size(); i++) { - MessageDefinition existing = list.get(i); - if (definition.priority() > existing.priority()) { - insertIndex = i; - break; + if (definition.beforeTag() != null || definition.afterTag() != null) { + // Relative tag was specified but anchor not found; per API docs, append to end. + insertIndex = list.size(); + } else { + // Fallback: insert by descending priority + insertIndex = list.size(); + for (int i = 0; i < list.size(); i++) { + MessageDefinition existing = list.get(i); + if (definition.priority() > existing.priority()) { + insertIndex = i; + break; + } } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java index 1ce71468a1b..fe507b74120 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java @@ -179,8 +179,10 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack } for (NetworkChannel networkChannel : identifiedChannels) { - List> message = network.createMessages(networkChannel, packet.getData(), MessageDirection.CLIENTBOUND); - network.handleMessages(networkChannel, message, MessageDirection.CLIENTBOUND); + Message message = network.createMessage(networkChannel, packet.getData(), MessageDirection.CLIENTBOUND); + if (message != null) { + network.handleMessages(networkChannel, List.of(message), MessageDirection.CLIENTBOUND); + } } }); } diff --git a/core/src/test/java/org/geysermc/geyser/network/MessageDispatchTest.java b/core/src/test/java/org/geysermc/geyser/network/MessageDispatchTest.java new file mode 100644 index 00000000000..1fee7ca92a3 --- /dev/null +++ b/core/src/test/java/org/geysermc/geyser/network/MessageDispatchTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2026 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network; + +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.MessageDirection; +import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.message.DataType; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.api.network.message.MessageBuffer; +import org.geysermc.geyser.api.network.message.MessageHandler; +import org.geysermc.geyser.impl.IdentifierImpl; +import org.geysermc.geyser.network.message.ByteBufCodec; +import org.geysermc.geyser.network.message.ByteBufMessageBuffer; +import org.geysermc.geyser.session.GeyserSession; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.geysermc.geyser.util.GeyserMockContext.mockContext; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class MessageDispatchTest { + private final NetworkChannel xuidChannel = new ExternalNetworkChannel(new IdentifierImpl(Key.key("geyser_test", "xuid")), XuidMessage.class); + + @Test + void handlersInvokedOncePerIncomingPayload() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + AtomicInteger decodeCount = new AtomicInteger(); + AtomicInteger handlerOneCount = new AtomicInteger(); + AtomicInteger handlerTwoCount = new AtomicInteger(); + + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, buffer -> { + decodeCount.incrementAndGet(); + return new XuidMessage(buffer); + }, registration)) + .clientbound(message -> { + handlerOneCount.incrementAndGet(); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, buffer -> { + decodeCount.incrementAndGet(); + return new XuidMessage(buffer); + }, registration)) + .clientbound(message -> { + handlerTwoCount.incrementAndGet(); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + byte[] payload = encodePayload("test-xuid"); + + Message message = network.createMessage(this.xuidChannel, payload, MessageDirection.CLIENTBOUND); + assertNotNull(message, "Channel should produce a decoded message"); + network.handleMessages(this.xuidChannel, List.of(message), MessageDirection.CLIENTBOUND); + + assertEquals(1, decodeCount.get(), "Payload should be decoded exactly once per channel, not once per definition"); + assertEquals(1, handlerOneCount.get(), "Handler one should be invoked exactly once per incoming payload"); + assertEquals(1, handlerTwoCount.get(), "Handler two should be invoked exactly once per incoming payload"); + }); + } + + @Test + void handlerMutationsFlowThroughLaterHandlers() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + NetworkChannel mutableChannel = new ExternalNetworkChannel(new IdentifierImpl(Key.key("geyser_test", "mutable_xuid")), MutableXuidMessage.class); + + new NetworkDefinitionBuilder(registration -> network.onRegister(mutableChannel, MutableXuidMessage::new, registration)) + .clientbound(message -> { + assertEquals("xuid-test", message.xuid()); + + message.xuid("xuid-test-1"); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + new NetworkDefinitionBuilder(registration -> network.onRegister(mutableChannel, MutableXuidMessage::new, registration)) + .clientbound(message -> { + assertEquals("xuid-test-1", message.xuid()); + + message.xuid("xuid-test-2"); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + ByteBufMessageBuffer encoded = ByteBufCodec.INSTANCE.createBuffer(); + new MutableXuidMessage("xuid-test").encode(encoded); + byte[] payload = encoded.serialize(); + + Message message = network.createMessage(mutableChannel, payload, MessageDirection.CLIENTBOUND); + assertNotNull(message, "Channel should produce a decoded message"); + network.handleMessages(mutableChannel, List.of(message), MessageDirection.CLIENTBOUND); + + MutableXuidMessage finalMessage = (MutableXuidMessage) message; + assertEquals("xuid-test-2", finalMessage.xuid(), "Handlers should observe modifications from earlier handlers"); + }); + } + + private static byte[] encodePayload(String xuid) { + ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE.createBuffer(); + new XuidMessage(xuid).encode(buffer); + return buffer.serialize(); + } + + public record XuidMessage(String xuid) implements Message.Simple { + + public XuidMessage(MessageBuffer buffer) { + this(buffer.read(DataType.STRING)); + } + + @Override + public void encode(@NonNull MessageBuffer buffer) { + buffer.write(DataType.STRING, this.xuid); + } + } + + public static final class MutableXuidMessage implements Message.Simple { + private String xuid; + + public MutableXuidMessage(String xuid) { + this.xuid = xuid; + } + + public MutableXuidMessage(MessageBuffer buffer) { + this(buffer.read(DataType.STRING)); + } + + public String xuid() { + return this.xuid; + } + + public void xuid(String xuid) { + this.xuid = xuid; + } + + @Override + public void encode(@NonNull MessageBuffer buffer) { + buffer.write(DataType.STRING, this.xuid); + } + } +} From 809c9eb4394c1ce3d80c2ae95325176179a1906c Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 27 Jun 2026 21:22:16 +0100 Subject: [PATCH 22/22] Better exception handling and distinguish between raw and wrapped packets in API --- .../SessionDefineNetworkChannelsEvent.java | 14 +- .../api/network/NetworkApiException.java | 75 ++++++ .../geyser/api/network/NetworkChannel.java | 14 +- .../api/network/NetworkDispatchException.java | 50 ++++ .../network/NetworkRegistrationException.java | 49 ++++ .../geyser/api/network/PacketChannel.java | 39 ++- .../geyser/api/network/RawPacketChannel.java | 79 ++++++ .../geyser/network/GeyserNetwork.java | 184 +++++++++----- .../network/NetworkDefinitionBuilder.java | 24 +- .../geyser/network/PacketChannelImpl.java | 26 +- .../geyser/network/RawPacketChannelImpl.java | 47 ++++ .../loader/ProviderRegistryLoader.java | 33 ++- .../geyser/network/NetworkExceptionTest.java | 236 ++++++++++++++++++ 13 files changed, 742 insertions(+), 128 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/NetworkApiException.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/NetworkDispatchException.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/NetworkRegistrationException.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/network/RawPacketChannel.java create mode 100644 core/src/main/java/org/geysermc/geyser/network/RawPacketChannelImpl.java create mode 100644 core/src/test/java/org/geysermc/geyser/network/NetworkExceptionTest.java diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java index a86bf05882d..f4cc9a4c341 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -208,9 +208,10 @@ interface Pipeline { /** * Places this handler before the handler with the given tag. *

- * The tag used here must be previously defined in a separate handler using {@link #tag(String)}. - * However, it should be noted that if the specified tag does not exist at the time of registration, - * the handler will be added to the end of the pipeline without throwing an error. + * The anchor must already be registered via {@link #tag(String)} when + * {@code register()} is called. If it is not, a + * {@link org.geysermc.geyser.api.network.NetworkRegistrationException} + * will be thrown identifying the missing anchor and the channel. * * @param tag the tag to place before * @return the pipeline instance @@ -220,9 +221,10 @@ interface Pipeline { /** * Places this handler after the handler with the given tag. *

- * The tag used here must be previously defined in a separate handler using {@link #tag(String)}. - * However, it should be noted that if the specified tag does not exist at the time of registration, - * the handler will be added to the end of the pipeline without throwing an error. + * The anchor must already be registered via {@link #tag(String)} when + * {@code register()} is called. If it is not, a + * {@link org.geysermc.geyser.api.network.NetworkRegistrationException} + * will be thrown identifying the missing anchor and the channel. * * @param tag the tag to place after * @return the pipeline instance diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkApiException.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkApiException.java new file mode 100644 index 00000000000..40d6950699d --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkApiException.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Base class for all Geyser networking API failures. + *

+ * Each exception carries an optional {@link #source() source} identifying who + * is responsible for the failure (typically the namespace of the extension or + * plugin that owns the channel involved). The {@linkplain #getMessage() detail + * message} is written to be readable by server administrators who are not + * developers, so they know which third party to contact. + * + * @since 2.9.2 + */ +public class NetworkApiException extends RuntimeException { + + private final @Nullable String source; + + public NetworkApiException(@Nullable String source, String message) { + super(formatMessage(source, message)); + this.source = source; + } + + public NetworkApiException(@Nullable String source, String message, Throwable cause) { + super(formatMessage(source, message), cause); + this.source = source; + } + + /** + * Returns the namespace of the party responsible for this failure. + *

+ * For extension-owned channels this is the extension id. For inbound + * custom payloads originating from a Java server plugin or mod this is + * the namespace of the payload. {@code null} indicates the responsible + * party could not be identified. + * + * @return the responsible party, or {@code null} if not known + */ + public @Nullable String source() { + return this.source; + } + + private static String formatMessage(@Nullable String source, String message) { + if (source == null || source.isBlank()) { + return "[Geyser Network API] " + message; + } + return "[Geyser Network API] " + message + " (responsible party: '" + source + "')"; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java index 23a10ee7290..94298537a6f 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -54,13 +54,17 @@ * * *

- * For packet channels, it can get slightly more complex as you need to - * know the packet ID, understand the constructed message type and have an extension - * available. The following example demonstrates this with the animate packet, - * assuming the AnimateMessage class represents the correct packet structure: + * For packet channels keyed by an existing packet class, no ID is needed: *

  * {@code
- *    private final NetworkChannel animateChannel = PacketChannel.bedrock(this, 44, AnimateMessage.class);
+ *    private final NetworkChannel animateChannel = PacketChannel.bedrock(this, AnimatePacket.class);
+ * }
+ * 
+ * For raw-buffer packet channels backed by a custom {@code Message.Packet} + * implementation, the packet ID must be supplied explicitly: + *
+ * {@code
+ *    private final NetworkChannel animateChannel = RawPacketChannel.bedrock(this, 44, AnimateMessage.class);
  * }
  * 
* diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkDispatchException.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkDispatchException.java new file mode 100644 index 00000000000..f7da571c6c6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkDispatchException.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Thrown when a message handler, encoder, or decoder fails while a network + * message is being dispatched through Geyser. + *

+ * The original failure is always preserved as the {@linkplain #getCause() + * cause} so the underlying stack trace remains intact when this exception + * surfaces through Netty's pipeline. Use {@link #source()} to identify which + * extension or plugin is at fault. + * + * @since 2.9.2 + */ +public class NetworkDispatchException extends NetworkApiException { + + public NetworkDispatchException(@Nullable String source, String message, Throwable cause) { + super(source, message, cause); + } + + public NetworkDispatchException(@Nullable String source, String message) { + super(source, message); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/NetworkRegistrationException.java b/api/src/main/java/org/geysermc/geyser/api/network/NetworkRegistrationException.java new file mode 100644 index 00000000000..b42e80ab17b --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkRegistrationException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Thrown when a {@link NetworkChannel} or message handler cannot be registered + * because the extension misused the registration builder. + *

+ * These failures happen at startup or session-define time, not during message + * dispatch. They almost always indicate a programming mistake in the extension + * that owns the channel. + * + * @since 2.9.2 + */ +public class NetworkRegistrationException extends NetworkApiException { + + public NetworkRegistrationException(@Nullable String source, String message) { + super(source, message); + } + + public NetworkRegistrationException(@Nullable String source, String message, Throwable cause) { + super(source, message, cause); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java index 88613484f13..1855b411b53 100644 --- a/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java @@ -25,9 +25,9 @@ package org.geysermc.geyser.api.network; -import org.checkerframework.checker.index.qual.NonNegative; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.network.message.Message; /** * Represents a channel for network communication associated with a packet. @@ -35,40 +35,37 @@ * This channel is used for listening to communication over * packets between the server and client and can be used to * send or receive packets. + *

+ * When the message type is the actual packet class (i.e., a Cloudburst Bedrock + * packet or an MCProtocolLib Java packet), Geyser can derive the packet ID + * from the class itself, so no ID is required here. For channels that operate + * on raw buffers via {@link Message.Packet} + * implementations, use {@link RawPacketChannel} instead, which requires the + * packet ID to be explicitly set. * * @since 2.9.2 */ public interface PacketChannel extends NetworkChannel { /** - * Gets the packet ID associated with this channel. - * - * @return the packet ID - */ - @NonNegative - int packetId(); - - /** - * Creates a new Bedrock packet {@link NetworkChannel} instance for a packet channel. + * Creates a new Bedrock {@link PacketChannel} keyed by the given packet class. * * @param extension the extension creating the channel - * @param packetId the packet ID - * @param packetType the type of the packet - * @return a new packet {@link NetworkChannel} instance for a packet channel + * @param packetType the packet class this channel handles + * @return a new Bedrock packet channel */ - static PacketChannel bedrock(Extension extension, @NonNegative int packetId, Class packetType) { - return GeyserApi.api().provider(PacketChannel.class, extension, "bedrock", packetId, packetType); + static PacketChannel bedrock(Extension extension, Class packetType) { + return GeyserApi.api().provider(PacketChannel.class, extension, "bedrock", packetType); } /** - * Creates a new Java packet {@link NetworkChannel} instance for a packet channel. + * Creates a new Java {@link PacketChannel} keyed by the given packet class. * * @param extension the extension creating the channel - * @param packetId the packet ID - * @param packetType the type of the packet - * @return a new packet {@link NetworkChannel} instance for a packet channel + * @param packetType the packet class this channel handles + * @return a new Java packet channel */ - static PacketChannel java(Extension extension, @NonNegative int packetId, Class packetType) { - return GeyserApi.api().provider(PacketChannel.class, extension, "java", packetId, packetType); + static PacketChannel java(Extension extension, Class packetType) { + return GeyserApi.api().provider(PacketChannel.class, extension, "java", packetType); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/network/RawPacketChannel.java b/api/src/main/java/org/geysermc/geyser/api/network/RawPacketChannel.java new file mode 100644 index 00000000000..c8d781a12b5 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/RawPacketChannel.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.network; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.network.message.Message; + +/** + * A {@link PacketChannel} that operates on raw packet buffers rather than + * a strongly typed packet class. + *

+ * Use this when the message type does not correspond to an existing packet + * implementation (i.e., a custom {@link Message.Packet}), in which case Geyser + * cannot infer the packet ID from the class itself, and it must be supplied + * explicitly. + * + * @since 2.9.2 + */ +public interface RawPacketChannel extends PacketChannel { + + /** + * Gets the packet ID associated with this channel. + * + * @return the packet ID + */ + @NonNegative + int packetId(); + + /** + * Creates a new raw Bedrock {@link PacketChannel} for the given packet ID + * and message type. + * + * @param extension the extension creating the channel + * @param packetId the packet ID + * @param messageType the type of the message sent over this channel + * @return a new raw Bedrock packet channel + */ + static RawPacketChannel bedrock(Extension extension, @NonNegative int packetId, Class messageType) { + return GeyserApi.api().provider(RawPacketChannel.class, extension, "bedrock", packetId, messageType); + } + + /** + * Creates a new raw Java {@link PacketChannel} for the given packet ID + * and message type. + * + * @param extension the extension creating the channel + * @param packetId the packet ID + * @param messageType the type of the message sent over this channel + * @return a new raw Java packet channel + */ + static RawPacketChannel java(Extension extension, @NonNegative int packetId, Class messageType) { + return GeyserApi.api().provider(RawPacketChannel.class, extension, "java", packetId, messageType); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java index e94e19213ad..b179296a384 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java @@ -40,9 +40,13 @@ import org.geysermc.geyser.api.network.MessageDirection; import org.geysermc.geyser.api.network.MessageFlag; import org.geysermc.geyser.api.network.Network; +import org.geysermc.geyser.api.network.NetworkApiException; import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.NetworkDispatchException; +import org.geysermc.geyser.api.network.NetworkRegistrationException; import org.geysermc.geyser.api.network.PacketChannel; import org.geysermc.geyser.api.network.PacketFlag; +import org.geysermc.geyser.api.network.RawPacketChannel; import org.geysermc.geyser.api.network.ProtocolState; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; @@ -65,7 +69,6 @@ import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -78,7 +81,7 @@ public class GeyserNetwork implements Network { private final GeyserJavaState javaState; private final Map>> definitions = new LinkedHashMap<>(); - private final Map> packetChannels = new HashMap<>(); + private final List packetChannels = new ArrayList<>(); public GeyserNetwork(GeyserSession session) { this.session = session; @@ -111,7 +114,9 @@ > void onRegister(@NonNull NetworkChannel chann private > void onRegister(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory, NetworkDefinitionBuilder.@NonNull RegistrationImpl registration) { if (channel instanceof PacketChannelImpl packetChannel && packetChannel.isJava() && registration.protocolState() == null) { - throw new IllegalArgumentException("Java packet channels must specify a protocol state"); + throw new NetworkRegistrationException(sourceOf(channel), + "Java packet channel '" + channel.identifier() + "' was registered without a protocol state. " + + "Call .protocolState(ProtocolState.GAME) (or another appropriate state) on the registration builder before .register()."); } MessageHandler handler; @@ -186,12 +191,24 @@ public void send(@NonNull NetworkChannel channel, @Non this.session.sendDownstreamPacket(javaPacket); } } else if (packetBase instanceof Message.Packet packet) { - PacketChannelImpl packetChannel = (PacketChannelImpl) channel; + if (!(channel instanceof RawPacketChannelImpl packetChannel)) { + throw new NetworkRegistrationException(sourceOf(channel), + "Message.Packet may only be sent over a RawPacketChannel, but channel '" + channel.identifier() + + "' is a " + channel.getClass().getSimpleName() + ". " + + "Use PacketChannel.bedrock(...)/java(...) with a wrapped packet, or register a RawPacketChannel instead."); + } int packetId = packetChannel.packetId(); if (packetChannel.isJava()) { ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE.createBuffer(); - packet.encode(buffer); + try { + packet.encode(buffer); + } catch (Throwable t) { + throw new NetworkDispatchException(sourceOf(channel), + "Encoding " + packet.getClass().getName() + " threw a " + t.getClass().getSimpleName() + + " on channel '" + channel.identifier() + + "'. This is a bug in the Message.Packet implementation.", t); + } MinecraftProtocol protocol = this.session.getDownstream().getSession().getPacketProtocol(); Packet javaPacket; @@ -202,7 +219,9 @@ public void send(@NonNull NetworkChannel channel, @Non } if (javaPacket == null) { - throw new IllegalArgumentException("No Java packet definition found for packet ID: " + packetId); + throw new NetworkDispatchException(sourceOf(channel), + "No Java packet definition found for packet ID " + packetId + " (channel '" + channel.identifier() + + "'). Check the packet ID supplied to RawPacketChannel.java(...) against the current Java protocol."); } if (direction == MessageDirection.CLIENTBOUND) { @@ -212,14 +231,23 @@ public void send(@NonNull NetworkChannel channel, @Non } } else { ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE_LE.createBuffer(); - packet.encode(buffer); + try { + packet.encode(buffer); + } catch (Throwable t) { + throw new NetworkDispatchException(sourceOf(channel), + "Encoding " + packet.getClass().getName() + " threw a " + t.getClass().getSimpleName() + + " on channel '" + channel.identifier() + + "'. This is a bug in the Message.Packet implementation.", t); + } BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); BedrockCodecHelper helper = this.session.getUpstream().getCodecHelper(); BedrockPacket bedrockPacket = codec.tryDecode(helper, buffer.buffer(), packetId); if (bedrockPacket == null) { - throw new IllegalArgumentException("No Bedrock packet definition found for packet ID: " + packetId); + throw new NetworkDispatchException(sourceOf(channel), + "No Bedrock packet definition found for packet ID " + packetId + " (channel '" + channel.identifier() + + "'). Check the packet ID supplied to RawPacketChannel.bedrock(...) against the active Bedrock codec."); } // Clientbound packets are sent upstream, serverbound packets are sent downstream @@ -304,27 +332,35 @@ private > M createMessage0(@NonNul return null; } - @NonNull - public List getPacketChannels(int packetId, boolean java) { - return this.packetChannels.getOrDefault(new PacketEntry(packetId, java), List.of()); - } - @SuppressWarnings("unchecked") public boolean handleBedrockPacket(BedrockPacket packet, MessageDirection direction) { if (this.packetChannels.isEmpty()) { return true; // Avoid processing anything if we have nothing to handle } - BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); - BedrockPacketDefinition definition = codec.getPacketDefinition((Class) packet.getClass()); - List channels = this.getPacketChannels(definition.getId(), false); - if (channels.isEmpty()) { - return true; - } + BedrockPacketDefinition definition = null; + for (PacketChannel channel : this.packetChannels) { + PacketChannelImpl impl = (PacketChannelImpl) channel; + if (impl.isJava()) { + continue; + } + + boolean typedMatch = impl.messageType().isInstance(packet); + if (!typedMatch) { + if (!(channel instanceof RawPacketChannel raw)) { + continue; + } + if (definition == null) { + BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); + definition = codec.getPacketDefinition((Class) packet.getClass()); + } + if (raw.packetId() != definition.getId()) { + continue; + } + } - for (PacketChannel channel : channels) { List> messages = new ArrayList<>(); - if (((PacketChannelImpl) channel).messageType().isInstance(packet)) { + if (typedMatch) { messages.add(new BedrockPacketMessage<>(packet)); } else { ByteBuf buffer = Unpooled.buffer(); @@ -357,22 +393,33 @@ public boolean handleJavaPacket(MinecraftPacket packet, MessageDirection directi return true; // Avoid processing anything if we have nothing to handle } - MinecraftProtocol protocol = this.session.getDownstream().getSession().getPacketProtocol(); - int packetId; - if (direction == MessageDirection.CLIENTBOUND) { - packetId = protocol.getInboundPacketRegistry().getClientboundId(packet.getClass()); - } else { - packetId = protocol.getOutboundPacketRegistry().getServerboundId(packet.getClass()); - } + Integer packetId = null; + for (PacketChannel channel : this.packetChannels) { + PacketChannelImpl impl = (PacketChannelImpl) channel; + if (!impl.isJava()) { + continue; + } - List channels = this.getPacketChannels(packetId, true); - if (channels.isEmpty()) { - return true; - } + boolean typedMatch = impl.messageType().isInstance(packet); + if (!typedMatch) { + if (!(channel instanceof RawPacketChannel raw)) { + continue; + } + if (packetId == null) { + MinecraftProtocol protocol = this.session.getDownstream().getSession().getPacketProtocol(); + if (direction == MessageDirection.CLIENTBOUND) { + packetId = protocol.getInboundPacketRegistry().getClientboundId(packet.getClass()); + } else { + packetId = protocol.getOutboundPacketRegistry().getServerboundId(packet.getClass()); + } + } + if (raw.packetId() != packetId) { + continue; + } + } - for (PacketChannel channel : channels) { List> messages = new ArrayList<>(); - if (((PacketChannelImpl) channel).messageType().isInstance(packet)) { + if (typedMatch) { messages.add(new JavaPacketMessage<>(packet)); } else { ByteBuf buffer = Unpooled.buffer(); @@ -459,14 +506,26 @@ public boolean handleMessages(@NonNull NetworkChannel MessageDefinition> definition = (MessageDefinition>) def; MessageHandler.State state; - if (definition.handler != null) { - state = definition.handler.handle(message, direction); - } else if (direction == MessageDirection.CLIENTBOUND && definition.clientboundHandler != null) { - state = definition.clientboundHandler.handle(message); - } else if (direction == MessageDirection.SERVERBOUND && definition.serverboundHandler != null) { - state = definition.serverboundHandler.handle(message); - } else { - continue; // no suitable handler; try next definition + try { + if (definition.handler != null) { + state = definition.handler.handle(message, direction); + } else if (direction == MessageDirection.CLIENTBOUND && definition.clientboundHandler != null) { + state = definition.clientboundHandler.handle(message); + } else if (direction == MessageDirection.SERVERBOUND && definition.serverboundHandler != null) { + state = definition.serverboundHandler.handle(message); + } else { + continue; // no suitable handler; try next definition + } + } catch (NetworkApiException rethrow) { + // Already a Network API exception with proper blame attribution; let it propagate. + throw rethrow; + } catch (Throwable t) { + throw new NetworkDispatchException(sourceOf(channel), + "Handler for channel '" + channel.identifier() + "' threw a " + + t.getClass().getSimpleName() + " while dispatching a " + + direction.name().toLowerCase(java.util.Locale.ROOT) + " " + + message.getClass().getName() + ". This is a bug in the handler; please report it to the responsible party.", + t); } if (state == MessageHandler.State.HANDLED) { @@ -528,30 +587,39 @@ private > void registerMessage(@No if (insertIndex == -1) { if (definition.beforeTag() != null || definition.afterTag() != null) { - // Relative tag was specified but anchor not found; per API docs, append to end. - insertIndex = list.size(); - } else { - // Fallback: insert by descending priority - insertIndex = list.size(); - for (int i = 0; i < list.size(); i++) { - MessageDefinition existing = list.get(i); - if (definition.priority() > existing.priority()) { - insertIndex = i; - break; - } + String relation = definition.beforeTag() != null ? "before" : "after"; + String anchor = definition.beforeTag() != null ? definition.beforeTag() : definition.afterTag(); + throw new NetworkRegistrationException(sourceOf(channel), + "Pipeline anchor tag '" + anchor + "' referenced by " + relation + "(...) was not found for channel '" + + channel.identifier() + "'. Make sure the handler that calls .tag(\"" + anchor + + "\") is registered before this one, or remove the " + relation + "(...) call."); + } + + // Fallback: insert by descending priority + insertIndex = list.size(); + for (int i = 0; i < list.size(); i++) { + MessageDefinition existing = list.get(i); + if (definition.priority() > existing.priority()) { + insertIndex = i; + break; } } } list.add(insertIndex, definition); - if (channel.isPacket() && channel instanceof PacketChannel packetChannel) { - int packetId = packetChannel.packetId(); - this.packetChannels.computeIfAbsent(new PacketEntry(packetId, ((PacketChannelImpl) packetChannel).isJava()), key -> new ArrayList<>()).add(packetChannel); + if (channel.isPacket() && channel instanceof PacketChannel packetChannel && !this.packetChannels.contains(packetChannel)) { + this.packetChannels.add(packetChannel); } } - - public record PacketEntry(int packetId, boolean java) { + + private static @org.checkerframework.checker.nullness.qual.Nullable String sourceOf(NetworkChannel channel) { + if (channel == null) { + return null; + } + + String ns = channel.identifier().namespace(); + return ns.isBlank() ? null : ns; } public record MessageDefinition>( diff --git a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java index b834a50a539..ff88455c944 100644 --- a/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java +++ b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java @@ -25,9 +25,9 @@ package org.geysermc.geyser.network; -import com.google.common.base.Preconditions; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.event.bedrock.SessionDefineNetworkChannelsEvent; +import org.geysermc.geyser.api.network.NetworkRegistrationException; import org.geysermc.geyser.api.network.ProtocolState; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageBuffer; @@ -112,8 +112,19 @@ public NetworkDefinitionBuilder(@NonNull Consumer> registrat @Override public SessionDefineNetworkChannelsEvent.@NonNull Registration register() { - Preconditions.checkState(!this.registered, "This message has already been registered"); - Preconditions.checkState(this.handler == null || (this.clientbound == null && this.serverbound == null), "Cannot register both bidirectional and sided handlers for the same message"); + if (this.registered) { + throw new NetworkRegistrationException(null, + "register() has already been called on this builder. Each define(...) call must be paired with exactly one register() call."); + } + if (this.handler != null && (this.clientbound != null || this.serverbound != null)) { + throw new NetworkRegistrationException(null, + "Cannot mix bidirectional() with clientbound()/serverbound() on the same registration. Pick one style: either a single bidirectional handler, or one or more sided handlers."); + } + if (this.handler == null && this.clientbound == null && this.serverbound == null) { + throw new NetworkRegistrationException(null, + "No handler was attached before register() was called. Add at least one of clientbound(...), serverbound(...), or bidirectional(...) so messages have somewhere to go."); + } + RegistrationImpl registration = new RegistrationImpl<>( this.protocolState, this.handler, @@ -136,8 +147,11 @@ private static final class PipelineImpl implements SessionDefineNetworkChannelsE @Override public SessionDefineNetworkChannelsEvent.Builder.@NonNull Pipeline tag(@NonNull String tag) { - Preconditions.checkNotNull(tag, "tag"); - Preconditions.checkState(!tag.isBlank(), "tag"); + Objects.requireNonNull(tag, "tag"); + if (tag.isBlank()) { + throw new NetworkRegistrationException(null, + "Pipeline tags cannot be blank. Provide a non-empty identifier so other handlers can target it with before(...) / after(...)."); + } this.tag = tag; return this; diff --git a/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java b/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java index a183ee56ab4..9ec090c6b2e 100644 --- a/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java +++ b/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java @@ -25,28 +25,17 @@ package org.geysermc.geyser.network; -import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.network.PacketChannel; import org.geysermc.geyser.api.util.Identifier; -import java.util.Objects; - public class PacketChannelImpl extends ExternalNetworkChannel implements PacketChannel { - private final int packetId; private final boolean java; - public PacketChannelImpl(@NonNull Identifier identifier, boolean java, @NonNegative int packetId, @NonNull Class packetType) { + public PacketChannelImpl(@NonNull Identifier identifier, boolean java, @NonNull Class packetType) { super(identifier, packetType); this.java = java; - this.packetId = packetId; - } - - @Override - @NonNegative - public int packetId() { - return this.packetId; } public boolean isJava() { @@ -57,17 +46,4 @@ public boolean isJava() { public boolean isPacket() { return true; } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - PacketChannelImpl that = (PacketChannelImpl) o; - return this.packetId == that.packetId; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), this.packetId); - } } diff --git a/core/src/main/java/org/geysermc/geyser/network/RawPacketChannelImpl.java b/core/src/main/java/org/geysermc/geyser/network/RawPacketChannelImpl.java new file mode 100644 index 00000000000..d129514191e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/RawPacketChannelImpl.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.RawPacketChannel; +import org.geysermc.geyser.api.util.Identifier; + +public class RawPacketChannelImpl extends PacketChannelImpl implements RawPacketChannel { + private final int packetId; + + public RawPacketChannelImpl(@NonNull Identifier identifier, boolean java, @NonNegative int packetId, @NonNull Class messageType) { + super(identifier, java, messageType); + + this.packetId = packetId; + } + + @Override + @NonNegative + public int packetId() { + return this.packetId; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index fdfa084670c..614d17c5fd7 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -61,6 +61,7 @@ import org.geysermc.geyser.api.item.custom.v2.component.java.JavaUseEffects; import org.geysermc.geyser.api.network.NetworkChannel; import org.geysermc.geyser.api.network.PacketChannel; +import org.geysermc.geyser.api.network.RawPacketChannel; import org.geysermc.geyser.api.network.message.Message; import org.geysermc.geyser.api.network.message.MessageCodec; import org.geysermc.geyser.api.pack.PathPackCodec; @@ -121,6 +122,7 @@ import org.geysermc.geyser.network.ExtensionNetworkChannel; import org.geysermc.geyser.network.ExternalNetworkChannel; import org.geysermc.geyser.network.PacketChannelImpl; +import org.geysermc.geyser.network.RawPacketChannelImpl; import org.geysermc.geyser.network.message.BedrockPacketMessage; import org.geysermc.geyser.network.message.ByteBufCodec; import org.geysermc.geyser.network.message.JavaPacketMessage; @@ -268,17 +270,14 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov }); providers.put(PacketChannel.class, args -> { - if (args.length < 4) { - throw new IllegalArgumentException("PacketChannel requires at least four arguments, got " + args.length); + if (args.length < 3) { + throw new IllegalArgumentException("PacketChannel requires at least three arguments, got " + args.length); } - if (args[0] instanceof Extension extension && args[1] instanceof String platform && args[2] instanceof Integer packetId - && args[3] instanceof Class packetType) { + if (args[0] instanceof Extension extension && args[1] instanceof String platform && args[2] instanceof Class packetType) { return switch (platform) { - case "java" -> - new PacketChannelImpl(new ExtensionIdentifierImpl(extension, "java_packet_" + packetId), true, packetId, packetType); - case "bedrock" -> - new PacketChannelImpl(new ExtensionIdentifierImpl(extension, "bedrock_packet_" + packetId), false, packetId, packetType); + case "java" -> new PacketChannelImpl(new ExtensionIdentifierImpl(extension, "java_packet_" + packetType.getName()), true, packetType); + case "bedrock" -> new PacketChannelImpl(new ExtensionIdentifierImpl(extension, "bedrock_packet_" + packetType.getName()), false, packetType); default -> throw new IllegalArgumentException("Unknown platform type for PacketChannel: " + platform); }; } @@ -287,6 +286,24 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov "Could not create a channel given the arguments: " + Arrays.toString(args)); }); + providers.put(RawPacketChannel.class, args -> { + if (args.length < 4) { + throw new IllegalArgumentException("RawPacketChannel requires at least four arguments, got " + args.length); + } + + if (args[0] instanceof Extension extension && args[1] instanceof String platform && args[2] instanceof Integer packetId + && args[3] instanceof Class messageType) { + return switch (platform) { + case "java" -> new RawPacketChannelImpl(new ExtensionIdentifierImpl(extension, "java_raw_packet_" + packetId), true, packetId, messageType); + case "bedrock" -> new RawPacketChannelImpl(new ExtensionIdentifierImpl(extension, "bedrock_raw_packet_" + packetId), false, packetId, messageType); + default -> throw new IllegalArgumentException("Unknown platform type for RawPacketChannel: " + platform); + }; + } + + throw new IllegalArgumentException("Unknown arguments provided for RawPacketChannel provider. " + + "Could not create a channel given the arguments: " + Arrays.toString(args)); + }); + return providers; } diff --git a/core/src/test/java/org/geysermc/geyser/network/NetworkExceptionTest.java b/core/src/test/java/org/geysermc/geyser/network/NetworkExceptionTest.java new file mode 100644 index 00000000000..dd8090f20c2 --- /dev/null +++ b/core/src/test/java/org/geysermc/geyser/network/NetworkExceptionTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network; + +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.MessageDirection; +import org.geysermc.geyser.api.network.NetworkApiException; +import org.geysermc.geyser.api.network.NetworkChannel; +import org.geysermc.geyser.api.network.NetworkDispatchException; +import org.geysermc.geyser.api.network.NetworkRegistrationException; +import org.geysermc.geyser.api.network.message.DataType; +import org.geysermc.geyser.api.network.message.Message; +import org.geysermc.geyser.api.network.message.MessageBuffer; +import org.geysermc.geyser.api.network.message.MessageHandler; +import org.geysermc.geyser.impl.IdentifierImpl; +import org.geysermc.geyser.session.GeyserSession; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.geysermc.geyser.util.GeyserMockContext.mockContext; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NetworkExceptionTest { + private final NetworkChannel xuidChannel = new ExternalNetworkChannel(new IdentifierImpl(Key.key("my_extension", "xuid")), XuidMessage.class); + + @Test + void registeringWithoutAnyHandlerThrows() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + NetworkRegistrationException ex = assertThrows(NetworkRegistrationException.class, () -> + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .register() + ); + + assertNull(ex.source(), "no channel was involved yet, so blame should be unattributed"); + assertTrue(ex.getMessage().contains("clientbound") && ex.getMessage().contains("serverbound") && ex.getMessage().contains("bidirectional"), "the message should tell the user what to call instead, but was: " + ex.getMessage()); + assertTrue(ex.getMessage().startsWith("[Geyser Network API]"), "messages should be tagged so log readers can grep them, but was: " + ex.getMessage()); + }); + } + + @Test + void mixingBidirectionalAndSidedThrows() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + // The public Sided/Bidirectional interfaces prevent chaining both, but a misbehaving + // implementation could still reach the underlying builder and set both fields. Validate + // the safety net through the impl directly. + NetworkDefinitionBuilder builder = new NetworkDefinitionBuilder<>(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)); + builder.bidirectional((message, direction) -> MessageHandler.State.UNHANDLED); + builder.clientbound(message -> MessageHandler.State.UNHANDLED); + + NetworkRegistrationException ex = assertThrows(NetworkRegistrationException.class, builder::register); + + assertTrue(ex.getMessage().contains("bidirectional") && ex.getMessage().contains("clientbound"), "message should name both conflicting kinds, but was: " + ex.getMessage()); + }); + } + + @Test + void doubleRegisterThrows() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + NetworkDefinitionBuilder builder = new NetworkDefinitionBuilder<>(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)); + builder.clientbound(message -> MessageHandler.State.UNHANDLED).register(); + + NetworkRegistrationException ex = assertThrows(NetworkRegistrationException.class, builder::register); + assertTrue(ex.getMessage().contains("already"), "message should make the duplicate call obvious, but was: " + ex.getMessage()); + }); + } + + @Test + void blankPipelineTagThrows() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + NetworkRegistrationException ex = assertThrows(NetworkRegistrationException.class, () -> + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(message -> MessageHandler.State.UNHANDLED) + .pipeline(pipeline -> pipeline.tag(" ")) + .register() + ); + + assertTrue(ex.getMessage().contains("blank"), "message should explain the blank tag rule, but was: " + ex.getMessage()); + }); + } + + @Test + void javaPacketChannelWithoutProtocolStateBlamesExtension() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + // Build a Java packet channel directly so we don't have to spin up the provider registry. + NetworkChannel javaPacketChannel = new RawPacketChannelImpl( + new IdentifierImpl(Key.key("my_extension", "java_raw_packet_2")), + true, + 2, + XuidMessage.class + ); + + NetworkRegistrationException ex = assertThrows(NetworkRegistrationException.class, () -> + new NetworkDefinitionBuilder(registration -> network.onRegister(javaPacketChannel, XuidMessage::new, registration)) + .clientbound(message -> MessageHandler.State.UNHANDLED) + .register() + ); + + assertEquals("my_extension", ex.source(), "the channel's namespace should be attributed"); + assertTrue(ex.getMessage().contains("protocolState"), "message should tell the user exactly which builder call to add, but was: " + ex.getMessage()); + assertTrue(ex.getMessage().contains("my_extension:java_raw_packet_2"), "message should include the channel identifier so logs are actionable, but was: " + ex.getMessage()); + assertTrue(ex.getMessage().contains("responsible party: 'my_extension'"), "the blame suffix should name the responsible party, but was: " + ex.getMessage()); + }); + } + + @Test + void missingPipelineAnchorThrowsAndNamesTheAnchor() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + NetworkRegistrationException ex = assertThrows(NetworkRegistrationException.class, () -> + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(message -> MessageHandler.State.UNHANDLED) + .pipeline(pipeline -> pipeline.before("does-not-exist")) + .register() + ); + + assertEquals("my_extension", ex.source()); + assertTrue(ex.getMessage().contains("does-not-exist"), "message should name the missing anchor tag, but was: " + ex.getMessage()); + assertTrue(ex.getMessage().contains("before"), "message should name whether before or after was used, but was: " + ex.getMessage()); + }); + } + + @Test + void handlerExceptionDuringDispatchWrapsWithBlameAndCause() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + RuntimeException handlerFailure = new RuntimeException("boom from extension code"); + + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(message -> { + throw handlerFailure; + }) + .register(); + + NetworkDispatchException ex = assertThrows(NetworkDispatchException.class, () -> + network.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND)); + + assertSame(handlerFailure, ex.getCause(), "original cause must be preserved for stack-trace forensics"); + assertEquals("my_extension", ex.source(), "blame should be the channel's namespace"); + assertNotNull(ex.getMessage()); + assertTrue(ex.getMessage().contains("RuntimeException"), "message should call out the thrown exception type, but was: " + ex.getMessage()); + assertTrue(ex.getMessage().contains("my_extension:xuid"), "message should include the channel identifier so admins know what to look at, but was: " + ex.getMessage()); + assertTrue(ex.getMessage().contains("clientbound"), "message should name the direction the dispatch was going, but was: " + ex.getMessage()); + }); + } + + @Test + void networkApiExceptionFromHandlerIsNotRewrapped() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + NetworkDispatchException originallyThrown = new NetworkDispatchException("downstream_extension", "pre-wrapped failure"); + + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(message -> { + throw originallyThrown; + }) + .register(); + + NetworkApiException ex = assertThrows(NetworkApiException.class, () -> + network.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND)); + + assertSame(originallyThrown, ex, "already-attributed Network API exceptions should propagate unchanged"); + assertEquals("downstream_extension", ex.source()); + }); + } + + @Test + void unattributedExceptionsOmitBlameSuffix() { + NetworkRegistrationException ex = new NetworkRegistrationException(null, "anonymous failure"); + assertTrue(ex.getMessage().startsWith("[Geyser Network API] anonymous failure")); + assertFalse(ex.getMessage().contains("responsible party"), "no source means no blame suffix"); + } + + public record XuidMessage(String xuid) implements Message.Simple { + + public XuidMessage(MessageBuffer buffer) { + this(buffer.read(DataType.STRING)); + } + + @Override + public void encode(@NonNull MessageBuffer buffer) { + buffer.write(DataType.STRING, this.xuid); + } + } +}