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..a490b6791 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; + + /** + * Instates 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; + } + + /** + * Instates 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 5d843d88d..10b3595f6 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -54,7 +54,7 @@ public enum ProtocolVersion { 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_17(-1, 21, "1.17"); // Snapshot: 21w14a, future protocol: 755 + MINECRAFT_1_17(-1, 22, "1.17"); // Snapshot: 21w15a, 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 5fb68d4c9..d22f0caf2 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; @@ -25,6 +26,7 @@ import java.util.UUID; import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Represents a player who is connected to the proxy. @@ -215,7 +217,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); /** @@ -225,10 +229,37 @@ 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 */ - default void sendResourcePack(String url, byte[] hash) { - sendResourcePack(url, hash, false); - } + @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}. + * + * @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 + */ + @Nullable + ResourcePackInfo getAppliedResourcePack(); + + /** + * Gets the {@link ResourcePackInfo} of the currently accepted + * and currently downloading resource-pack or null if none. + * + * @return the pending resource pack + */ + @Nullable + ResourcePackInfo getPendingResourcePack(); /** * Note that this method does not send a plugin message to the server the player @@ -241,21 +272,4 @@ public interface Player extends CommandSource, Identified, InboundConnection, */ @Override boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data); - - /** - * Sends the specified resource pack from {@code url} to the user, using the specified 20-byte - * SHA-1 hash. To monitor the status of the sent resource pack, subscribe to - * {@link PlayerResourcePackStatusEvent}. - * In 1.17 and newer you can additionally specify - * whether the resource pack is required or not. Setting this for an older client will have - * no effect. - * - * @param url the URL for the resource pack - * @param hash the SHA-1 hash value for the resource pack - * @param isRequired Only in 1.17+ or newer: If true shows the pack as required to play, - * and removes the decline option. Declining it anyway will disconnect the user. - */ - void sendResourcePack(String url, byte[] hash, boolean isRequired); - } - 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 073cd3bb7..bd3430a13 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; @@ -217,4 +218,24 @@ public interface ProxyServer extends Audience { @NonNull BossBar createBossBar(net.kyori.text.Component title, @NonNull BossBarColor color, @NonNull BossBarOverlay overlay, float progress); + + /** + * 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 Cloudfront + * may cause issues in downloading. + * - Be in location with appropriate bandwidth 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.

+ * + * @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..067dd26ba --- /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 player declined to download the resource pack. + */ + 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 63b3b7550..725e8403b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -30,6 +30,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; @@ -45,6 +46,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.network.ConnectionManager; import com.velocitypowered.proxy.plugin.VelocityEventManager; @@ -682,4 +684,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/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 0c2b402eb..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; @@ -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 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 1a1a0d444..459a7ea26 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.MessagePosition; @@ -55,6 +57,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; @@ -76,12 +79,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; @@ -133,6 +138,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) { @@ -871,31 +880,133 @@ 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.setRequired(false); + 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, boolean isRequired) { - 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)); - request.setRequired(isRequired); - 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) { + CompletableFuture.supplyAsync(() -> { + tickResourcePackQueue(); + return true; + }); + } + + 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/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 213e15222..b56cb8f02 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -226,16 +226,16 @@ public enum StateRegistry { map(0x39, MINECRAFT_1_16_2, true), map(0x3C, 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(0x3B, MINECRAFT_1_17, 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(0x3B, MINECRAFT_1_17, false)); clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), 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 c2115970d..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; @@ -31,6 +33,7 @@ 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; @@ -56,12 +59,25 @@ public class ResourcePackRequest implements MinecraftPacket { 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; + } } } @@ -74,6 +90,12 @@ public class ResourcePackRequest implements MinecraftPacket { 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); + } } } @@ -87,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) {