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 389b721a436..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 @@ -34,6 +34,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.Network; import org.geysermc.geyser.api.skin.SkinData; import org.jspecify.annotations.Nullable; @@ -121,6 +122,15 @@ public interface GeyserConnection extends Connection, CommandSource { */ void sendCommand(String command); + /** + * Gets the {@link Network} used for handling + * network channels and sending messages. + * + * @return the network + * @since 2.9.2 + */ + Network network(); + /** * Gets the hostname or ip address the player used to join this Geyser instance. * Example: @@ -131,7 +141,7 @@ public interface GeyserConnection extends Connection, CommandSource { * * * @throws NoSuchElementException if called before the session is fully initialized - * @return the ip address or hostname string the player used to join + * @return the ip address or hostname string the player used to join * @since 2.8.3 */ String joinAddress(); @@ -145,7 +155,7 @@ public interface GeyserConnection extends Connection, CommandSource { * * * @throws NoSuchElementException if called before the session is fully initialized - * @return the port the player used to join + * @return the port the player used to join * @since 2.8.3 */ @Positive 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..f4cc9a4c341 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java @@ -0,0 +1,281 @@ +/* + * 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.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; +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; +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. + *

+ * 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 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. + * + * @since 2.9.2 + */ +public abstract class SessionDefineNetworkChannelsEvent extends ConnectionEvent { + private final State state; + + public SessionDefineNetworkChannelsEvent(GeyserConnection connection, State state) { + super(connection); + + this.state = state; + } + + /** + * Gets the state of the connection at the time of channel registration. + * + * @return the registration state + */ + public State state() { + return this.state; + } + + /** + * Defines the registration of a new network channel with a message factory. + * + * @param channel the channel to register + * @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 + */ + public abstract > Builder.Initial define(NetworkChannel channel, MessageFactory messageFactory); + + /** + * 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 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 + */ + public abstract > Builder.Initial define(NetworkChannel channel, MessageCodec codec, 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 + */ + @This Builder pipeline(Consumer pipeline); + + /** + * Finalizes the registration. + * + * @return the completed registration + */ + Registration register(); + + interface Initial> extends Sided, Bidirectional { + + /** + * 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 + */ + @This Initial protocolState(ProtocolState state); + + /** + * {@inheritDoc} + */ + @Override + @This Initial pipeline(Consumer pipeline); + } + + interface Sided> extends Builder { + + /** + * Registers a clientbound handler. + */ + @This Sided clientbound(MessageHandler.Sided handler); + + /** + * Registers a clientbound handler with a priority. + */ + @This Sided clientbound(MessagePriority priority, MessageHandler.Sided handler); + + /** + * Registers a serverbound handler. + */ + @This Sided serverbound(MessageHandler.Sided handler); + + /** + * Registers a serverbound handler with a priority. + */ + @This Sided serverbound(MessagePriority priority, MessageHandler.Sided handler); + + /** + * {@inheritDoc} + */ + @Override + @This Sided pipeline(Consumer pipeline); + } + + interface Bidirectional> extends Builder { + + /** + * Registers a bidirectional handler which receives the message and passes + * through the direction. + */ + @This Bidirectional bidirectional(MessageHandler handler); + + /** + * Registers a bidirectional handler with a priority. + */ + @This Bidirectional bidirectional(MessagePriority priority, MessageHandler handler); + + /** + * {@inheritDoc} + */ + @Override + @This Bidirectional pipeline(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 + */ + @This Pipeline tag(String tag); + + /** + * Places this handler before the handler with the given tag. + *

+ * 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 + */ + @This Pipeline before(String tag); + + /** + * Places this handler after the handler with the given tag. + *

+ * 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 + */ + @This Pipeline after(String tag); + } + } + + 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 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. This + * is the earliest point 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/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..57b6e1824a7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/JavaState.java @@ -0,0 +1,48 @@ +/* + * 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 state of a Java connection. + * + * @since 2.9.2 + */ +public interface JavaState { + + /** + * Gets the inbound protocol state. + * + * @return the inbound protocol state + */ + ProtocolState inbound(); + + /** + * Gets the outbound protocol state. + * + * @return the outbound protocol state + */ + ProtocolState outbound(); +} 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..7ec85949a38 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/MessageDirection.java @@ -0,0 +1,48 @@ +/* + * 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.9.2 + */ +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/MessageFlag.java b/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java new file mode 100644 index 00000000000..7e49fa55671 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/MessageFlag.java @@ -0,0 +1,34 @@ +/* + * 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. + * + * @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 new file mode 100644 index 00000000000..35233e163ca --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/Network.java @@ -0,0 +1,65 @@ +/* + * 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.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 handler responsible for handling network operations + * for a {@link GeyserConnection}. + * + * @since 2.9.2 + */ +public interface Network { + + /** + * Gets the Java protocol state of the connection. + * + * @return the Java protocol state + */ + JavaState javaState(); + + /** + * Gets the registered network channels. + * + * @return the registered network channels + */ + Set registeredChannels(); + + /** + * 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) + * @param flags the message flags to use when sending the message + */ + void send(NetworkChannel channel, Message message, MessageDirection direction, MessageFlag... flags); +} 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 new file mode 100644 index 00000000000..94298537a6f --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java @@ -0,0 +1,138 @@ +/* + * 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.geysermc.geyser.api.GeyserApi; +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", MyMessage.class);
+ * }
+ * 
+ * Or when inside an extension, with 'this' being the extension instance: + *
+ * {@code
+ *     private final NetworkChannel myChannel = NetworkChannel.of(this, "my_channel", MyMessage.class);
+ * }
+ * 
+ * + *

+ * For packet channels keyed by an existing packet class, no ID is needed: + *

+ * {@code
+ *    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);
+ * }
+ * 
+ * + *

+ * Packet channels can also be registered against packet objects from + * 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.9.2 + */ +public interface NetworkChannel { + + /** + * Gets the identifier that owns this channel. + * + * @return the identifier that owns this channel + */ + Identifier identifier(); + + /** + * Checks if this channel is a packet channel. + * + * @return true if this channel is a packet channel, false otherwise + */ + boolean isPacket(); + + /** + * 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 + * @param messageType the type of the message sent over this channel + * @return a new external {@link NetworkChannel} instance + */ + static NetworkChannel of(Extension extension, String channel, Class messageType) { + return GeyserApi.api().provider(NetworkChannel.class, extension, channel, messageType); + } + + /** + * 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 + * @param messageType the type of the message sent over this channel + * @return a new external {@link NetworkChannel} instance + */ + static NetworkChannel of(String id, String channel, Class messageType) { + return of(Identifier.of(id, channel), messageType); + } + + /** + * 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 + * @param messageType the type of the message sent over this channel + * @return a new external {@link NetworkChannel} instance + */ + 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/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 new file mode 100644 index 00000000000..1855b411b53 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.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; + +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. + *

+ * 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 { + + /** + * Creates a new Bedrock {@link PacketChannel} keyed by the given packet class. + * + * @param extension the extension creating the channel + * @param packetType the packet class this channel handles + * @return a new Bedrock packet channel + */ + static PacketChannel bedrock(Extension extension, Class packetType) { + return GeyserApi.api().provider(PacketChannel.class, extension, "bedrock", packetType); + } + + /** + * Creates a new Java {@link PacketChannel} keyed by the given packet class. + * + * @param extension the extension creating the channel + * @param packetType the packet class this channel handles + * @return a new Java packet channel + */ + 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/PacketFlag.java b/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java new file mode 100644 index 00000000000..f0d439c26e3 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/PacketFlag.java @@ -0,0 +1,39 @@ +/* + * 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. + * + * @since 2.9.2 + */ +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..f53937b7d2a --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/ProtocolState.java @@ -0,0 +1,54 @@ +/* + * 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. + * + * @since 2.9.2 + */ +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/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/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..89d51514965 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/DataType.java @@ -0,0 +1,230 @@ +/* + * 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 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. + * + * @param the type + * @since 2.9.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); + /** + * 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; + + private DataType(Reader reader, 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 + */ + public > T read(C codec, 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(C codec, B buffer, 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 + */ + public static DataType of(Reader reader, 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 + */ + public static DataType> optional(DataType type) { + return new DataType<>(new Reader<>() { + + @Override + public > Optional read(C codec, B buffer) { + if (codec.readBoolean(buffer)) { + return Optional.of(type.read(codec, buffer)); + } else { + return Optional.empty(); + } + } + }, new Writer<>() { + + @Override + public > void write(C codec, B buffer, 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 + */ + public static DataType> list(DataType type) { + return new DataType<>(new Reader<>() { + + @Override + public > List read(C codec, 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(C codec, B buffer, 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 + */ + > T read(C codec, 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(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 new file mode 100644 index 00000000000..d60277673e5 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/Message.java @@ -0,0 +1,113 @@ +/* + * 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.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.9.2 + */ +public interface Message { + + /** + * Encodes the message to the given buffer. + * + * @param buffer the buffer to write to + */ + void encode(T buffer); + + /** + * Represents a simple message using the built-in {@link MessageBuffer} implementation. + */ + interface Simple extends Message { + } + + /** + * Represents a packet message with an unknown packet ID. + * + * @param the type of message buffer + */ + 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 + */ + @SuppressWarnings("unchecked") + static PacketWrapped of(Object packet) { + return (PacketWrapped) GeyserApi.api().provider(PacketWrapped.class, packet); + } + + /** + * 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 + */ + static MessageFactory> of(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 + */ + static MessageFactory> of(Function substitutor, Function packetSupplier) { + return buffer -> of(packetSupplier.apply(substitutor.apply(buffer))); + } + } + + /** + * Represents a packet message that wraps an underlying packet object. + * + * @param the type of message buffer + */ + interface PacketWrapped extends PacketBase { + + /** + * Gets the packet associated with this message. + * + * @return the 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 new file mode 100644 index 00000000000..d4138a6ce63 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageBuffer.java @@ -0,0 +1,83 @@ +/* + * 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; + +/** + * A buffer for messages that can be sent over the network. + * + * @since 2.9.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 + */ + T read(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(DataType type, T value); + + /** + * Serializes the buffer to a byte array. + * + * @return the serialized byte array + */ + byte[] serialize(); + + /** + * Gets the length of the buffer. + * + * @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 + */ + 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 new file mode 100644 index 00000000000..61f2d4022e6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageCodec.java @@ -0,0 +1,295 @@ +/* + * 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.geysermc.geyser.api.GeyserApi; + +import java.util.UUID; + +/** + * A codec for encoding and decoding messages. + * + * @param the type of {@link MessageBuffer} used for encoding and decoding + * @since 2.9.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(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(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(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(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(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(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(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(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(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(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(T buffer); + + /** + * Reads a string from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the string read + */ + String readString(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 + */ + String readString(T buffer, int maxLength); + + /** + * Reads a UUID from the {@link T buffer}. + * + * @param buffer the buffer to read from + * @return the UUID read + */ + UUID readUuid(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(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(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(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(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(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(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(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(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(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(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(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(T buffer, 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(T buffer, UUID uuid); + + /** + * Creates a new {@link T buffer} instance. + * + * @return a new instance of {@link MessageBuffer} + */ + 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 + */ + T createBuffer(byte[] 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 + */ + static >> C provided(Class type, EncoderOptions... options) { + return GeyserApi.api().provider(MessageCodec.class, type, options); + } + + /** + * 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 new file mode 100644 index 00000000000..7898c1677a2 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.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.api.network.message; + +/** + * A factory interface for creating messages from a given message buffer. + * + * @param the type of the message buffer + * @param the type of the message + * @since 2.9.2 + */ +@FunctionalInterface +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 + */ + 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 new file mode 100644 index 00000000000..b7caf6f1938 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessageHandler.java @@ -0,0 +1,82 @@ +/* + * 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.geysermc.geyser.api.network.MessageDirection; + +/** + * Represents a handler for processing messages. + * + * @param the type of message to handle + * @since 2.9.2 + */ +@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 + */ + State handle(T message, 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 + */ + State handle(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..6b55ebb3f54 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java @@ -0,0 +1,72 @@ +/* + * 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.common.value.qual.IntRange; + +/** + * Represents the priority of a message when being processed. + * + * @since 2.9.2 + */ +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; + } + + /** + * 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 + */ + 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; + if (value <= -25) return LATE; + return NORMAL; + } +} 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; 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/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/core/src/main/java/org/geysermc/geyser/network/ExtensionNetworkChannel.java b/core/src/main/java/org/geysermc/geyser/network/ExtensionNetworkChannel.java new file mode 100644 index 00000000000..5afe0d4b1b3 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/ExtensionNetworkChannel.java @@ -0,0 +1,85 @@ +/* + * 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.extension.Extension; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.Objects; + +/** + * Represents a network channel associated with an extension. + */ +public class ExtensionNetworkChannel extends BaseNetworkChannel { + private final Extension extension; + private final String channel; + + public ExtensionNetworkChannel(@NonNull Extension extension, @NonNull String channel, @NonNull Class messageType) { + super(messageType); + + this.extension = extension; + this.channel = channel; + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public Identifier identifier() { + return Identifier.of(this.extension.description().id(), this.channel); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPacket() { + return false; + } + + @Override + public boolean equals(Object o) { + 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(), this.messageType()); + } + + @Override + public String toString() { + return "ExtensionNetworkChannel{" + + "extension=" + this.extension.description().id() + + ", channel='" + this.channel + '\'' + + ", messageType=" + this.messageType() + + '}'; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/ExternalNetworkChannel.java b/core/src/main/java/org/geysermc/geyser/network/ExternalNetworkChannel.java new file mode 100644 index 00000000000..bce86b10896 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/ExternalNetworkChannel.java @@ -0,0 +1,83 @@ +/* + * 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.util.Identifier; + +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. + */ +public class ExternalNetworkChannel extends BaseNetworkChannel { + private final Identifier identifier; + + public ExternalNetworkChannel(@NonNull Identifier identifier, @NonNull Class messageType) { + super(messageType); + + this.identifier = identifier; + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public Identifier identifier() { + return this.identifier; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPacket() { + return false; + } + + @Override + public boolean equals(Object o) { + 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(), 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/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/GeyserNetwork.java b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java new file mode 100644 index 00000000000..b179296a384 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/GeyserNetwork.java @@ -0,0 +1,662 @@ +/* + * 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 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.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.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; +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.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.NotNull; +import org.jetbrains.annotations.VisibleForTesting; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +public class GeyserNetwork implements Network { + private final GeyserSession session; + private final GeyserJavaState javaState; + + private final Map>> definitions = new LinkedHashMap<>(); + private final List packetChannels = new ArrayList<>(); + + public GeyserNetwork(GeyserSession session) { + this.session = session; + this.javaState = new GeyserJavaState(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) { + return new NetworkDefinitionBuilder<>(registration -> onRegister(channel, ByteBufCodec.INSTANCE, messageFactory, registration)); + } + + @Override + public > Builder.@NonNull Initial define(@NonNull NetworkChannel channel, @NonNull MessageCodec codec, @NonNull MessageFactory messageFactory) { + return new NetworkDefinitionBuilder<>(registration -> onRegister(channel, codec, messageFactory, registration)); + } + }; + + 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) { + if (channel instanceof PacketChannelImpl packetChannel && packetChannel.isJava() && registration.protocolState() == null) { + 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; + 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.protocolState(), + registration.tag(), + registration.beforeTag(), + registration.afterTag() + ); + + this.registerMessage(channel, definition); + } + + @Override + public @NonNull JavaState javaState() { + return this.javaState; + } + + @Override + @Nonnull + public Set registeredChannels() { + return Set.copyOf(this.definitions.keySet()); + } + + @Override + public void send(@NonNull NetworkChannel channel, @NonNull Message message, @NonNull MessageDirection direction, @NonNull MessageFlag... 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) { + 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) { + 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) { + 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(); + 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; + if (direction == MessageDirection.CLIENTBOUND) { + javaPacket = protocol.getInboundPacketRegistry().createClientboundPacket(packetId, buffer.buffer()); + } else { + javaPacket = protocol.getOutboundPacketRegistry().createServerboundPacket(packetId, buffer.buffer()); + } + + if (javaPacket == null) { + 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) { + Registries.JAVA_PACKET_TRANSLATORS.translate(javaPacket.getClass(), javaPacket, this.session, true); + } else { + this.session.sendDownstreamPacket(javaPacket); + } + } else { + ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE_LE.createBuffer(); + 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 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 + 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); + } + } + } + + 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); + + Key channelKey = Key.key(channel.identifier().toString()); + byte[] data = buffer.serialize(); + + ServerboundCustomPayloadPacket packet = new ServerboundCustomPayloadPacket(channelKey, data); + this.session.sendDownstreamPacket(packet); + } + + @Nullable + public Message createMessage(@NonNull NetworkChannel channel, byte @NonNull[] data, @NotNull MessageDirection direction) { + return this.createMessage0(channel, definition -> definition.createBuffer(data), direction); + } + + @Nullable + public Message createMessage(@NonNull NetworkChannel channel, @NonNull T buffer, @NotNull MessageDirection direction) { + return this.createMessage0(channel, def -> buffer, direction); + } + + @SuppressWarnings("unchecked") + @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); + } + + for (MessageDefinition def : definitions) { + 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; + } + + 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); + if (message instanceof BedrockPacketMessage packetMessage) { + packetMessage.postProcess(this.session, (ByteBufMessageBuffer) buffer); + } + + return message; + } + + return null; + } + + @SuppressWarnings("unchecked") + public boolean handleBedrockPacket(BedrockPacket packet, MessageDirection direction) { + if (this.packetChannels.isEmpty()) { + return true; // Avoid processing anything if we have nothing to handle + } + + 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; + } + } + + List> messages = new ArrayList<>(); + if (typedMatch) { + messages.add(new BedrockPacketMessage<>(packet)); + } else { + ByteBuf buffer = Unpooled.buffer(); + 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(); + } + } + + boolean handled = this.handleMessages(channel, messages, direction); + if (!handled) { + return false; + } + } + + return true; + } + + public boolean handleJavaPacket(MinecraftPacket packet, MessageDirection direction) { + if (this.packetChannels.isEmpty()) { + return true; // Avoid processing anything if we have nothing to handle + } + + Integer packetId = 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 (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; + } + } + + List> messages = new ArrayList<>(); + if (typedMatch) { + messages.add(new JavaPacketMessage<>(packet)); + } else { + ByteBuf buffer = Unpooled.buffer(); + 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(); + } + } + + 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); + 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) { + // 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 + 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)) { + throw new IllegalArgumentException("Unsupported NetworkChannel implementation: " + channel.getClass().getName() + "; expected BaseNetworkChannel"); + } + + if (message instanceof Message.PacketWrapped wrapped && !base.messageType().isInstance(wrapped.packet())) { + continue; + } else if (!(message instanceof Message.PacketWrapped) && !base.messageType().isInstance(message)) { + continue; + } + + MessageDefinition> definition = (MessageDefinition>) def; + + MessageHandler.State state; + 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) { + 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); + } + + MessageDefinition> definition = null; + for (MessageDefinition def : definitions) { + if (channel instanceof BaseNetworkChannel baseChannel) { + if (baseChannel.messageType().isInstance(message)) { + definition = (MessageDefinition>) def; + break; + } + } + } + + 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 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) { + if (definition.beforeTag() != null || definition.afterTag() != null) { + 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 && !this.packetChannels.contains(packetChannel)) { + this.packetChannels.add(packetChannel); + } + } + + 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>( + @NonNull MessageCodec codec, + @NonNull MessageFactory messageFactory, + @Nullable MessageHandler handler, + MessageHandler.Sided clientboundHandler, + MessageHandler.Sided serverboundHandler, + int priority, + @Nullable Integer clientboundPriority, + @Nullable Integer serverboundPriority, + @Nullable ProtocolState state, + @Nullable String tag, + @Nullable String beforeTag, + @Nullable String afterTag + ) { + + @NonNull + public T createBuffer(byte @NonNull[] data) { + return this.codec.createBuffer(data); + } + + @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/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/network/NetworkDefinitionBuilder.java b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java new file mode 100644 index 00000000000..ff88455c944 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java @@ -0,0 +1,195 @@ +/* + * 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.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; +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 ProtocolState protocolState; + 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; + } + + @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"); + 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() { + 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, + 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) { + 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; + } + + @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>( + ProtocolState protocolState, + 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/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java b/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.java new file mode 100644 index 00000000000..9ec090c6b2e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/PacketChannelImpl.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.network; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.network.PacketChannel; +import org.geysermc.geyser.api.util.Identifier; + +public class PacketChannelImpl extends ExternalNetworkChannel implements PacketChannel { + private final boolean java; + + public PacketChannelImpl(@NonNull Identifier identifier, boolean java, @NonNull Class packetType) { + super(identifier, packetType); + + this.java = java; + } + + public boolean isJava() { + return this.java; + } + + @Override + public boolean isPacket() { + return true; + } +} 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/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 35cc66f3ee8..7771d86beab 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -54,8 +54,10 @@ 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; import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePackManifest; @@ -108,7 +110,11 @@ public UpstreamPacketHandler(GeyserImpl geyser, GeyserSession session) { } private PacketSignal translateAndDefault(BedrockPacket packet) { - Registries.BEDROCK_PACKET_TRANSLATORS.translate(packet.getClass(), packet, session, false); + if (!this.session.getNetwork().handleBedrockPacket(packet, MessageDirection.SERVERBOUND)) { + return PacketSignal.HANDLED; + } + + Registries.BEDROCK_PACKET_TRANSLATORS.translate(packet.getClass(), packet, this.session, false); return PacketSignal.HANDLED; // PacketSignal.UNHANDLED will log a WARN publicly } @@ -221,6 +227,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/network/message/BedrockPacketMessage.java b/core/src/main/java/org/geysermc/geyser/network/message/BedrockPacketMessage.java new file mode 100644 index 00000000000..744231f316a --- /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 T 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 MessageBuffer."); + } +} 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..b00d9166f6c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodec.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.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; +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(); + public static final ByteBufCodecLE INSTANCE_LE = new ByteBufCodecLE(); + + private ByteBufCodec() { + } + + @Override + public boolean readBoolean(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readBoolean(); + } + + @Override + public byte readByte(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readByte(); + } + + @Override + public short readShort(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readShort(); + } + + @Override + public int readInt(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readInt(); + } + + @Override + public float readFloat(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readFloat(); + } + + @Override + public double readDouble(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readDouble(); + } + + @Override + public long readLong(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readLong(); + } + + @Override + public int readVarInt(@NonNull ByteBufMessageBuffer buffer) { + return VarInts.readInt(buffer.buffer()); + } + + @Override + public int readUnsignedVarInt(@NonNull ByteBufMessageBuffer buffer) { + return VarInts.readUnsignedInt(buffer.buffer()); + } + + @Override + public long readVarLong(@NonNull ByteBufMessageBuffer buffer) { + return VarInts.readLong(buffer.buffer()); + } + + @Override + public long readUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer) { + return VarInts.readUnsignedLong(buffer.buffer()); + } + + @Override + public @NonNull String readString(@NonNull 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 @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 @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); + } + + @Override + public void writeByte(@NonNull ByteBufMessageBuffer buffer, byte value) { + buffer.buffer().writeByte(value); + } + + @Override + public void writeShort(@NonNull ByteBufMessageBuffer buffer, short value) { + buffer.buffer().writeShort(value); + } + + @Override + public void writeInt(@NonNull ByteBufMessageBuffer buffer, int value) { + buffer.buffer().writeInt(value); + } + + @Override + public void writeFloat(@NonNull ByteBufMessageBuffer buffer, float value) { + buffer.buffer().writeFloat(value); + } + + @Override + public void writeDouble(@NonNull ByteBufMessageBuffer buffer, double value) { + buffer.buffer().writeDouble(value); + } + + @Override + public void writeLong(@NonNull ByteBufMessageBuffer buffer, long value) { + buffer.buffer().writeLong(value); + } + + @Override + public void writeVarInt(@NonNull ByteBufMessageBuffer buffer, int value) { + VarInts.writeInt(buffer.buffer(), value); + } + + @Override + public void writeUnsignedVarInt(@NonNull ByteBufMessageBuffer buffer, int value) { + VarInts.writeUnsignedInt(buffer.buffer(), value); + } + + @Override + public void writeVarLong(@NonNull ByteBufMessageBuffer buffer, long value) { + VarInts.writeLong(buffer.buffer(), value); + } + + @Override + public void writeUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer, long value) { + VarInts.writeUnsignedLong(buffer.buffer(), value); + } + + @Override + 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 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); + } + + @Override + 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 new file mode 100644 index 00000000000..6468fe27803 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java @@ -0,0 +1,206 @@ +/* + * 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.ByteBufUtil; +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 java.nio.charset.StandardCharsets; +import java.util.UUID; + +public class ByteBufCodecLE implements MessageCodec { + + ByteBufCodecLE() { + } + + @Override + public boolean readBoolean(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readBoolean(); + } + + @Override + public byte readByte(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readByte(); + } + + @Override + public short readShort(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readShortLE(); + } + + @Override + public int readInt(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readIntLE(); + } + + @Override + public float readFloat(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readFloatLE(); + } + + @Override + public double readDouble(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readDoubleLE(); + } + + @Override + public long readLong(@NonNull ByteBufMessageBuffer buffer) { + return buffer.buffer().readLongLE(); + } + + @Override + public int readVarInt(@NonNull ByteBufMessageBuffer buffer) { + return VarInts.readInt(buffer.buffer()); + } + + @Override + public int readUnsignedVarInt(@NonNull ByteBufMessageBuffer buffer) { + return VarInts.readUnsignedInt(buffer.buffer()); + } + + @Override + public long readVarLong(@NonNull ByteBufMessageBuffer buffer) { + return VarInts.readLong(buffer.buffer()); + } + + @Override + public long readUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer) { + return VarInts.readUnsignedLong(buffer.buffer()); + } + + @Override + public @NonNull String readString(@NonNull 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 @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 @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); + } + + @Override + public void writeByte(@NonNull ByteBufMessageBuffer buffer, byte value) { + buffer.buffer().writeByte(value); + } + + @Override + public void writeShort(@NonNull ByteBufMessageBuffer buffer, short value) { + buffer.buffer().writeShortLE(value); + } + + @Override + public void writeInt(@NonNull ByteBufMessageBuffer buffer, int value) { + buffer.buffer().writeIntLE(value); + } + + @Override + public void writeFloat(@NonNull ByteBufMessageBuffer buffer, float value) { + buffer.buffer().writeFloatLE(value); + } + + @Override + public void writeDouble(@NonNull ByteBufMessageBuffer buffer, double value) { + buffer.buffer().writeDoubleLE(value); + } + + @Override + public void writeLong(@NonNull ByteBufMessageBuffer buffer, long value) { + buffer.buffer().writeLongLE(value); + } + + @Override + public void writeVarInt(@NonNull ByteBufMessageBuffer buffer, int value) { + VarInts.writeInt(buffer.buffer(), value); + } + + @Override + public void writeUnsignedVarInt(@NonNull ByteBufMessageBuffer buffer, int value) { + VarInts.writeUnsignedInt(buffer.buffer(), value); + } + + @Override + public void writeVarLong(@NonNull ByteBufMessageBuffer buffer, long value) { + VarInts.writeLong(buffer.buffer(), value); + } + + @Override + public void writeUnsignedVarLong(@NonNull ByteBufMessageBuffer buffer, long value) { + VarInts.writeUnsignedLong(buffer.buffer(), value); + } + + @Override + 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 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); + } + + @Override + 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/ByteBufMessageBuffer.java b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java new file mode 100644 index 00000000000..e436c3384d4 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.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.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.DataType; +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.Wrapped { + + 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; + } + + @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 new file mode 100644 index 00000000000..51ff19420e2 --- /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(T packet) implements Message.PacketWrapped { + + @Override + public void encode(@NonNull ByteBufMessageBuffer buffer) { + throw new UnsupportedOperationException("JavaPacketMessage does not support encoding directly to a MessageBuffer."); + } +} 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 4e4f3983db8..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 @@ -25,6 +25,8 @@ 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; import org.geysermc.geyser.api.block.custom.CustomBlockData; @@ -57,6 +59,11 @@ import org.geysermc.geyser.api.item.custom.v2.component.java.JavaTool; import org.geysermc.geyser.api.item.custom.v2.component.java.JavaUseCooldown; 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; import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.api.pack.option.PriorityOption; @@ -73,6 +80,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.GeyserDimensionPredicate; import org.geysermc.geyser.impl.HoldersImpl; import org.geysermc.geyser.impl.IdentifierImpl; @@ -111,16 +119,26 @@ 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.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; 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.Arrays; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.function.Predicate; /** @@ -200,6 +218,92 @@ 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()); + } + }); + + 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) { + return new ExtensionNetworkChannel(extension, channel, messageType); + } 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)); + } + }); + + providers.put(PacketChannel.class, args -> { + 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 Class packetType) { + return switch (platform) { + 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); + }; + } + + throw new IllegalArgumentException("Unknown arguments provided for PacketChannel provider. " + + "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/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 1b0d1c25886..62a4eb63297 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -27,6 +27,7 @@ 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; @@ -119,8 +120,11 @@ 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; +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; @@ -159,6 +163,7 @@ import org.geysermc.geyser.level.gamerule.GameRuleHandler; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.network.GameProtocol; +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; @@ -204,12 +209,12 @@ import org.geysermc.geyser.util.MathUtils; 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; @@ -830,9 +835,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private final Set inputLocksSet = EnumSet.noneOf(InputLocksFlag.class); private boolean inputLockDirty; + private final GeyserNetwork network; + 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); @@ -874,6 +881,7 @@ public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSessio this.loggedIn = false; this.remoteServer = geyser.defaultRemoteServer(); + this.network = new GeyserNetwork(this); } /** @@ -1151,6 +1159,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 :) @@ -1159,18 +1169,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 { @@ -2613,6 +2650,12 @@ public boolean hasFormOpen() { return formCache.hasFormOpen(); } + @Override + @NonNull + public Network network() { + return this.network; + } + @Override public void closeForm() { formCache.closeForms(); 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 4bb8de585fa..ba5bcaa886d 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.getNetwork().handleBedrockPacket(packet, MessageDirection.CLIENTBOUND)) { + return; + } + if (!isClosed()) { session.sendPacket(packet); } } public void sendPacketImmediately(@NonNull BedrockPacket packet) { + if (!this.geyserSession.getNetwork().handleBedrockPacket(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 0e74f35f572..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 @@ -41,6 +41,11 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.api.network.AuthType; +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.GeyserNetwork; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -48,6 +53,11 @@ import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; 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) public class JavaCustomPayloadTranslator extends PacketTranslator { @@ -146,6 +156,35 @@ public void translate(GeyserSession session, ClientboundCustomPayloadPacket pack session.sendUpstreamPacket(toSend); }); + default -> session.ensureInEventLoop(() -> { + GeyserNetwork network = session.getNetwork(); + Set channels = network.registeredChannels(); + if (channels.isEmpty()) { + this.logger.debug("Received a custom payload for an unregistered channel: " + channel); + return; + } + + Map> channelMap = new HashMap<>(); + for (NetworkChannel registeredChannel : channels) { + if (!registeredChannel.isPacket()) { + String identifier = registeredChannel.identifier().toString(); + channelMap.computeIfAbsent(identifier, k -> new ArrayList<>()).add(registeredChannel); + } + } + + List identifiedChannels = channelMap.get(channel); + if (identifiedChannels == null || identifiedChannels.isEmpty()) { + this.logger.debug("Received a custom payload for an unregistered channel: " + channel); + return; + } + + for (NetworkChannel networkChannel : identifiedChannels) { + 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/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java index 5c89433306a..d9c536363b8 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) { 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..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 @@ -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,20 @@ public void translate(GeyserSession session, ClientboundLoginPacket packet) { } session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8))); + Set registeredChannels = session.getNetwork().registeredChannels(); + if (!registeredChannels.isEmpty()) { + String channels = registeredChannels + .stream() + .filter(channel -> !channel.isPacket()) + .map(channel -> channel.identifier().namespace() + ":" + channel.identifier().path()) + .distinct() + .collect(Collectors.joining("\0")); + + if (!channels.isEmpty()) { + 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()) { 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); + } + } +} 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..258f7084e81 --- /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); + GeyserNetwork network = new GeyserNetwork(session); + + AtomicInteger state = new AtomicInteger(0); + + 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 -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.LATE, message -> { + assertEquals(2, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + new NetworkDefinitionBuilder(registration -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.NORMAL, message -> { + assertEquals(1, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + network.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); + }); + } + + @Test + void testPipelineTags() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + AtomicInteger state = new AtomicInteger(0); + + new NetworkDefinitionBuilder(registration -> network.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 -> network.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 -> network.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 -> network.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 -> network.onRegister(this.xuidChannel, XuidMessage::new, registration)) + .clientbound(MessagePriority.EARLY, message -> { + assertEquals(0, state.getAndIncrement()); + return MessageHandler.State.UNHANDLED; + }) + .register(); + + network.handleMessages(this.xuidChannel, List.of(new XuidMessage("test-xuid")), MessageDirection.CLIENTBOUND); + }); + } + + @Test + void testMixedHandlers() { + mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + GeyserNetwork network = new GeyserNetwork(session); + + AtomicInteger state = new AtomicInteger(0); + + // Simple early clientbound + new NetworkDefinitionBuilder(registration -> network.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 -> network.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 -> network.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 -> 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(); + + network.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/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); + } + } +} 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 024ac3ff94c..0185760dba2 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 @@ -52,10 +52,10 @@ import java.util.Map; import java.util.Optional; -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.assertNextPacketType; -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.assertNextPacketType; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.spawnArmorStand; import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.spawnPlayerSilently; 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 783644a71fb..055f30903de 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 @@ -60,10 +60,10 @@ import java.util.Optional; import java.util.UUID; -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.assertNextPacketType; -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.assertNextPacketType; +import static org.geysermc.geyser.util.AssertUtils.assertNoNextPacket; 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 c84df44f2ae..d8fd15f77eb 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 @@ -38,14 +38,15 @@ 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; import java.util.UUID; import java.util.function.Consumer; -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; 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 4c646f0a7ad..25791d09a30 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,11 +23,12 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.scoreboard.network.util; +package org.geysermc.geyser.util; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.configuration.GeyserConfig; +import org.geysermc.geyser.event.GeyserEventBus; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.mockito.Mockito; @@ -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); }