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 new file mode 100644 index 000000000..0411a8676 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerResourcePackStatusEvent.java @@ -0,0 +1,49 @@ +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.Player; + +public class PlayerResourcePackStatusEvent { + private final Player player; + private final Status result; + + public PlayerResourcePackStatusEvent(Player player, Status result) { + this.player = Preconditions.checkNotNull(player, "player"); + this.result = Preconditions.checkNotNull(result, "result"); + } + + public Player getPlayer() { + return player; + } + + public Status getResult() { + return result; + } + + @Override + public String toString() { + return "PlayerResourcePackStatusEvent{" + + "player=" + player + + ", result=" + result + + '}'; + } + + public enum Status { + /** + * The resource pack was applied successfully. + */ + SUCCESSFUL, + /** + * The player declined to download the resource pack. + */ + DECLINED, + /** + * The player could not download the resource pack. + */ + FAILED_DOWNLOAD, + /** + * The player has accepted the resource pack and is now downloading it. + */ + ACCEPTED + } +} 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 ce8a8c4b1..24f330943 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -151,4 +151,20 @@ public interface Player extends CommandSource, InboundConnection, ChannelMessage * @param input the chat input to send */ void spoofChatInput(String input); + + /** + * Sends the specified resource pack from {@code url} to the user. If at all possible, send the + * resource pack using {@link #sendResourcePack(String, byte[])}. + * + * @param url the URL for the resource pack + */ + void sendResourcePack(String url); + + /** + * Sends the specified resource pack from {@code url} to the user. + * + * @param url the URL for the resource pack + * @param hash the SHA-1 hash value for the resource pack + */ + void sendResourcePack(String url, byte[] hash); } 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 13bfcf4a2..2789e17f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -18,6 +18,8 @@ import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; 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.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.Respawn; import com.velocitypowered.proxy.protocol.packet.ServerLogin; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; @@ -175,4 +177,12 @@ public interface MinecraftSessionHandler { default boolean handle(PlayerListItem packet) { return false; } + + default boolean handle(ResourcePackRequest packet) { + return false; + } + + default boolean handle(ResourcePackResponse packet) { + return false; + } } 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 5795310d4..cb8cd4618 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 @@ -4,6 +4,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; @@ -18,6 +19,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.Respawn; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; @@ -235,6 +237,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return true; } + @Override + public boolean handle(ResourcePackResponse packet) { + server.getEventManager().fireAndForget(new PlayerResourcePackStatusEvent(player, + packet.getStatus())); + return false; + } + @Override public void handleGeneric(MinecraftPacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); 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 f6f210964..8e73c05ec 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 @@ -38,9 +38,11 @@ import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; 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.server.VelocityRegisteredServer; import com.velocitypowered.proxy.tablist.VelocityTabList; +import io.netty.buffer.ByteBufUtil; import java.net.InetSocketAddress; import java.util.Collections; import java.util.List; @@ -467,6 +469,28 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { ensureBackendConnection().write(Chat.createServerbound(input)); } + @Override + public void sendResourcePack(String url) { + Preconditions.checkNotNull(url, "url"); + + ResourcePackRequest request = new ResourcePackRequest(); + request.setUrl(url); + request.setHash(""); + 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"); + + ResourcePackRequest request = new ResourcePackRequest(); + request.setUrl(url); + request.setHash(ByteBufUtil.hexDump(hash)); + connection.write(request); + } + /** * Sends a {@link KeepAlive} packet to the player with a random ID. * The response will be ignored by Velocity as it will not match the 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 d44414085..387884666 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -17,7 +17,6 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4; import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; -import com.google.common.collect.ImmutableList; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; @@ -34,6 +33,8 @@ import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; 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.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.Respawn; import com.velocitypowered.proxy.protocol.packet.ServerLogin; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; @@ -110,6 +111,11 @@ public enum StateRegistry { map(0x0C, MINECRAFT_1_12, false), map(0x0B, MINECRAFT_1_12_1, false), map(0x0E, MINECRAFT_1_13, false)); + serverbound.register(ResourcePackResponse.class, ResourcePackResponse::new, + map(0x19, MINECRAFT_1_8, false), + map(0x16, MINECRAFT_1_9, false), + map(0x18, MINECRAFT_1_12, false), + map(0x1D, MINECRAFT_1_13, false)); clientbound.register(BossBar.class, BossBar::new, map(0x0C, MINECRAFT_1_9, false), @@ -153,6 +159,12 @@ public enum StateRegistry { map(0x34, MINECRAFT_1_12, true), map(0x35, MINECRAFT_1_12_2, true), map(0x38, MINECRAFT_1_13, 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)); 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 new file mode 100644 index 000000000..5170e3b9f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java @@ -0,0 +1,64 @@ +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 com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ResourcePackRequest implements MinecraftPacket { + + @MonotonicNonNull + private String url; + @MonotonicNonNull + private String hash; + + @Nullable + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Nullable + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + @Override + public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + this.url = ProtocolUtils.readString(buf); + this.hash = ProtocolUtils.readString(buf); + } + + @Override + public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + if (url == null || hash == null) { + throw new IllegalStateException("Packet not fully filled in yet!"); + } + ProtocolUtils.writeString(buf, url); + ProtocolUtils.writeString(buf, hash); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + @Override + public String toString() { + return "ResourcePackRequest{" + + "url='" + url + '\'' + + ", hash='" + hash + '\'' + + '}'; + } +} 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 new file mode 100644 index 000000000..a37af73fe --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java @@ -0,0 +1,45 @@ +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent.Status; +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 com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +public class ResourcePackResponse implements MinecraftPacket { + + @MonotonicNonNull + private Status status; + + public Status getStatus() { + if (status == null) { + throw new IllegalStateException("Packet not yet deserialized"); + } + return status; + } + + @Override + public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + this.status = Status.values()[ProtocolUtils.readVarInt(buf)]; + } + + @Override + public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeVarInt(buf, status.ordinal()); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + @Override + public String toString() { + return "ResourcePackResponse{" + + "status=" + status + + '}'; + } +}