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 extends Message.Packet> 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 extends Message.Packet> 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 extends T> 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 extends BedrockPacket> 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