diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerResourcePackStatusEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerResourcePackStatusEvent.java index 5bffcce89..bfd7a5ea5 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/PlayerResourcePackStatusEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerResourcePackStatusEvent.java @@ -8,7 +8,11 @@ package com.velocitypowered.api.event.player; import com.google.common.base.Preconditions; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; /** * This event is fired when the status of a resource pack sent to the player by the server is @@ -18,10 +22,29 @@ public class PlayerResourcePackStatusEvent { private final Player player; private final Status status; + private final @MonotonicNonNull ResourcePackInfo packInfo; + private boolean overwriteKick; + + /** + * Instantiates this event. + * @deprecated Use {@link PlayerResourcePackStatusEvent#PlayerResourcePackStatusEvent + * (Player, Status, ResourcePackInfo)} instead. + */ + @Deprecated public PlayerResourcePackStatusEvent(Player player, Status status) { this.player = Preconditions.checkNotNull(player, "player"); this.status = Preconditions.checkNotNull(status, "status"); + this.packInfo = null; + } + + /** + * Instantiates this event. + */ + public PlayerResourcePackStatusEvent(Player player, Status status, ResourcePackInfo packInfo) { + this.player = Preconditions.checkNotNull(player, "player"); + this.status = Preconditions.checkNotNull(status, "status"); + this.packInfo = packInfo; } /** @@ -42,11 +65,49 @@ public class PlayerResourcePackStatusEvent { return status; } + /** + * Returns the {@link ResourcePackInfo} this response is for. + * + * @return the resource-pack info or null if no request was recorded + */ + @Nullable + public ResourcePackInfo getPackInfo() { + return packInfo; + } + + /** + * Gets whether or not to override the kick resulting from + * {@link ResourcePackInfo#getShouldForce()} being true. + * + * @return whether or not to overwrite the result + */ + public boolean isOverwriteKick() { + return overwriteKick; + } + + /** + * Set to true to prevent {@link ResourcePackInfo#getShouldForce()} + * from kicking the player. + * Overwriting this kick is only possible on versions older than 1.17, + * as the client or server will enforce this regardless. Cancelling the resulting + * kick-events will not prevent the player from disconnecting from the proxy. + * + * @param overwriteKick whether or not to cancel the kick + * @throws IllegalArgumentException if the player version is 1.17 or newer + */ + public void setOverwriteKick(boolean overwriteKick) { + Preconditions.checkArgument(player.getProtocolVersion() + .compareTo(ProtocolVersion.MINECRAFT_1_17) < 0, + "overwriteKick is not supported on 1.17 or newer"); + this.overwriteKick = overwriteKick; + } + @Override public String toString() { return "PlayerResourcePackStatusEvent{" + "player=" + player + ", status=" + status + + ", packInfo=" + packInfo + '}'; } diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 8b3632f9f..20662ab66 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -53,7 +53,8 @@ public enum ProtocolVersion { MINECRAFT_1_16_1(736, "1.16.1"), MINECRAFT_1_16_2(751, "1.16.2"), MINECRAFT_1_16_3(753, "1.16.3"), - MINECRAFT_1_16_4(754, "1.16.4", "1.16.5"); + MINECRAFT_1_16_4(754, "1.16.4", "1.16.5"), + MINECRAFT_1_17(-1, 34, "1.17"); // Snapshot: 1.17-rc1, future protocol: 755 private static final int SNAPSHOT_BIT = 30; 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 53704a291..b6cca5dd9 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.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.proxy.player.PlayerSettings; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; @@ -22,6 +23,7 @@ import java.util.Optional; import java.util.UUID; import net.kyori.adventure.identity.Identified; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Represents a player who is connected to the proxy. @@ -158,7 +160,9 @@ public interface Player extends CommandSource, Identified, InboundConnection, * sent resource pack, subscribe to {@link PlayerResourcePackStatusEvent}. * * @param url the URL for the resource pack + * @deprecated Use {@link #sendResourcePackOffer(ResourcePackInfo)} instead */ + @Deprecated void sendResourcePack(String url); /** @@ -168,9 +172,41 @@ public interface Player extends CommandSource, Identified, InboundConnection, * * @param url the URL for the resource pack * @param hash the SHA-1 hash value for the resource pack + * @deprecated Use {@link #sendResourcePackOffer(ResourcePackInfo)} instead */ + @Deprecated void sendResourcePack(String url, byte[] hash); + /** + * Queues and sends a new Resource-pack offer to the player. + * To monitor the status of the sent resource pack, subscribe to + * {@link PlayerResourcePackStatusEvent}. + * To create a {@link ResourcePackInfo} use the + * {@link ProxyServer#createResourcePackBuilder(String)} builder. + * + * @param packInfo the resource-pack in question + */ + void sendResourcePackOffer(ResourcePackInfo packInfo); + + /** + * Gets the {@link ResourcePackInfo} of the currently applied + * resource-pack or null if none. + * + * @return the applied resource pack or null if none. + */ + @Nullable + ResourcePackInfo getAppliedResourcePack(); + + /** + * Gets the {@link ResourcePackInfo} of the resource pack + * the user is currently downloading or is currently + * prompted to install or null if none. + * + * @return the pending resource pack or null if none + */ + @Nullable + ResourcePackInfo getPendingResourcePack(); + /** * 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 diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index ed0593de2..e16bb0673 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -13,6 +13,7 @@ import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.proxy.config.ProxyConfig; import com.velocitypowered.api.proxy.messages.ChannelRegistrar; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.api.scheduler.Scheduler; @@ -186,4 +187,26 @@ public interface ProxyServer extends Audience { * @return the proxy version */ ProxyVersion getVersion(); + + /** + * Creates a builder to build a {@link ResourcePackInfo} instance for use with + * {@link com.velocitypowered.api.proxy.Player#sendResourcePackOffer(ResourcePackInfo)}. + * + *

Note: The resource-pack location should always: + * - Use HTTPS with a valid certificate. + * - Be in a crawler-accessible location. Having it behind Cloudflare or other DoS/Bot/crawler + * protection may cause issues in downloading. + * - Be on a web-server with enough bandwidth and reliable connection + * so the download does not time out or fail.

+ * + *

Do also make sure that the resource pack is in the correct format for the version + * of the client. It is also highly recommended to always provide the resource-pack SHA-1 hash + * of the resource pack with {@link ResourcePackInfo.Builder#setHash(byte[])} + * whenever possible to save bandwidth. If a hash is present the client will first check + * if it already has a resource pack by that hash cached.

+ * + * @param url The url where the resource pack can be found + * @return a ResourcePackInfo builder + */ + ResourcePackInfo.Builder createResourcePackBuilder(String url); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/ResourcePackInfo.java b/api/src/main/java/com/velocitypowered/api/proxy/player/ResourcePackInfo.java new file mode 100644 index 000000000..4643dd422 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/ResourcePackInfo.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2018 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.player; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +public interface ResourcePackInfo { + + /** + * Gets the link the resource-pack can be found at. + * + * @return the location of the resource-pack + */ + String getUrl(); + + /** + * Gets the {@link Component} that is displayed on the resource-pack prompt. + * This is only displayed if the client version is 1.17 or newer. + * + * @return the prompt if present or null otherwise + */ + @Nullable + Component getPrompt(); + + /** + * Gets whether or not the acceptance of the resource-pack is enforced. + * See {@link Builder#setShouldForce(boolean)} for more information. + * + * @return whether or not to force usage of this resource-pack + */ + boolean getShouldForce(); + + /** + * Gets the SHA-1 hash of the resource-pack + * See {@link Builder#setHash(byte[])} for more information. + * + * @return the hash if present or null otherwise + */ + @Nullable + byte[] getHash(); + + /** + * Gets the {@link Origin} of the resource-pack. + * + * @return the origin of the resource pack + */ + Origin getOrigin(); + + interface Builder { + + /** + * Sets the resource-pack as required to play on the network. + * This feature was introduced in 1.17. + * Setting this to true has one of two effects: + * If the client is on 1.17 or newer: + * - The resource-pack prompt will display without a decline button + * - Accept or disconnect are the only available options but players may still press escape. + * - Forces the resource-pack offer prompt to display even if the player has + * previously declined or disabled resource packs + * - The player will be disconnected from the network if they close/skip the prompt. + * If the client is on a version older than 1.17: + * - If the player accepts the resource pack or has previously accepted a resource-pack + * then nothing else will happen. + * - If the player declines the resource pack or has previously declined a resource-pack + * the player will be disconnected from the network + * + * @param shouldForce whether or not to force the client to accept the resource pack + */ + Builder setShouldForce(boolean shouldForce); + + /** + * Sets the SHA-1 hash of the provided resource pack. + * Note: It is recommended to always set this hash. + * If this hash is not set/ not present then the client will always download + * the resource pack even if it may still be cached. By having this hash present, + * the client will check first whether or not a resource pack by this hash is cached + * before downloading. + * + * @param hash the SHA-1 hash of the resource-pack + */ + Builder setHash(@Nullable byte[] hash); + + /** + * Sets a {@link Component} to display on the download prompt. + * This will only display if the client version is 1.17 or newer. + * + * @param prompt the component to display + */ + Builder setPrompt(@Nullable Component prompt); + + /** + * Builds the {@link ResourcePackInfo} from the provided info for use with + * {@link com.velocitypowered.api.proxy.Player#sendResourcePackOffer(ResourcePackInfo)}. + * Note: Some features may be version-dependent. Check before use. + * + * @return a ResourcePackInfo instance from the provided information + */ + ResourcePackInfo build(); + } + + /** + * Represents the origin of the resource-pack. + */ + enum Origin { + /** + * Resource-pack originated from the downstream server. + */ + DOWNSTREAM_SERVER, + /** + * The resource-pack originated from a plugin on this proxy. + */ + PLUGIN_ON_PROXY + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index ed3bbf801..1dd4e8a35 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -31,6 +31,7 @@ import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.api.util.Favicon; @@ -43,6 +44,7 @@ import com.velocitypowered.proxy.command.builtin.ShutdownCommand; import com.velocitypowered.proxy.command.builtin.VelocityCommand; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.console.VelocityConsole; import com.velocitypowered.proxy.event.VelocityEventManager; import com.velocitypowered.proxy.network.ConnectionManager; @@ -660,4 +662,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { return version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0 ? POST_1_16_PING_SERIALIZER : PRE_1_16_PING_SERIALIZER; } + + @Override + public ResourcePackInfo.Builder createResourcePackBuilder(String url) { + return new VelocityResourcePackInfo.BuilderImpl(url); + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index 3dcb3807a..e72f42d64 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -46,7 +46,12 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; -import com.velocitypowered.proxy.protocol.packet.TitlePacket; +import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleSubtitlePacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleTextPacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleTimesPacket; import io.netty.buffer.ByteBuf; public interface MinecraftSessionHandler { @@ -191,7 +196,27 @@ public interface MinecraftSessionHandler { return false; } - default boolean handle(TitlePacket packet) { + default boolean handle(LegacyTitlePacket packet) { + return false; + } + + default boolean handle(TitleTextPacket packet) { + return false; + } + + default boolean handle(TitleSubtitlePacket packet) { + return false; + } + + default boolean handle(TitleActionbarPacket packet) { + return false; + } + + default boolean handle(TitleTimesPacket packet) { + return false; + } + + default boolean handle(TitleClearPacket packet) { return false; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index b3c7133cf..f42a6c085 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -19,6 +19,7 @@ package com.velocitypowered.proxy.connection.backend; import static com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder.getBungeeCordChannel; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.tree.CommandNode; @@ -28,10 +29,12 @@ import com.velocitypowered.api.event.command.PlayerAvailableCommandsEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.AvailableCommands; @@ -40,6 +43,7 @@ import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PlayerListItem; import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; @@ -128,6 +132,21 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return false; // forward } + @Override + public boolean handle(ResourcePackRequest packet) { + ResourcePackInfo.Builder builder = new VelocityResourcePackInfo.BuilderImpl( + Preconditions.checkNotNull(packet.getUrl())) + .setPrompt(packet.getPrompt()) + .setShouldForce(packet.isRequired()); + // Why SpotBugs decides that this is unsafe I have no idea; + if (packet.getHash() != null && !Preconditions.checkNotNull(packet.getHash()).isEmpty()) { + builder.setHash(ByteBufUtil.decodeHexDump(packet.getHash())); + } + + serverConn.getPlayer().queueResourcePack(builder.build()); + return true; + } + @Override public boolean handle(PluginMessage packet) { if (bungeecordMessageResponder.process(packet)) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 8ac397500..7c8729610 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -33,6 +33,7 @@ import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -53,7 +54,7 @@ import com.velocitypowered.proxy.protocol.packet.Respawn; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer; -import com.velocitypowered.proxy.protocol.packet.TitlePacket; +import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -72,6 +73,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -283,9 +285,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ResourcePackResponse packet) { - server.getEventManager().fireAndForget(new PlayerResourcePackStatusEvent(player, - packet.getStatus())); - return false; + return player.onResourcePackResponse(packet.getStatus(), + ByteBufUtil.decodeHexDump(packet.getHash())); } @Override @@ -400,7 +401,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Clear any title from the previous server. if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { player.getConnection() - .delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion())); + .delayedWrite(GenericTitlePacket.constructTitlePacket( + GenericTitlePacket.ActionType.RESET, player.getProtocolVersion())); } // Flush everything diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientSettingsWrapper.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientSettingsWrapper.java index b268ee19c..38942fa16 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientSettingsWrapper.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientSettingsWrapper.java @@ -26,7 +26,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class ClientSettingsWrapper implements PlayerSettings { static final PlayerSettings DEFAULT = new ClientSettingsWrapper( - new ClientSettings("en_US", (byte) 10, 0, true, (short) 127, 1)); + new ClientSettings("en_US", (byte) 10, 0, true, (short) 127, 1, true)); private final ClientSettings settings; private final SkinParts parts; 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 b3f14adea..9e2c36562 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 @@ -31,6 +31,7 @@ import com.velocitypowered.api.event.player.KickedFromServerEvent.Notify; import com.velocitypowered.api.event.player.KickedFromServerEvent.RedirectPlayer; import com.velocitypowered.api.event.player.KickedFromServerEvent.ServerKickResult; import com.velocitypowered.api.event.player.PlayerModInfoEvent; +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; import com.velocitypowered.api.network.ProtocolVersion; @@ -42,6 +43,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.player.PlayerSettings; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ModInfo; @@ -51,6 +53,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.protocol.ProtocolUtils; @@ -62,7 +65,7 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; -import com.velocitypowered.proxy.protocol.packet.TitlePacket; +import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.tablist.VelocityTabList; @@ -72,12 +75,14 @@ import com.velocitypowered.proxy.util.collect.CappedSet; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; +import java.util.ArrayDeque; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; +import java.util.Queue; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -129,6 +134,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { private final Collection knownChannels; private final CompletableFuture teardownFuture = new CompletableFuture<>(); private @MonotonicNonNull List serversToTry = null; + private @MonotonicNonNull Boolean previousResourceResponse; + private final Queue outstandingResourcePacks = new ArrayDeque<>(); + private @Nullable ResourcePackInfo pendingResourcePack; + private @Nullable ResourcePackInfo appliedResourcePack; ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, @Nullable InetSocketAddress virtualHost, boolean onlineMode) { @@ -269,8 +278,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { ProtocolVersion playerVersion = getProtocolVersion(); if (playerVersion.compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0) { // Use the title packet instead. - TitlePacket pkt = new TitlePacket(); - pkt.setAction(TitlePacket.SET_ACTION_BAR); + GenericTitlePacket pkt = GenericTitlePacket.constructTitlePacket( + GenericTitlePacket.ActionType.SET_ACTION_BAR, playerVersion); pkt.setComponent(ProtocolUtils.getJsonChatSerializer(playerVersion) .serialize(message)); connection.write(pkt); @@ -321,17 +330,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer(this .getProtocolVersion()); - TitlePacket titlePkt = new TitlePacket(); - titlePkt.setAction(TitlePacket.SET_TITLE); + GenericTitlePacket titlePkt = GenericTitlePacket.constructTitlePacket( + GenericTitlePacket.ActionType.SET_TITLE, this.getProtocolVersion()); titlePkt.setComponent(serializer.serialize(title.title())); connection.delayedWrite(titlePkt); - TitlePacket subtitlePkt = new TitlePacket(); - subtitlePkt.setAction(TitlePacket.SET_SUBTITLE); + GenericTitlePacket subtitlePkt = GenericTitlePacket.constructTitlePacket( + GenericTitlePacket.ActionType.SET_SUBTITLE, this.getProtocolVersion()); subtitlePkt.setComponent(serializer.serialize(title.subtitle())); connection.delayedWrite(subtitlePkt); - TitlePacket timesPkt = TitlePacket.timesForProtocolVersion(this.getProtocolVersion()); + GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket( + GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion()); net.kyori.adventure.title.Title.Times times = title.times(); if (times != null) { timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn())); @@ -347,14 +357,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { @Override public void clearTitle() { if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { - connection.write(TitlePacket.hideForProtocolVersion(this.getProtocolVersion())); + connection.write(GenericTitlePacket.constructTitlePacket( + GenericTitlePacket.ActionType.HIDE, this.getProtocolVersion())); } } @Override public void resetTitle() { if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { - connection.write(TitlePacket.resetForProtocolVersion(this.getProtocolVersion())); + connection.write(GenericTitlePacket.constructTitlePacket( + GenericTitlePacket.ActionType.RESET, this.getProtocolVersion())); } } @@ -752,29 +764,132 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } @Override + @Deprecated public void sendResourcePack(String url) { - Preconditions.checkNotNull(url, "url"); + sendResourcePackOffer(new VelocityResourcePackInfo.BuilderImpl(url).build()); + } + @Override + @Deprecated + public void sendResourcePack(String url, byte[] hash) { + sendResourcePackOffer(new VelocityResourcePackInfo.BuilderImpl(url).setHash(hash).build()); + } + + @Override + public void sendResourcePackOffer(ResourcePackInfo packInfo) { if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + Preconditions.checkNotNull(packInfo, "packInfo"); + queueResourcePack(packInfo); + } + } + + /** + * Queues a resource-pack for sending to the player and sends it + * immediately if the queue is empty. + */ + public void queueResourcePack(ResourcePackInfo info) { + outstandingResourcePacks.add(info); + if (outstandingResourcePacks.size() == 1) { + tickResourcePackQueue(); + } + } + + private void tickResourcePackQueue() { + ResourcePackInfo queued = outstandingResourcePacks.peek(); + + if (queued != null) { + // Check if the player declined a resource pack once already + if (previousResourceResponse != null && !previousResourceResponse) { + // If that happened we can flush the queue right away. + // Unless its 1.17+ and forced it will come back denied anyway + while (!outstandingResourcePacks.isEmpty()) { + queued = outstandingResourcePacks.peek(); + if (queued.getShouldForce() && getProtocolVersion() + .compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { + break; + } + onResourcePackResponse(PlayerResourcePackStatusEvent.Status.DECLINED, new byte[0]); + queued = null; + } + if (queued == null) { + // Exit as the queue was cleared + return; + } + } + ResourcePackRequest request = new ResourcePackRequest(); - request.setUrl(url); - request.setHash(""); + request.setUrl(queued.getUrl()); + if (queued.getHash() != null) { + request.setHash(ByteBufUtil.hexDump(queued.getHash())); + } else { + request.setHash(""); + } + request.setRequired(queued.getShouldForce()); + request.setPrompt(queued.getPrompt()); + connection.write(request); } } @Override - public void sendResourcePack(String url, byte[] hash) { - Preconditions.checkNotNull(url, "url"); - Preconditions.checkNotNull(hash, "hash"); - Preconditions.checkArgument(hash.length == 20, "Hash length is not 20"); + public @Nullable ResourcePackInfo getAppliedResourcePack() { + return appliedResourcePack; + } - if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { - ResourcePackRequest request = new ResourcePackRequest(); - request.setUrl(url); - request.setHash(ByteBufUtil.hexDump(hash)); - connection.write(request); + @Override + public @Nullable ResourcePackInfo getPendingResourcePack() { + return pendingResourcePack; + } + + /** + * Processes a client response to a sent resource-pack. + */ + public boolean onResourcePackResponse(PlayerResourcePackStatusEvent.Status status, + @Nullable byte[] hash) { + + final boolean peek = status == PlayerResourcePackStatusEvent.Status.ACCEPTED; + final ResourcePackInfo queued = peek + ? outstandingResourcePacks.peek() : outstandingResourcePacks.poll(); + + server.getEventManager().fire(new PlayerResourcePackStatusEvent(this, status, queued)) + .thenAcceptAsync(event -> { + if (event.getStatus() == PlayerResourcePackStatusEvent.Status.DECLINED + && event.getPackInfo() != null && event.getPackInfo().getShouldForce() + && (!event.isOverwriteKick() || event.getPlayer() + .getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) + ) { + event.getPlayer().disconnect(Component + .translatable("multiplayer.requiredTexturePrompt.disconnect")); + } + }); + + + switch (status) { + case ACCEPTED: + previousResourceResponse = true; + pendingResourcePack = queued; + break; + case DECLINED: + previousResourceResponse = false; + break; + case SUCCESSFUL: + appliedResourcePack = queued; + pendingResourcePack = null; + break; + case FAILED_DOWNLOAD: + pendingResourcePack = null; + break; + default: + break; } + + if (!peek) { + connection.eventLoop().execute(() -> { + tickResourcePackQueue(); + }); + } + + return queued != null && queued.getOrigin() == ResourcePackInfo.Origin.DOWNSTREAM_SERVER; } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java new file mode 100644 index 000000000..250e9b3aa --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class VelocityResourcePackInfo implements ResourcePackInfo { + private final String url; + private final @Nullable byte[] hash; + private final boolean shouldForce; + private final @Nullable Component prompt; // 1.17+ only + private final Origin origin; + + private VelocityResourcePackInfo(String url, @Nullable byte[] hash, boolean shouldForce, + @Nullable Component prompt, Origin origin) { + this.url = url; + this.hash = hash; + this.shouldForce = shouldForce; + this.prompt = prompt; + this.origin = origin; + } + + @Override + public String getUrl() { + return url; + } + + @Override + public @Nullable Component getPrompt() { + return prompt; + } + + @Override + public boolean getShouldForce() { + return shouldForce; + } + + @Override + public @Nullable byte[] getHash() { + return hash == null ? null : hash.clone(); // Thanks spotbugs, very helpful. + } + + @Override + public Origin getOrigin() { + return origin; + } + + public static final class BuilderImpl implements ResourcePackInfo.Builder { + private final String url; + private boolean shouldForce; + private @Nullable byte[] hash; + private @Nullable Component prompt; + private Origin origin = Origin.PLUGIN_ON_PROXY; + + public BuilderImpl(String url) { + this.url = Preconditions.checkNotNull(url, "url"); + } + + @Override + public BuilderImpl setShouldForce(boolean shouldForce) { + this.shouldForce = shouldForce; + return this; + } + + @Override + public BuilderImpl setHash(@Nullable byte[] hash) { + if (hash != null) { + Preconditions.checkArgument(hash.length == 20, "Hash length is not 20"); + this.hash = hash.clone(); // Thanks spotbugs, very helpful. + } else { + this.hash = null; + } + return this; + } + + @Override + public BuilderImpl setPrompt(@Nullable Component prompt) { + this.prompt = prompt; + return this; + } + + @Override + public ResourcePackInfo build() { + return new VelocityResourcePackInfo(url, hash, shouldForce, prompt, origin); + } + + public void setOrigin(Origin origin) { + this.origin = origin; + } + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DimensionData.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DimensionData.java index 406bce93a..07b69baef 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DimensionData.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DimensionData.java @@ -43,6 +43,8 @@ public final class DimensionData { private final @Nullable Boolean createDragonFight; private final @Nullable Double coordinateScale; private final @Nullable String effects; + private final @Nullable Integer minY; // Required and added by 1.17 + private final @Nullable Integer height; // Required and added by 1.17 /** * Initializes a new {@link DimensionData} instance. @@ -64,6 +66,8 @@ public final class DimensionData { * @param createDragonFight optional. Internal flag used in the end dimension * @param coordinateScale optional, unknown purpose * @param effects optional, unknown purpose + * @param minY the world effective lowest build-level + * @param height the world height above zero */ public DimensionData(String registryIdentifier, @Nullable Integer dimensionId, @@ -75,7 +79,8 @@ public final class DimensionData { int logicalHeight, String burningBehaviourIdentifier, @Nullable Long fixedTime, @Nullable Boolean createDragonFight, @Nullable Double coordinateScale, - @Nullable String effects) { + @Nullable String effects, + @Nullable Integer minY, @Nullable Integer height) { Preconditions.checkNotNull( registryIdentifier, "registryIdentifier cannot be null"); Preconditions.checkArgument(registryIdentifier.length() > 0, @@ -103,6 +108,8 @@ public final class DimensionData { this.createDragonFight = createDragonFight; this.coordinateScale = coordinateScale; this.effects = effects; + this.minY = minY; + this.height = height; } public String getRegistryIdentifier() { @@ -173,6 +180,14 @@ public final class DimensionData { return coordinateScale; } + public @Nullable Integer getMinY() { + return minY; + } + + public @Nullable Integer getHeight() { + return height; + } + /** * Returns a fresh {@link DimensionData} with the specified {@code registryIdentifier} * and {@code dimensionId}. @@ -186,7 +201,7 @@ public final class DimensionData { return new DimensionData(registryIdentifier, dimensionId, isNatural, ambientLight, isShrunk, isUltrawarm, hasCeiling, hasSkylight, isPiglinSafe, doBedsWork, doRespawnAnchorsWork, hasRaids, logicalHeight, burningBehaviourIdentifier, fixedTime, createDragonFight, - coordinateScale, effects); + coordinateScale, effects, minY, height); } public boolean isUnannotated() { @@ -223,11 +238,19 @@ public final class DimensionData { ? details.getDouble("coordinate_scale") : null; String effects = details.keySet().contains("effects") ? details.getString("effects") : null; + Integer minY = details.keySet().contains("min_y") ? details.getInt("min_y") : null; + Integer height = details.keySet().contains("height") ? details.getInt("height") : null; + if (version.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { + Preconditions.checkNotNull(height, + "DimensionData requires 'height' to be present for this version"); + Preconditions.checkNotNull(minY, + "DimensionData requires 'minY' to be present for this version"); + } return new DimensionData( UNKNOWN_DIMENSION_ID, null, isNatural, ambientLight, isShrunk, isUltrawarm, hasCeiling, hasSkylight, isPiglinSafe, doBedsWork, doRespawnAnchorsWork, hasRaids, logicalHeight, burningBehaviourIdentifier, fixedTime, hasEnderdragonFight, - coordinateScale, effects); + coordinateScale, effects, minY, height); } /** @@ -306,6 +329,12 @@ public final class DimensionData { if (effects != null) { ret.putString("effects", effects); } + if (minY != null) { + ret.putInt("min_y", minY); + } + if (height != null) { + ret.putInt("height", height); + } return ret.build(); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 87bc5d091..8e787539f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -25,6 +25,8 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_2; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_4; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -60,7 +62,12 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; -import com.velocitypowered.proxy.protocol.packet.TitlePacket; +import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleSubtitlePacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleTextPacket; +import com.velocitypowered.proxy.protocol.packet.title.TitleTimesPacket; import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; @@ -124,7 +131,8 @@ public enum StateRegistry { map(0x0A, MINECRAFT_1_12, false), map(0x09, MINECRAFT_1_12_1, false), map(0x0A, MINECRAFT_1_13, false), - map(0x0B, MINECRAFT_1_14, false)); + map(0x0B, MINECRAFT_1_14, false), + map(0x0A, MINECRAFT_1_17, false)); serverbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x0B, MINECRAFT_1_9, false), @@ -132,7 +140,8 @@ public enum StateRegistry { map(0x0B, MINECRAFT_1_12_1, false), map(0x0E, MINECRAFT_1_13, false), map(0x0F, MINECRAFT_1_14, false), - map(0x10, MINECRAFT_1_16, false)); + map(0x10, MINECRAFT_1_16, false), + map(0x0F, MINECRAFT_1_17, false)); serverbound.register(ResourcePackResponse.class, ResourcePackResponse::new, map(0x19, MINECRAFT_1_8, false), map(0x16, MINECRAFT_1_9, false), @@ -145,25 +154,29 @@ public enum StateRegistry { clientbound.register(BossBar.class, BossBar::new, map(0x0C, MINECRAFT_1_9, false), map(0x0D, MINECRAFT_1_15, false), - map(0x0C, MINECRAFT_1_16, false)); + map(0x0C, MINECRAFT_1_16, false), + map(0x0D, MINECRAFT_1_17, false)); clientbound.register(Chat.class, Chat::new, map(0x02, MINECRAFT_1_7_2, true), map(0x0F, MINECRAFT_1_9, true), map(0x0E, MINECRAFT_1_13, true), map(0x0F, MINECRAFT_1_15, true), - map(0x0E, MINECRAFT_1_16, true)); + map(0x0E, MINECRAFT_1_16, true), + map(0x0F, MINECRAFT_1_17, true)); clientbound.register(TabCompleteResponse.class, TabCompleteResponse::new, map(0x3A, MINECRAFT_1_7_2, false), map(0x0E, MINECRAFT_1_9, false), map(0x10, MINECRAFT_1_13, false), map(0x11, MINECRAFT_1_15, false), map(0x10, MINECRAFT_1_16, false), - map(0x0F, MINECRAFT_1_16_2, false)); + map(0x0F, MINECRAFT_1_16_2, false), + map(0x11, MINECRAFT_1_17, false)); clientbound.register(AvailableCommands.class, AvailableCommands::new, map(0x11, MINECRAFT_1_13, false), map(0x12, MINECRAFT_1_15, false), map(0x11, MINECRAFT_1_16, false), - map(0x10, MINECRAFT_1_16_2, false)); + map(0x10, MINECRAFT_1_16_2, false), + map(0x12, MINECRAFT_1_17, false)); clientbound.register(PluginMessage.class, PluginMessage::new, map(0x3F, MINECRAFT_1_7_2, false), map(0x18, MINECRAFT_1_9, false), @@ -171,7 +184,8 @@ public enum StateRegistry { map(0x18, MINECRAFT_1_14, false), map(0x19, MINECRAFT_1_15, false), map(0x18, MINECRAFT_1_16, false), - map(0x17, MINECRAFT_1_16_2, false)); + map(0x17, MINECRAFT_1_16_2, false), + map(0x18, MINECRAFT_1_17, false)); clientbound.register(Disconnect.class, Disconnect::new, map(0x40, MINECRAFT_1_7_2, false), map(0x1A, MINECRAFT_1_9, false), @@ -179,7 +193,8 @@ public enum StateRegistry { map(0x1A, MINECRAFT_1_14, false), map(0x1B, MINECRAFT_1_15, false), map(0x1A, MINECRAFT_1_16, false), - map(0x19, MINECRAFT_1_16_2, false)); + map(0x19, MINECRAFT_1_16_2, false), + map(0x1A, MINECRAFT_1_17, false)); clientbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x1F, MINECRAFT_1_9, false), @@ -187,7 +202,8 @@ public enum StateRegistry { map(0x20, MINECRAFT_1_14, false), map(0x21, MINECRAFT_1_15, false), map(0x20, MINECRAFT_1_16, false), - map(0x1F, MINECRAFT_1_16_2, false)); + map(0x1F, MINECRAFT_1_16_2, false), + map(0x21, MINECRAFT_1_17, false)); clientbound.register(JoinGame.class, JoinGame::new, map(0x01, MINECRAFT_1_7_2, false), map(0x23, MINECRAFT_1_9, false), @@ -195,7 +211,8 @@ public enum StateRegistry { map(0x25, MINECRAFT_1_14, false), map(0x26, MINECRAFT_1_15, false), map(0x25, MINECRAFT_1_16, false), - map(0x24, MINECRAFT_1_16_2, false)); + map(0x24, MINECRAFT_1_16_2, false), + map(0x26, MINECRAFT_1_17, false)); clientbound.register(Respawn.class, Respawn::new, map(0x07, MINECRAFT_1_7_2, true), map(0x33, MINECRAFT_1_9, true), @@ -205,17 +222,19 @@ public enum StateRegistry { map(0x3A, MINECRAFT_1_14, true), map(0x3B, MINECRAFT_1_15, true), map(0x3A, MINECRAFT_1_16, true), - map(0x39, MINECRAFT_1_16_2, true)); + map(0x39, MINECRAFT_1_16_2, true), + map(0x3D, MINECRAFT_1_17, true)); clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new, - map(0x48, MINECRAFT_1_8, true), - map(0x32, MINECRAFT_1_9, true), - map(0x33, MINECRAFT_1_12, true), - map(0x34, MINECRAFT_1_12_1, true), - map(0x37, MINECRAFT_1_13, true), - map(0x39, MINECRAFT_1_14, true), - map(0x3A, MINECRAFT_1_15, true), - map(0x39, MINECRAFT_1_16, true), - map(0x38, MINECRAFT_1_16_2, true)); + map(0x48, MINECRAFT_1_8, false), + map(0x32, MINECRAFT_1_9, false), + map(0x33, MINECRAFT_1_12, false), + map(0x34, MINECRAFT_1_12_1, false), + map(0x37, MINECRAFT_1_13, false), + map(0x39, MINECRAFT_1_14, false), + map(0x3A, MINECRAFT_1_15, false), + map(0x39, MINECRAFT_1_16, false), + map(0x38, MINECRAFT_1_16_2, false), + map(0x3C, MINECRAFT_1_17, false)); clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), @@ -225,8 +244,9 @@ public enum StateRegistry { map(0x4E, MINECRAFT_1_13, true), map(0x53, MINECRAFT_1_14, true), map(0x54, MINECRAFT_1_15, true), - map(0x53, MINECRAFT_1_16, true)); - clientbound.register(TitlePacket.class, TitlePacket::new, + map(0x53, MINECRAFT_1_16, true), + map(0x5E, MINECRAFT_1_17, true)); + clientbound.register(LegacyTitlePacket.class, LegacyTitlePacket::new, map(0x45, MINECRAFT_1_8, true), map(0x45, MINECRAFT_1_9, true), map(0x47, MINECRAFT_1_12, true), @@ -234,7 +254,17 @@ public enum StateRegistry { map(0x4B, MINECRAFT_1_13, true), map(0x4F, MINECRAFT_1_14, true), map(0x50, MINECRAFT_1_15, true), - map(0x4F, MINECRAFT_1_16, true)); + map(0x4F, MINECRAFT_1_16, MINECRAFT_1_16_4, true)); + clientbound.register(TitleSubtitlePacket.class, TitleSubtitlePacket::new, + map(0x57, MINECRAFT_1_17, true)); + clientbound.register(TitleTextPacket.class, TitleTextPacket::new, + map(0x59, MINECRAFT_1_17, true)); + clientbound.register(TitleActionbarPacket.class, TitleActionbarPacket::new, + map(0x41, MINECRAFT_1_17, true)); + clientbound.register(TitleTimesPacket.class, TitleTimesPacket::new, + map(0x5A, MINECRAFT_1_17, true)); + clientbound.register(TitleClearPacket.class, TitleClearPacket::new, + map(0x10, MINECRAFT_1_17, true)); clientbound.register(PlayerListItem.class, PlayerListItem::new, map(0x38, MINECRAFT_1_7_2, false), map(0x2D, MINECRAFT_1_9, false), @@ -243,7 +273,8 @@ public enum StateRegistry { map(0x33, MINECRAFT_1_14, false), map(0x34, MINECRAFT_1_15, false), map(0x33, MINECRAFT_1_16, false), - map(0x32, MINECRAFT_1_16_2, false)); + map(0x32, MINECRAFT_1_16_2, false), + map(0x36, MINECRAFT_1_17, false)); } }, LOGIN { @@ -311,8 +342,20 @@ public enum StateRegistry { for (int i = 0; i < mappings.length; i++) { PacketMapping current = mappings[i]; PacketMapping next = (i + 1 < mappings.length) ? mappings[i + 1] : current; + ProtocolVersion from = current.protocolVersion; - ProtocolVersion to = current == next ? getLast(SUPPORTED_VERSIONS) : next.protocolVersion; + ProtocolVersion lastValid = current.lastValidProtocolVersion; + if (lastValid != null) { + if (next != current) { + throw new IllegalArgumentException("Cannot add a mapping after last valid mapping"); + } + if (from.compareTo(lastValid) > 0) { + throw new IllegalArgumentException( + "Last mapping version cannot be higher than highest mapping version"); + } + } + ProtocolVersion to = current == next ? lastValid != null + ? lastValid : getLast(SUPPORTED_VERSIONS) : next.protocolVersion; if (from.compareTo(to) >= 0 && from != getLast(SUPPORTED_VERSIONS)) { throw new IllegalArgumentException(String.format( @@ -400,10 +443,13 @@ public enum StateRegistry { private final int id; private final ProtocolVersion protocolVersion; private final boolean encodeOnly; + private final @Nullable ProtocolVersion lastValidProtocolVersion; - PacketMapping(int id, ProtocolVersion protocolVersion, boolean packetDecoding) { + PacketMapping(int id, ProtocolVersion protocolVersion, + ProtocolVersion lastValidProtocolVersion, boolean packetDecoding) { this.id = id; this.protocolVersion = protocolVersion; + this.lastValidProtocolVersion = lastValidProtocolVersion; this.encodeOnly = packetDecoding; } @@ -445,7 +491,21 @@ public enum StateRegistry { * @return PacketMapping with the provided arguments */ private static PacketMapping map(int id, ProtocolVersion version, boolean encodeOnly) { - return new PacketMapping(id, version, encodeOnly); + return map(id, version, null, encodeOnly); + } + + /** + * Creates a PacketMapping using the provided arguments. + * + * @param id Packet Id + * @param version Protocol version + * @param encodeOnly When true packet decoding will be disabled + * @param lastValidProtocolVersion Last version this Mapping is valid at + * @return PacketMapping with the provided arguments + */ + private static PacketMapping map(int id, ProtocolVersion version, + ProtocolVersion lastValidProtocolVersion, boolean encodeOnly) { + return new PacketMapping(id, version, lastValidProtocolVersion, encodeOnly); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java index a2d7e32bc..ef3818c99 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java @@ -33,12 +33,13 @@ public class ClientSettings implements MinecraftPacket { private byte difficulty; // 1.7 Protocol private short skinParts; private int mainHand; + private boolean chatFilteringEnabled; // Added in 1.17 public ClientSettings() { } public ClientSettings(String locale, byte viewDistance, int chatVisibility, boolean chatColors, - short skinParts, int mainHand) { + short skinParts, int mainHand, boolean chatFilteringEnabled) { this.locale = locale; this.viewDistance = viewDistance; this.chatVisibility = chatVisibility; @@ -98,6 +99,14 @@ public class ClientSettings implements MinecraftPacket { this.mainHand = mainHand; } + public boolean isChatFilteringEnabled() { + return chatFilteringEnabled; + } + + public void setChatFilteringEnabled(boolean chatFilteringEnabled) { + this.chatFilteringEnabled = chatFilteringEnabled; + } + @Override public String toString() { return "ClientSettings{" @@ -107,6 +116,7 @@ public class ClientSettings implements MinecraftPacket { + ", chatColors=" + chatColors + ", skinParts=" + skinParts + ", mainHand=" + mainHand + + ", chatFilteringEnabled=" + chatFilteringEnabled + '}'; } @@ -125,6 +135,10 @@ public class ClientSettings implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { this.mainHand = ProtocolUtils.readVarInt(buf); + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { + this.chatFilteringEnabled = buf.readBoolean(); + } } } @@ -146,6 +160,10 @@ public class ClientSettings implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { ProtocolUtils.writeVarInt(buf, mainHand); + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { + buf.writeBoolean(chatFilteringEnabled); + } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java index 522ddc2c7..d17505560 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java @@ -23,6 +23,8 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import io.netty.buffer.ByteBuf; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -30,6 +32,8 @@ public class ResourcePackRequest implements MinecraftPacket { private @MonotonicNonNull String url; private @MonotonicNonNull String hash; + private boolean isRequired; // 1.17+ + private @Nullable Component prompt; // 1.17+ public @Nullable String getUrl() { return url; @@ -39,6 +43,10 @@ public class ResourcePackRequest implements MinecraftPacket { this.url = url; } + public boolean isRequired() { + return isRequired; + } + public @Nullable String getHash() { return hash; } @@ -47,10 +55,30 @@ public class ResourcePackRequest implements MinecraftPacket { this.hash = hash; } + public void setRequired(boolean required) { + isRequired = required; + } + + public @Nullable Component getPrompt() { + return prompt; + } + + public void setPrompt(Component prompt) { + this.prompt = prompt; + } + @Override public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { this.url = ProtocolUtils.readString(buf); this.hash = ProtocolUtils.readString(buf); + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { + this.isRequired = buf.readBoolean(); + if (buf.readBoolean()) { + this.prompt = GsonComponentSerializer.gson().deserialize(ProtocolUtils.readString(buf)); + } else { + this.prompt = null; + } + } } @Override @@ -60,6 +88,15 @@ public class ResourcePackRequest implements MinecraftPacket { } ProtocolUtils.writeString(buf, url); ProtocolUtils.writeString(buf, hash); + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { + buf.writeBoolean(isRequired); + if (prompt != null) { + buf.writeBoolean(true); + ProtocolUtils.writeString(buf, GsonComponentSerializer.gson().serialize(prompt)); + } else { + buf.writeBoolean(false); + } + } } @Override @@ -72,6 +109,8 @@ public class ResourcePackRequest implements MinecraftPacket { return "ResourcePackRequest{" + "url='" + url + '\'' + ", hash='" + hash + '\'' + + ", isRequired=" + isRequired + + ", prompt='" + prompt + '\'' + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java index 245e2f501..3269ff0d5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java @@ -38,6 +38,10 @@ public class ResourcePackResponse implements MinecraftPacket { return status; } + public String getHash() { + return hash; + } + @Override public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9_4) <= 0) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TitlePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TitlePacket.java deleted file mode 100644 index 49fe876df..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TitlePacket.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2018 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.packet; - -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; -import org.checkerframework.checker.nullness.qual.Nullable; - -public class TitlePacket implements MinecraftPacket { - - public static final int SET_TITLE = 0; - public static final int SET_SUBTITLE = 1; - public static final int SET_ACTION_BAR = 2; - public static final int SET_TIMES = 3; - public static final int SET_TIMES_OLD = 2; - public static final int HIDE = 4; - public static final int HIDE_OLD = 3; - public static final int RESET = 5; - public static final int RESET_OLD = 4; - - private int action; - private @Nullable String component; - private int fadeIn; - private int stay; - private int fadeOut; - - @Override - public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - throw new UnsupportedOperationException(); // encode only - } - - @Override - public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - ProtocolUtils.writeVarInt(buf, action); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0) { - // 1.11+ shifted the action enum by 1 to handle the action bar - switch (action) { - case SET_TITLE: - case SET_SUBTITLE: - case SET_ACTION_BAR: - if (component == null) { - throw new IllegalStateException("No component found for " + action); - } - ProtocolUtils.writeString(buf, component); - break; - case SET_TIMES: - buf.writeInt(fadeIn); - buf.writeInt(stay); - buf.writeInt(fadeOut); - break; - case HIDE: - case RESET: - break; - default: - throw new UnsupportedOperationException("Unknown action " + action); - } - } else { - switch (action) { - case SET_TITLE: - case SET_SUBTITLE: - if (component == null) { - throw new IllegalStateException("No component found for " + action); - } - ProtocolUtils.writeString(buf, component); - break; - case SET_TIMES_OLD: - buf.writeInt(fadeIn); - buf.writeInt(stay); - buf.writeInt(fadeOut); - break; - case HIDE_OLD: - case RESET_OLD: - break; - default: - throw new UnsupportedOperationException("Unknown action " + action); - } - } - } - - public int getAction() { - return action; - } - - public void setAction(int action) { - this.action = action; - } - - public @Nullable String getComponent() { - return component; - } - - public void setComponent(@Nullable String component) { - this.component = component; - } - - public int getFadeIn() { - return fadeIn; - } - - public void setFadeIn(int fadeIn) { - this.fadeIn = fadeIn; - } - - public int getStay() { - return stay; - } - - public void setStay(int stay) { - this.stay = stay; - } - - public int getFadeOut() { - return fadeOut; - } - - public void setFadeOut(int fadeOut) { - this.fadeOut = fadeOut; - } - - public static TitlePacket hideForProtocolVersion(ProtocolVersion version) { - TitlePacket packet = new TitlePacket(); - packet.setAction(version.compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0 ? TitlePacket.HIDE - : TitlePacket.HIDE_OLD); - return packet; - } - - public static TitlePacket resetForProtocolVersion(ProtocolVersion version) { - TitlePacket packet = new TitlePacket(); - packet.setAction(version.compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0 ? TitlePacket.RESET - : TitlePacket.RESET_OLD); - return packet; - } - - public static TitlePacket timesForProtocolVersion(ProtocolVersion version) { - TitlePacket packet = new TitlePacket(); - packet.setAction(version.compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0 ? TitlePacket.SET_TIMES - : TitlePacket.SET_TIMES_OLD); - return packet; - } - - @Override - public String toString() { - return "TitlePacket{" - + "action=" + action - + ", component='" + component + '\'' - + ", fadeIn=" + fadeIn - + ", stay=" + stay - + ", fadeOut=" + fadeOut - + '}'; - } - - @Override - public boolean handle(MinecraftSessionHandler handler) { - return handler.handle(this); - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/GenericTitlePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/GenericTitlePacket.java new file mode 100644 index 000000000..38c890c03 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/GenericTitlePacket.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.title; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +public abstract class GenericTitlePacket implements MinecraftPacket { + + public enum ActionType { + SET_TITLE(0), + SET_SUBTITLE(1), + SET_ACTION_BAR(2), + SET_TIMES(3), + HIDE(4), + RESET(5); + + private final int action; + + ActionType(int action) { + this.action = action; + } + + public int getAction(ProtocolVersion version) { + return version.compareTo(ProtocolVersion.MINECRAFT_1_11) < 0 + ? action > 2 ? action - 1 : action : action; + } + } + + + private ActionType action; + + protected void setAction(ActionType action) { + this.action = action; + } + + public final ActionType getAction() { + return action; + } + + public String getComponent() { + throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); + } + + public void setComponent(String component) { + throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); + } + + public int getFadeIn() { + throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); + } + + public void setFadeIn(int fadeIn) { + throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); + } + + public int getStay() { + throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); + } + + public void setStay(int stay) { + throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); + } + + public int getFadeOut() { + throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); + } + + public void setFadeOut(int fadeOut) { + throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); + } + + + + @Override + public final void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + throw new UnsupportedOperationException(); // encode only + } + + /** + * Creates a version and type dependent TitlePacket. + * + * @param type Action the packet should invoke + * @param version Protocol version of the target player + * @return GenericTitlePacket instance that follows the invoker type/version + */ + public static GenericTitlePacket constructTitlePacket(ActionType type, ProtocolVersion version) { + GenericTitlePacket packet = null; + if (version.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { + switch (type) { + case SET_ACTION_BAR: + packet = new TitleActionbarPacket(); + break; + case SET_SUBTITLE: + packet = new TitleSubtitlePacket(); + break; + case SET_TIMES: + packet = new TitleTimesPacket(); + break; + case SET_TITLE: + packet = new TitleTextPacket(); + break; + case HIDE: + case RESET: + packet = new TitleClearPacket(); + break; + default: + throw new IllegalArgumentException("Invalid ActionType"); + } + } else { + packet = new LegacyTitlePacket(); + } + packet.setAction(type); + return packet; + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/LegacyTitlePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/LegacyTitlePacket.java new file mode 100644 index 000000000..7b25325e6 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/LegacyTitlePacket.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.title; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class LegacyTitlePacket extends GenericTitlePacket { + + private @Nullable String component; + private int fadeIn; + private int stay; + private int fadeOut; + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_11) < 0 + && getAction() == ActionType.SET_ACTION_BAR) { + throw new IllegalStateException("Action bars are only supported on 1.11 and newer"); + } + ProtocolUtils.writeVarInt(buf, getAction().getAction(version)); + + switch (getAction()) { + case SET_TITLE: + case SET_SUBTITLE: + case SET_ACTION_BAR: + if (component == null) { + throw new IllegalStateException("No component found for " + getAction()); + } + ProtocolUtils.writeString(buf, component); + break; + case SET_TIMES: + buf.writeInt(fadeIn); + buf.writeInt(stay); + buf.writeInt(fadeOut); + break; + case HIDE: + case RESET: + break; + default: + throw new UnsupportedOperationException("Unknown action " + getAction()); + } + + } + + @Override + public void setAction(ActionType action) { + super.setAction(action); + } + + @Override + public @Nullable String getComponent() { + return component; + } + + @Override + public void setComponent(@Nullable String component) { + this.component = component; + } + + @Override + public int getFadeIn() { + return fadeIn; + } + + @Override + public void setFadeIn(int fadeIn) { + this.fadeIn = fadeIn; + } + + @Override + public int getStay() { + return stay; + } + + @Override + public void setStay(int stay) { + this.stay = stay; + } + + @Override + public int getFadeOut() { + return fadeOut; + } + + @Override + public void setFadeOut(int fadeOut) { + this.fadeOut = fadeOut; + } + + @Override + public String toString() { + return "GenericTitlePacket{" + + "action=" + getAction() + + ", component='" + component + '\'' + + ", fadeIn=" + fadeIn + + ", stay=" + stay + + ", fadeOut=" + fadeOut + + '}'; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleActionbarPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleActionbarPacket.java new file mode 100644 index 000000000..e875ae3cd --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleActionbarPacket.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.title; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class TitleActionbarPacket extends GenericTitlePacket { + + private String component; + + public TitleActionbarPacket() { + setAction(ActionType.SET_TITLE); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + ProtocolUtils.writeString(buf, component); + } + + @Override + public String getComponent() { + return component; + } + + @Override + public void setComponent(String component) { + this.component = component; + } + + @Override + public String toString() { + return "TitleActionbarPacket{" + + ", component='" + component + '\'' + + '}'; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleClearPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleClearPacket.java new file mode 100644 index 000000000..626abb517 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleClearPacket.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.title; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class TitleClearPacket extends GenericTitlePacket { + + public TitleClearPacket() { + setAction(ActionType.HIDE); + } + + @Override + public void setAction(ActionType action) { + if (action != ActionType.HIDE && action != ActionType.RESET) { + throw new IllegalArgumentException("TitleClearPacket only accepts CLEAR and RESET actions"); + } + super.setAction(action); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + buf.writeBoolean(getAction() == ActionType.RESET); + } + + @Override + public String toString() { + return "TitleClearPacket{" + + ", resetTimes=" + (getAction() == ActionType.RESET) + + '}'; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleSubtitlePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleSubtitlePacket.java new file mode 100644 index 000000000..1640862f7 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleSubtitlePacket.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.title; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class TitleSubtitlePacket extends GenericTitlePacket { + + private String component; + + public TitleSubtitlePacket() { + setAction(ActionType.SET_SUBTITLE); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + ProtocolUtils.writeString(buf, component); + } + + @Override + public String getComponent() { + return component; + } + + @Override + public void setComponent(String component) { + this.component = component; + } + + @Override + public String toString() { + return "TitleSubtitlePacket{" + + ", component='" + component + '\'' + + '}'; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTextPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTextPacket.java new file mode 100644 index 000000000..135eb27e4 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTextPacket.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.title; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class TitleTextPacket extends GenericTitlePacket { + + private String component; + + public TitleTextPacket() { + setAction(ActionType.SET_TITLE); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + ProtocolUtils.writeString(buf, component); + } + + @Override + public String getComponent() { + return component; + } + + @Override + public void setComponent(String component) { + this.component = component; + } + + @Override + public String toString() { + return "TitleTextPacket{" + + ", component='" + component + '\'' + + '}'; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTimesPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTimesPacket.java new file mode 100644 index 000000000..7450caccd --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTimesPacket.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.title; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class TitleTimesPacket extends GenericTitlePacket { + + private int fadeIn; + private int stay; + private int fadeOut; + + public TitleTimesPacket() { + setAction(ActionType.SET_TIMES); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + buf.writeInt(fadeIn); + buf.writeInt(stay); + buf.writeInt(fadeOut); + } + + @Override + public int getFadeIn() { + return fadeIn; + } + + @Override + public void setFadeIn(int fadeIn) { + this.fadeIn = fadeIn; + } + + @Override + public int getStay() { + return stay; + } + + @Override + public void setStay(int stay) { + this.stay = stay; + } + + @Override + public int getFadeOut() { + return fadeOut; + } + + @Override + public void setFadeOut(int fadeOut) { + this.fadeOut = fadeOut; + } + + @Override + public String toString() { + return "TitleTimesPacket{" + + ", fadeIn=" + fadeIn + + ", stay=" + stay + + ", fadeOut=" + fadeOut + + '}'; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java index 1780d14ac..b2f6e857a 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java @@ -23,7 +23,11 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14_2; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -43,8 +47,9 @@ class PacketRegistryTest { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( ProtocolUtils.Direction.CLIENTBOUND); registry.register(Handshake.class, Handshake::new, - new StateRegistry.PacketMapping(0x01, MINECRAFT_1_8, false), - new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, false)); + new StateRegistry.PacketMapping(0x01, MINECRAFT_1_8, null, false), + new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, null, false), + new StateRegistry.PacketMapping(0x00, MINECRAFT_1_15, MINECRAFT_1_16, false)); return registry; } @@ -73,6 +78,8 @@ class PacketRegistryTest { "Registry did not return the correct packet ID"); assertNull(registry.getProtocolRegistry(MINECRAFT_1_14_2).createPacket(0x01), "Registry should return a null"); + assertNull(registry.getProtocolRegistry(MINECRAFT_1_16_2).createPacket(0), + "Registry should return null"); } @Test @@ -91,12 +98,19 @@ class PacketRegistryTest { ProtocolUtils.Direction.CLIENTBOUND); assertThrows(IllegalArgumentException.class, () -> registry.register(Handshake.class, Handshake::new, - new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, false), - new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, false))); + new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, null, false), + new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, null, false))); assertThrows(IllegalArgumentException.class, () -> registry.register(Handshake.class, Handshake::new, - new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, false), - new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, false))); + new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, null, false), + new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, null, false))); + assertThrows(IllegalArgumentException.class, + () -> registry.register(Handshake.class, Handshake::new, + new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, MINECRAFT_1_8, false))); + assertThrows(IllegalArgumentException.class, + () -> registry.register(Handshake.class, Handshake::new, + new StateRegistry.PacketMapping(0x01, MINECRAFT_1_8, MINECRAFT_1_14, false), + new StateRegistry.PacketMapping(0x00, MINECRAFT_1_16, null, false))); } @Test @@ -104,13 +118,13 @@ class PacketRegistryTest { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( ProtocolUtils.Direction.CLIENTBOUND); registry.register(Handshake.class, Handshake::new, - new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, false)); + new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, null, false)); assertThrows(IllegalArgumentException.class, () -> registry.register(Handshake.class, Handshake::new, - new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12, false))); + new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12, null, false))); assertThrows(IllegalArgumentException.class, () -> registry.register(StatusPing.class, StatusPing::new, - new StateRegistry.PacketMapping(0x00, MINECRAFT_1_13, false))); + new StateRegistry.PacketMapping(0x00, MINECRAFT_1_13, null, false))); } @Test @@ -118,9 +132,9 @@ class PacketRegistryTest { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( ProtocolUtils.Direction.CLIENTBOUND); assertDoesNotThrow(() -> registry.register(Handshake.class, Handshake::new, - new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, false), + new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, null, false), new StateRegistry.PacketMapping(0x01, getLast(ProtocolVersion.SUPPORTED_VERSIONS), - false))); + null, false))); } @Test @@ -128,9 +142,9 @@ class PacketRegistryTest { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( ProtocolUtils.Direction.CLIENTBOUND); registry.register(Handshake.class, Handshake::new, - new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, false), - new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12_1, false), - new StateRegistry.PacketMapping(0x02, MINECRAFT_1_13, false)); + new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, null, false), + new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12_1, null, false), + new StateRegistry.PacketMapping(0x02, MINECRAFT_1_13, null, false)); assertEquals(Handshake.class, registry.getProtocolRegistry(MINECRAFT_1_12).createPacket(0x00).getClass()); assertEquals(Handshake.class,