diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index a73ff451b..ae9a8006c 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -13,6 +13,7 @@ import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSource; +import com.velocitypowered.api.proxy.messages.PluginMessageEncoder; import com.velocitypowered.api.proxy.player.PlayerSettings; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.player.TabList; @@ -282,16 +283,42 @@ public interface Player extends @NotNull Collection getPendingResourcePacks(); /** - * Note that this method does not send a plugin message to the server the player + * {@inheritDoc} + * + *

Note that this method does not send a plugin message to the server the player * is connected to. You should only use this method if you are trying to communicate - * with a mod that is installed on the player's client. To send a plugin message to the server + * with a mod that is installed on the player's client.

+ * + *

To send a plugin message to the server * from the player, you should use the equivalent method on the instance returned by * {@link #getCurrentServer()}. * - * @inheritDoc + *

+   *    final ChannelIdentifier identifier;
+   *    final Player player;
+   *    player.getCurrentServer()
+   *          .map(ServerConnection::getServer)
+   *          .ifPresent((RegisteredServer server) -> {
+   *            server.sendPluginMessage(identifier, data);
+   *          });
+   *  
+ * */ @Override - boolean sendPluginMessage(@NotNull ChannelIdentifier identifier, byte @NotNull[] data); + boolean sendPluginMessage(@NotNull ChannelIdentifier identifier, byte @NotNull [] data); + + /** + * {@inheritDoc} + *

Note that this method does not send a plugin message to the server the player + * is connected to. You should only use this method if you are trying to communicate + * with a mod that is installed on the player's client.

+ * + *

To send a plugin message to the server + * from the player, you should use the equivalent method on the instance returned by + * {@link #getCurrentServer()}. + */ + @Override + boolean sendPluginMessage(@NotNull ChannelIdentifier identifier, @NotNull PluginMessageEncoder dataEncoder); @Override default @NotNull Key key() { diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSink.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSink.java index b310f13bc..3c1031f43 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSink.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSink.java @@ -7,6 +7,9 @@ package com.velocitypowered.api.proxy.messages; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + /** * Represents something that can be sent plugin messages. */ @@ -19,5 +22,26 @@ public interface ChannelMessageSink { * @param data the data to send * @return whether or not the message could be sent */ - boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data); + boolean sendPluginMessage(@NotNull ChannelIdentifier identifier, byte @NotNull[] data); + + /** + * Sends a plugin message to this target. + * + *

+   *   final ChannelMessageSink target;
+   *   final ChannelIdentifier identifier;
+   *   final boolean result = target.sendPluginMessage(identifier, (output) -> {
+   *     output.writeUTF("some input");
+   *     output.writeInt(1);
+   *   });
+   * 
+ * + * @param identifier the channel identifier to send the message on + * @param dataEncoder the encoder of the data to be sent + * @return whether the message could be sent + */ + @ApiStatus.Experimental + boolean sendPluginMessage( + @NotNull ChannelIdentifier identifier, + @NotNull PluginMessageEncoder dataEncoder); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/PluginMessageEncoder.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/PluginMessageEncoder.java new file mode 100644 index 000000000..b4fcf91a3 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/PluginMessageEncoder.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.proxy.messages; + +import com.google.common.io.ByteArrayDataOutput; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A data encoder to be sent via a plugin message. + * + * @since 3.3.0 + */ +@FunctionalInterface +@ApiStatus.Experimental +public interface PluginMessageEncoder { + + /** + * Encodes data into a {@link ByteArrayDataOutput} to be transmitted by plugin messages. + * + * @param output the {@link ByteArrayDataOutput} provided + */ + void encode(@NotNull ByteArrayDataOutput output); +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ComponentLoggerProviderImpl.java similarity index 97% rename from proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java rename to proxy/src/main/java/com/velocitypowered/proxy/adventure/ComponentLoggerProviderImpl.java index 3cdb945a7..12b4aaf50 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ComponentLoggerProviderImpl.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.provider; +package com.velocitypowered.proxy.adventure; import com.google.auto.service.AutoService; import com.velocitypowered.proxy.util.TranslatableMapper; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index e0b448bc8..8c027546f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -19,11 +19,13 @@ package com.velocitypowered.proxy.connection.backend; import static com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN; import static com.velocitypowered.proxy.network.Connections.HANDLER; +import static java.util.Objects.requireNonNull; import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.PluginMessageEncoder; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.proxy.VelocityServer; @@ -41,6 +43,7 @@ import com.velocitypowered.proxy.protocol.packet.HandshakePacket; import com.velocitypowered.proxy.protocol.packet.JoinGamePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; +import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -52,6 +55,7 @@ import java.util.concurrent.CompletableFuture; import net.kyori.adventure.nbt.CompoundBinaryTag; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; /** * Handles a connection from the proxy to some backend server. @@ -255,10 +259,31 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, } @Override - public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + public boolean sendPluginMessage( + final @NotNull ChannelIdentifier identifier, + final byte @NotNull [] data + ) { return sendPluginMessage(identifier, Unpooled.wrappedBuffer(data)); } + @Override + public boolean sendPluginMessage( + final @NotNull ChannelIdentifier identifier, + final @NotNull PluginMessageEncoder dataEncoder + ) { + requireNonNull(identifier); + requireNonNull(dataEncoder); + final ByteBuf buf = Unpooled.buffer(); + final ByteBufDataOutput dataOutput = new ByteBufDataOutput(buf); + dataEncoder.encode(dataOutput); + if (buf.isReadable()) { + return sendPluginMessage(identifier, buf); + } else { + buf.release(); + return false; + } + } + /** * Sends a plugin message to the server through this connection. * @@ -270,9 +295,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, Preconditions.checkNotNull(identifier, "identifier"); Preconditions.checkNotNull(data, "data"); - MinecraftConnection mc = ensureConnected(); + final MinecraftConnection mc = ensureConnected(); - PluginMessagePacket message = new PluginMessagePacket(identifier.getId(), data); + final PluginMessagePacket message = new PluginMessagePacket(identifier.getId(), data); mc.write(message); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 935a3c481..16df7bb84 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -19,6 +19,7 @@ package com.velocitypowered.proxy.connection.client; import static com.velocitypowered.api.proxy.ConnectionRequestBuilder.Status.ALREADY_CONNECTED; import static com.velocitypowered.proxy.connection.util.ConnectionRequestResults.plainResult; +import static java.util.Objects.requireNonNull; import static java.util.concurrent.CompletableFuture.completedFuture; import com.google.common.base.Preconditions; @@ -44,6 +45,7 @@ import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.PluginMessageEncoder; import com.velocitypowered.api.proxy.player.PlayerSettings; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -75,6 +77,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatPacket; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; +import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.tablist.InternalTabList; import com.velocitypowered.proxy.tablist.KeyedVelocityTabList; @@ -83,6 +86,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabListLegacy; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.DurationUtils; import com.velocitypowered.proxy.util.TranslatableMapper; +import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; import java.util.Collection; @@ -947,12 +951,32 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, public boolean sendPluginMessage(@NotNull ChannelIdentifier identifier, byte @NotNull [] data) { Preconditions.checkNotNull(identifier, "identifier"); Preconditions.checkNotNull(data, "data"); - PluginMessagePacket message = new PluginMessagePacket(identifier.getId(), + final PluginMessagePacket message = new PluginMessagePacket(identifier.getId(), Unpooled.wrappedBuffer(data)); connection.write(message); return true; } + @Override + public boolean sendPluginMessage( + final @NotNull ChannelIdentifier identifier, + final @NotNull PluginMessageEncoder dataEncoder + ) { + requireNonNull(identifier); + requireNonNull(dataEncoder); + final ByteBuf buf = Unpooled.buffer(); + final ByteBufDataOutput dataOutput = new ByteBufDataOutput(buf); + dataEncoder.encode(dataOutput); + if (buf.isReadable()) { + final PluginMessagePacket message = new PluginMessagePacket(identifier.getId(), buf); + connection.write(message); + return true; + } else { + buf.release(); + return false; + } + } + @Override @Nullable public String getClientBrand() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java index 837c46a25..a3ddd71ca 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -23,11 +23,13 @@ import static com.velocitypowered.proxy.network.Connections.HANDLER; import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER; import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; +import static java.util.Objects.requireNonNull; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.PluginMessageEncoder; import com.velocitypowered.api.proxy.server.PingOptions; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; @@ -42,6 +44,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; @@ -59,6 +62,7 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; /** * Represents a server registered on the proxy. @@ -107,9 +111,9 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud throw new IllegalStateException("No Velocity proxy instance available"); } CompletableFuture pingFuture = new CompletableFuture<>(); - server.createBootstrap(loop).handler(new ChannelInitializer() { + server.createBootstrap(loop).handler(new ChannelInitializer<>() { @Override - protected void initChannel(Channel ch) throws Exception { + protected void initChannel(Channel ch) { ch.pipeline().addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) .addLast(READ_TIMEOUT, new ReadTimeoutHandler( pingOptions.getTimeout() == 0 @@ -143,10 +147,30 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud } @Override - public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + public boolean sendPluginMessage(final @NotNull ChannelIdentifier identifier, final byte @NotNull [] data) { + requireNonNull(identifier); + requireNonNull(data); return sendPluginMessage(identifier, Unpooled.wrappedBuffer(data)); } + @Override + public boolean sendPluginMessage( + final @NotNull ChannelIdentifier identifier, + final @NotNull PluginMessageEncoder dataEncoder + ) { + requireNonNull(identifier); + requireNonNull(dataEncoder); + final ByteBuf buf = Unpooled.buffer(); + final ByteBufDataOutput dataInput = new ByteBufDataOutput(buf); + dataEncoder.encode(dataInput); + if (buf.isReadable()) { + return sendPluginMessage(identifier, buf); + } else { + buf.release(); + return false; + } + } + /** * Sends a plugin message to the server through this connection. The message will be released * afterwards. @@ -156,8 +180,8 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud * @return whether or not the message was sent */ public boolean sendPluginMessage(ChannelIdentifier identifier, ByteBuf data) { - for (ConnectedPlayer player : players.values()) { - VelocityServerConnection serverConnection = player.getConnectedServer(); + for (final ConnectedPlayer player : players.values()) { + final VelocityServerConnection serverConnection = player.getConnectedServer(); if (serverConnection != null && serverConnection.getConnection() != null && serverConnection.getServer() == this) { return serverConnection.sendPluginMessage(identifier, data);