From cbd07b14349c449202e84a840da1167b42487cca Mon Sep 17 00:00:00 2001 From: Adrian <68704415+4drian3d@users.noreply.github.com> Date: Thu, 8 Feb 2024 08:51:45 -0500 Subject: [PATCH] Refactor ResourcePacks handling (#1225) * Initial ResourcePack refactor * Implement sendResourcePacks method * Initializes the ResourcePackHandler at player initialization * Move adventure to velocity resource pack conversion to the same class * Added some internal resource pack documentation * Refactored Modern ResourcePack handling * Handle RemoveResourcePackPacket from backend server * Fixed license * Use removeIf instead of manual iteration * Improve ModernResourcePackHandler * fix hash conversion * bundle resource packs * keep old constructors of PlayerResourcePackStatusEvent * add @Nullable to PlayerResourcePackStatusEvent#getPackId * Use a single instance of BundleDelimiterPacket * Throw UnSupportedOperationException on operations not supported by LegacyResourcePackHandler * Use a single instance on empty packets * Handle active packet bundle sending from backend server in case of sending a packet bundle of resource packs * Improve packet bundling * Fixed login for players with version 1.20.2 --------- Co-authored-by: Gero --- .../player/PlayerResourcePackStatusEvent.java | 33 ++- .../api/proxy/player/ResourcePackInfo.java | 3 +- .../connection/MinecraftSessionHandler.java | 5 + .../backend/BackendPlaySessionHandler.java | 23 +- .../backend/ConfigSessionHandler.java | 13 +- .../connection/client/AuthSessionHandler.java | 16 +- .../client/BundleDelimiterHandler.java | 94 +++++++ .../client/ClientConfigSessionHandler.java | 19 +- .../client/ClientPlaySessionHandler.java | 11 +- .../connection/client/ConnectedPlayer.java | 246 +++++------------- .../client/HandshakeSessionHandler.java | 53 ++-- .../client/InitialInboundConnection.java | 4 +- .../client/StatusSessionHandler.java | 2 +- .../player/VelocityResourcePackInfo.java | 40 ++- .../Legacy117ResourcePackHandler.java | 36 +++ .../LegacyResourcePackHandler.java | 176 +++++++++++++ .../ModernResourcePackHandler.java | 176 +++++++++++++ .../resourcepack/ResourcePackHandler.java | 142 ++++++++++ .../ResourcePackResponseBundle.java | 25 ++ .../proxy/crypto/IdentifiedKeyImpl.java | 6 +- .../proxy/protocol/StateRegistry.java | 13 +- .../protocol/netty/LegacyPingEncoder.java | 2 +- .../packet/BundleDelimiterPacket.java | 46 ++++ .../protocol/packet/LegacyDisconnect.java | 35 +-- .../packet/RemoveResourcePackPacket.java | 5 +- .../packet/ResourcePackRequestPacket.java | 9 +- .../packet/ResourcePackResponsePacket.java | 4 + .../packet/config/FinishedUpdatePacket.java | 4 + .../packet/config/StartUpdatePacket.java | 4 + .../proxy/util/TranslatableMapper.java | 7 +- 30 files changed, 945 insertions(+), 307 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackResponseBundle.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/BundleDelimiterPacket.java 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 95640596e..d212666b7 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 @@ -12,6 +12,7 @@ import com.velocitypowered.api.event.annotation.AwaitingEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import java.util.UUID; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -24,6 +25,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class PlayerResourcePackStatusEvent { private final Player player; + private final @MonotonicNonNull UUID packId; private final Status status; private final @MonotonicNonNull ResourcePackInfo packInfo; private boolean overwriteKick; @@ -32,20 +34,31 @@ public class PlayerResourcePackStatusEvent { * Instantiates this event. * * @deprecated Use {@link PlayerResourcePackStatusEvent#PlayerResourcePackStatusEvent - * (Player, Status, ResourcePackInfo)} instead. + * (Player, UUID, 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; + this(player, null, status, null); + } + + /** + * Instantiates this event. + * + * @deprecated Use {@link PlayerResourcePackStatusEvent#PlayerResourcePackStatusEvent + * (Player, UUID, Status, ResourcePackInfo)} instead. + */ + @Deprecated + public PlayerResourcePackStatusEvent(Player player, Status status, ResourcePackInfo packInfo) { + this(player, null, status, packInfo); } /** * Instantiates this event. */ - public PlayerResourcePackStatusEvent(Player player, Status status, ResourcePackInfo packInfo) { + public PlayerResourcePackStatusEvent( + Player player, UUID packId, Status status, ResourcePackInfo packInfo) { this.player = Preconditions.checkNotNull(player, "player"); + this.packId = packId == null ? packInfo == null ? null : packInfo.getId() : packId; this.status = Preconditions.checkNotNull(status, "status"); this.packInfo = packInfo; } @@ -59,6 +72,16 @@ public class PlayerResourcePackStatusEvent { return player; } + /** + * Returns the id of the resource pack. + * + * @return the id + */ + @Nullable + public UUID getPackId() { + return packId; + } + /** * Returns the new status for the resource pack. * 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 index 6c3d44591..8f4b0d2ef 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/ResourcePackInfo.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/ResourcePackInfo.java @@ -8,13 +8,14 @@ package com.velocitypowered.api.proxy.player; import java.util.UUID; +import net.kyori.adventure.resource.ResourcePackRequestLike; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; /** * Represents the information for a resource pack to apply that can be sent to the client. */ -public interface ResourcePackInfo { +public interface ResourcePackInfo extends ResourcePackRequestLike { /** * Gets the id of this resource-pack. 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 94db57917..ad6b6db17 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -20,6 +20,7 @@ package com.velocitypowered.proxy.connection; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket; import com.velocitypowered.proxy.protocol.packet.BossBarPacket; +import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; @@ -324,4 +325,8 @@ public interface MinecraftSessionHandler { default boolean handle(ChatAcknowledgementPacket chatAcknowledgement) { return false; } + + default boolean handle(BundleDelimiterPacket bundleDelimiterPacket) { + 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 0b224536a..8724cbad5 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 @@ -35,13 +35,16 @@ import com.velocitypowered.proxy.command.CommandGraphInjector; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket; import com.velocitypowered.proxy.protocol.packet.BossBarPacket; +import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; @@ -125,6 +128,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return false; } + @Override + public boolean handle(BundleDelimiterPacket bundleDelimiterPacket) { + serverConn.getPlayer().getBundleHandler().toggleBundleSession(); + return false; + } + @Override public boolean handle(StartUpdatePacket packet) { MinecraftConnection smc = serverConn.ensureConnected(); @@ -188,13 +197,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return; } if (serverResourcePackSendEvent.getResult().isAllowed()) { - ResourcePackInfo toSend = serverResourcePackSendEvent.getProvidedResourcePack(); + final ResourcePackInfo toSend = serverResourcePackSendEvent.getProvidedResourcePack(); if (toSend != serverResourcePackSendEvent.getReceivedResourcePack()) { ((VelocityResourcePackInfo) toSend) .setOriginalOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); } - serverConn.getPlayer().queueResourcePack(toSend); + serverConn.getPlayer().resourcePackHandler().queueResourcePack(toSend); } else if (serverConn.getConnection() != null) { serverConn.getConnection().write(new ResourcePackResponsePacket( packet.getId(), @@ -219,7 +228,15 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(RemoveResourcePackPacket packet) { - return false; //TODO + final ConnectedPlayer player = serverConn.getPlayer(); + final ResourcePackHandler handler = player.resourcePackHandler(); + if (packet.getId() != null) { + handler.remove(packet.getId()); + } else { + handler.clearAppliedResourcePacks(); + } + playerConnection.write(packet); + return true; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java index ce3370b3d..421063ade 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -84,8 +84,8 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { public void activated() { ConnectedPlayer player = serverConn.getPlayer(); if (player.getProtocolVersion() == ProtocolVersion.MINECRAFT_1_20_2) { - resourcePackToApply = player.getAppliedResourcePack(); - player.clearAppliedResourcePack(); + resourcePackToApply = player.resourcePackHandler().getFirstAppliedPack(); + player.resourcePackHandler().clearAppliedResourcePacks(); } } @@ -136,7 +136,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { } resourcePackToApply = null; - serverConn.getPlayer().queueResourcePack(toSend); + serverConn.getPlayer().resourcePackHandler().queueResourcePack(toSend); } else if (serverConn.getConnection() != null) { serverConn.getConnection().write(new ResourcePackResponsePacket( packet.getId(), packet.getHash(), PlayerResourcePackStatusEvent.Status.DECLINED)); @@ -174,8 +174,9 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } - if (player.getAppliedResourcePack() == null && resourcePackToApply != null) { - player.queueResourcePack(resourcePackToApply); + if (player.resourcePackHandler().getFirstAppliedPack() == null + && resourcePackToApply != null) { + player.resourcePackHandler().queueResourcePack(resourcePackToApply); } smc.setAutoReading(true); }, smc.eventLoop()); @@ -228,7 +229,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { /** * Represents the state of the configuration stage. */ - public static enum State { + public enum State { START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE } } \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index d8339f62f..4315edf7a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -142,10 +142,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } if (player.getIdentifiedKey() != null) { - IdentifiedKey playerKey = player.getIdentifiedKey(); + final IdentifiedKey playerKey = player.getIdentifiedKey(); if (playerKey.getSignatureHolder() == null) { - if (playerKey instanceof IdentifiedKeyImpl) { - IdentifiedKeyImpl unlinkedKey = (IdentifiedKeyImpl) playerKey; + if (playerKey instanceof IdentifiedKeyImpl unlinkedKey) { // Failsafe if (!unlinkedKey.internalAddHolder(player.getUniqueId())) { if (onlineMode) { @@ -153,7 +152,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { Component.translatable("multiplayer.disconnect.invalid_public_key")); return; } else { - logger.warn("Key for player " + player.getUsername() + " could not be verified!"); + logger.warn("Key for player {} could not be verified!", player.getUsername()); } } } else { @@ -161,8 +160,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } } else { if (!Objects.equals(playerKey.getSignatureHolder(), playerUniqueId)) { - logger.warn("UUID for Player " + player.getUsername() + " mismatches! " - + "Chat/Commands signatures will not work correctly for this player!"); + logger.warn("UUID for Player {} mismatches! " + + "Chat/Commands signatures will not work correctly for this player!", + player.getUsername()); } } } @@ -240,7 +240,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { return server.getEventManager().fire(event).thenRunAsync(() -> { Optional toTry = event.getInitialServer(); - if (!toTry.isPresent()) { + if (toTry.isEmpty()) { player.disconnect0( Component.translatable("velocity.error.no-available-servers", NamedTextColor.RED), true); @@ -263,7 +263,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { this.inbound.cleanup(); } - static enum State { + enum State { START, SUCCESS_SENT, ACKNOWLEDGED } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java new file mode 100644 index 000000000..e92d10587 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 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.client; + +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; +import java.util.concurrent.CompletableFuture; + +/** + * BundleDelimiterHandler. + */ +public final class BundleDelimiterHandler { + private final ConnectedPlayer player; + private boolean inBundleSession = false; + private CompletableFuture finishedBundleSessionFuture; + + public BundleDelimiterHandler(ConnectedPlayer player) { + this.player = player; + } + + public boolean isInBundleSession() { + return this.inBundleSession; + } + + /** + * Toggles the player to be in the process of receiving multiple packets + * from the backend server via a packet bundle. + */ + public void toggleBundleSession() { + if (this.inBundleSession) { + this.finishedBundleSessionFuture.complete(null); + this.finishedBundleSessionFuture = null; + } else { + this.finishedBundleSessionFuture = new CompletableFuture<>(); + } + this.inBundleSession = !this.inBundleSession; + } + + /** + * Bundles all packets sent in the given Runnable. + */ + public CompletableFuture bundlePackets(final Runnable sendPackets) { + VelocityServerConnection connectedServer = player.getConnectedServer(); + MinecraftConnection connection = connectedServer == null + ? null : connectedServer.getConnection(); + if (connection == null) { + sendPackets(sendPackets); + return CompletableFuture.completedFuture(null); + } + CompletableFuture future = new CompletableFuture<>(); + connection.eventLoop().execute(() -> { + if (inBundleSession) { + finishedBundleSessionFuture.thenRun(() -> { + sendPackets(sendPackets); + future.complete(null); + }); + } else { + if (connection.getState() == StateRegistry.PLAY) { + sendPackets(sendPackets); + } else { + sendPackets.run(); + } + future.complete(null); + } + }); + return future; + } + + private void sendPackets(Runnable sendPackets) { + player.getConnection().write(BundleDelimiterPacket.INSTANCE); + try { + sendPackets.run(); + } finally { + player.getConnection().write(BundleDelimiterPacket.INSTANCE); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java index 759e2775f..32ce19c0d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -22,6 +22,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; @@ -70,17 +71,13 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { configSwitchFuture = new CompletableFuture<>(); } - @Override - public void deactivated() { - } - @Override public boolean handle(KeepAlivePacket packet) { - VelocityServerConnection serverConnection = player.getConnectedServer(); + final VelocityServerConnection serverConnection = player.getConnectedServer(); if (serverConnection != null) { - Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); if (sentTime != null) { - MinecraftConnection smc = serverConnection.getConnection(); + final MinecraftConnection smc = serverConnection.getConnection(); if (smc != null) { player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); smc.write(packet); @@ -101,7 +98,9 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { if (player.getConnectionInFlight() != null) { player.getConnectionInFlight().ensureConnected().write(packet); } - return player.onResourcePackResponse(packet.getStatus()); + return player.resourcePackHandler().onResourcePackResponse( + new ResourcePackResponseBundle(packet.getId(), packet.getStatus()) + ); } @Override @@ -196,9 +195,9 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { smc.write(brandPacket); } - player.getConnection().write(new FinishedUpdatePacket()); + player.getConnection().write(FinishedUpdatePacket.INSTANCE); - smc.write(new FinishedUpdatePacket()); + smc.write(FinishedUpdatePacket.INSTANCE); smc.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY); return configSwitchFuture; 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 13c3fed1a..cd2db3546 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 @@ -38,6 +38,7 @@ import com.velocitypowered.proxy.connection.backend.BackendConnectionPhases; import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.BossBarPacket; @@ -172,11 +173,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(KeepAlivePacket packet) { - VelocityServerConnection serverConnection = player.getConnectedServer(); + final VelocityServerConnection serverConnection = player.getConnectedServer(); if (serverConnection != null) { - Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); if (sentTime != null) { - MinecraftConnection smc = serverConnection.getConnection(); + final MinecraftConnection smc = serverConnection.getConnection(); if (smc != null) { player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); smc.write(packet); @@ -204,7 +205,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return true; } - if (!updateTimeKeeper(packet.getTimeStamp())) { return true; } @@ -392,7 +392,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ResourcePackResponsePacket packet) { - return player.onResourcePackResponse(packet.getStatus()); + return player.resourcePackHandler().onResourcePackResponse( + new ResourcePackResponseBundle(packet.getId(), packet.getStatus())); } @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 90b1953b7..5963678ad 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,7 +31,6 @@ 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; @@ -55,6 +54,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.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; @@ -66,7 +66,6 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket; -import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import com.velocitypowered.proxy.protocol.packet.chat.ChatType; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; @@ -82,19 +81,14 @@ import com.velocitypowered.proxy.tablist.VelocityTabListLegacy; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.DurationUtils; import com.velocitypowered.proxy.util.TranslatableMapper; -import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -129,7 +123,6 @@ import org.jetbrains.annotations.NotNull; public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable, VelocityInboundConnection { - private static final int MAX_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; @@ -159,14 +152,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private ClientConnectionPhase connectionPhase; 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; - private @NotNull List pendingResourcePacks = new ArrayList<>(); - private @NotNull List appliedResourcePacks = new ArrayList<>(); + private final ResourcePackHandler resourcePackHandler; + private final BundleDelimiterHandler bundleHandler = new BundleDelimiterHandler(this); + private final @NotNull Pointers pointers = - Player.super.pointers().toBuilder().withDynamic(Identity.UUID, this::getUniqueId) + Player.super.pointers().toBuilder() + .withDynamic(Identity.UUID, this::getUniqueId) .withDynamic(Identity.NAME, this::getUsername) .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) .withDynamic(Identity.LOCALE, this::getEffectiveLocale) @@ -174,7 +165,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, .withStatic(FacetPointers.TYPE, Type.PLAYER).build(); private @Nullable String clientBrand; private @Nullable Locale effectiveLocale; - private @Nullable IdentifiedKey playerKey; + private final @Nullable IdentifiedKey playerKey; private @Nullable ClientSettingsPacket clientSettingsPacket; private final ChatQueue chatQueue; private final ChatBuilderFactory chatBuilderFactory; @@ -200,6 +191,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.playerKey = playerKey; this.chatQueue = new ChatQueue(this); this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion()); + this.resourcePackHandler = ResourcePackHandler.create(this, server); } /** @@ -219,6 +211,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return chatQueue; } + public BundleDelimiterHandler getBundleHandler() { + return this.bundleHandler; + } + @Override public @NonNull Identity identity() { return this.identity; @@ -238,7 +234,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } @Override - public void setEffectiveLocale(Locale locale) { + public void setEffectiveLocale(final @Nullable Locale locale) { effectiveLocale = locale; } @@ -293,6 +289,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return settings == null ? ClientSettingsWrapper.DEFAULT : this.settings; } + @Nullable public ClientSettingsPacket getClientSettingsPacket() { return clientSettingsPacket; } @@ -367,7 +364,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, @Override public void sendMessage(@NonNull Identity identity, @NonNull Component message) { - Component translated = translateMessage(message); + final Component translated = translateMessage(message); connection.write(getChatBuilderFactory().builder() .component(translated).forIdentity(identity).toClient()); @@ -432,7 +429,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } @Override - public void sendPlayerListHeaderAndFooter(final Component header, final Component footer) { + public void sendPlayerListHeaderAndFooter(final @NotNull Component header, + final @NotNull Component footer) { Component translatedHeader = translateMessage(header); Component translatedFooter = translateMessage(footer); this.playerListHeader = translatedHeader; @@ -472,6 +470,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } } + @SuppressWarnings("ConstantValue") @Override public void sendTitlePart(@NotNull TitlePart part, @NotNull T value) { if (part == null) { @@ -744,11 +743,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return; } - if (event.getResult() instanceof DisconnectPlayer) { - DisconnectPlayer res = (DisconnectPlayer) event.getResult(); + if (event.getResult() instanceof final DisconnectPlayer res) { disconnect(res.getReasonComponent()); - } else if (event.getResult() instanceof RedirectPlayer) { - RedirectPlayer res = (RedirectPlayer) event.getResult(); + } else if (event.getResult() instanceof final RedirectPlayer res) { createConnectionRequest(res.getServer(), previousConnection).connect() .whenCompleteAsync((status, throwable) -> { if (throwable != null) { @@ -794,8 +791,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, break; } }, connection.eventLoop()); - } else if (event.getResult() instanceof Notify) { - Notify res = (Notify) event.getResult(); + } else if (event.getResult() instanceof final Notify res) { if (event.kickedDuringServerConnect() && previousConnection != null) { sendMessage(Identity.nil(), res.getMessageComponent()); } else { @@ -906,7 +902,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, DisconnectEvent.LoginStatus status; if (connectedPlayer.isPresent()) { - if (!connectedPlayer.get().getCurrentServer().isPresent()) { + if (connectedPlayer.get().getCurrentServer().isEmpty()) { status = LoginStatus.PRE_SERVER_JOIN; } else { status = connectedPlayer.get() == this ? LoginStatus.SUCCESSFUL_LOGIN @@ -933,9 +929,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, @Override public String toString() { - boolean isPlayerAddressLoggingEnabled = server.getConfiguration() + final boolean isPlayerAddressLoggingEnabled = server.getConfiguration() .isPlayerAddressLoggingEnabled(); - String playerIp = + final String playerIp = isPlayerAddressLoggingEnabled ? getRemoteAddress().toString() : ""; return "[connected player] " + profile.getName() + " (" + playerIp + ")"; } @@ -956,11 +952,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } @Override + @Nullable public String getClientBrand() { return clientBrand; } - void setClientBrand(String clientBrand) { + void setClientBrand(final @Nullable String clientBrand) { this.clientBrand = clientBrand; } @@ -981,6 +978,15 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } } + /** + * Get the ResourcePackHandler corresponding to the player's version. + * + * @return the ResourcePackHandler of this player + */ + public ResourcePackHandler resourcePackHandler() { + return this.resourcePackHandler; + } + @Override @Deprecated public void sendResourcePack(String url) { @@ -997,7 +1003,15 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, public void sendResourcePackOffer(ResourcePackInfo packInfo) { if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) { Preconditions.checkNotNull(packInfo, "packInfo"); - queueResourcePack(packInfo); + this.resourcePackHandler.queueResourcePack(packInfo); + } + } + + @Override + public void sendResourcePacks(@NotNull ResourcePackRequest request) { + if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) { + Preconditions.checkNotNull(request, "packRequest"); + this.resourcePackHandler.queueResourcePack(request); } } @@ -1005,15 +1019,21 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, public void clearResourcePacks() { if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) { connection.write(new RemoveResourcePackPacket()); + this.resourcePackHandler.clearAppliedResourcePacks(); } } @Override public void removeResourcePacks(@NotNull UUID id, @NotNull UUID @NotNull ... others) { if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) { - connection.write(new RemoveResourcePackPacket(id)); + Preconditions.checkNotNull(id, "packUUID"); + if (this.resourcePackHandler.remove(id)) { + connection.write(new RemoveResourcePackPacket(id)); + } for (final UUID other : others) { - connection.write(new RemoveResourcePackPacket(other)); + if (this.resourcePackHandler.remove(other)) { + connection.write(new RemoveResourcePackPacket(other)); + } } } } @@ -1039,174 +1059,26 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } } - /** - * 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() - .noLessThan(ProtocolVersion.MINECRAFT_1_17)) { - break; - } - onResourcePackResponse(PlayerResourcePackStatusEvent.Status.DECLINED); - queued = null; - } - if (queued == null) { - // Exit as the queue was cleared - return; - } - } - - ResourcePackRequestPacket request = new ResourcePackRequestPacket(); - request.setId(queued.getId()); - 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() == null ? null : - new ComponentHolder(getProtocolVersion(), queued.getPrompt())); - - connection.write(request); - } - } - @Override @Deprecated public @Nullable ResourcePackInfo getAppliedResourcePack() { - return appliedResourcePack; + return this.resourcePackHandler.getFirstAppliedPack(); } @Override @Deprecated public @Nullable ResourcePackInfo getPendingResourcePack() { - return pendingResourcePack; + return this.resourcePackHandler.getFirstPendingPack(); } @Override public Collection getAppliedResourcePacks() { - return new ArrayList<>(appliedResourcePacks); + return this.resourcePackHandler.getAppliedResourcePacks(); } @Override public Collection getPendingResourcePacks() { - return new ArrayList<>(pendingResourcePacks); - } - - /** - * Clears the applied resource pack field. - */ - public void clearAppliedResourcePack() { - appliedResourcePack = null; - } - - /** - * Processes a client response to a sent resource-pack. - */ - public boolean onResourcePackResponse(PlayerResourcePackStatusEvent.Status status) { - final boolean peek = status.isIntermediate(); - 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().noLessThan(ProtocolVersion.MINECRAFT_1_17)) - ) { - event.getPlayer().disconnect(Component - .translatable("multiplayer.requiredTexturePrompt.disconnect")); - } - }); - - switch (status) { - case ACCEPTED: - previousResourceResponse = true; - pendingResourcePack = queued; - if (this.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_3)) { - pendingResourcePacks.clear(); - } - pendingResourcePacks.add(queued); - break; - case DECLINED: - previousResourceResponse = false; - break; - case SUCCESSFUL: - appliedResourcePack = queued; - pendingResourcePack = null; - appliedResourcePacks.add(queued); - if (this.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_3)) { - pendingResourcePacks.clear(); - } - if (queued != null) { - pendingResourcePacks.removeIf(resourcePackInfo -> { - if (resourcePackInfo.getId() == null) { - return resourcePackInfo.getUrl().equals(queued.getUrl()) - && Arrays.equals(resourcePackInfo.getHash(), queued.getHash()); - } - return resourcePackInfo.getId().equals(queued.getId()); - }); - } - break; - case FAILED_DOWNLOAD: - pendingResourcePack = null; - if (this.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_3)) { - pendingResourcePacks.clear(); - } - if (queued != null) { - pendingResourcePacks.removeIf(resourcePackInfo -> { - if (resourcePackInfo.getId() == null) { - return resourcePackInfo.getUrl().equals(queued.getUrl()) - && Arrays.equals(resourcePackInfo.getHash(), queued.getHash()); - } - return resourcePackInfo.getId().equals(queued.getId()); - }); - } - break; - case DISCARDED: - if (queued != null && queued.getId() != null) { - appliedResourcePacks.removeIf(resourcePackInfo -> { - return queued.getId().equals(resourcePackInfo.getId()); - }); - } - break; - default: - break; - } - - if (!peek) { - connection.eventLoop().execute(this::tickResourcePackQueue); - } - - return queued != null - && queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER; - } - - /** - * Gives an indication about the previous resource pack responses. - */ - public @Nullable Boolean getPreviousResourceResponse() { - //TODO can probably be removed - return previousResourceResponse; + return this.resourcePackHandler.getPendingResourcePacks(); } /** @@ -1228,7 +1100,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, */ public void switchToConfigState() { CompletableFuture.runAsync(() -> { - connection.write(new StartUpdatePacket()); + connection.write(StartUpdatePacket.INSTANCE); connection.getChannel().pipeline() .get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); // Make sure we don't send any play packets to the player after update start @@ -1271,7 +1143,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } } - private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { + private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { private final RegisteredServer toConnect; private final @Nullable VelocityRegisteredServer previousServer; @@ -1315,7 +1187,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, new ServerPreConnectEvent(ConnectedPlayer.this, toConnect, previousServer); return server.getEventManager().fire(event).thenComposeAsync(newEvent -> { Optional newDest = newEvent.getResult().getServer(); - if (!newDest.isPresent()) { + if (newDest.isEmpty()) { return completedFuture( plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED, toConnect)); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index a38e3c03a..8d497d7e1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -67,7 +67,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(LegacyPingPacket packet) { connection.setProtocolVersion(ProtocolVersion.LEGACY); - StatusSessionHandler handler = + final StatusSessionHandler handler = new StatusSessionHandler(server, new LegacyInboundConnection(connection, packet)); connection.setActiveSessionHandler(StateRegistry.STATUS, handler); handler.handle(packet); @@ -85,9 +85,9 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(HandshakePacket handshake) { - InitialInboundConnection ic = new InitialInboundConnection(connection, + final InitialInboundConnection ic = new InitialInboundConnection(connection, cleanVhost(handshake.getServerAddress()), handshake); - StateRegistry nextState = getStateForProtocol(handshake.getNextStatus()); + final StateRegistry nextState = getStateForProtocol(handshake.getNextStatus()); if (nextState == null) { LOGGER.error("{} provided invalid protocol {}", ic, handshake.getNextStatus()); connection.close(true); @@ -96,14 +96,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { connection.setAssociation(ic); switch (nextState) { - case STATUS: - connection.setActiveSessionHandler(StateRegistry.STATUS, + case STATUS -> connection.setActiveSessionHandler(StateRegistry.STATUS, new StatusSessionHandler(server, ic)); - break; - case LOGIN: - this.handleLogin(handshake, ic); - break; - default: + case LOGIN -> this.handleLogin(handshake, ic); + default -> // If you get this, it's a bug in Velocity. throw new AssertionError("getStateForProtocol provided invalid state!"); } @@ -113,24 +109,23 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { } private static @Nullable StateRegistry getStateForProtocol(int status) { - switch (status) { - case StateRegistry.STATUS_ID: - return StateRegistry.STATUS; - case StateRegistry.LOGIN_ID: - return StateRegistry.LOGIN; - default: - return null; - } + return switch (status) { + case StateRegistry.STATUS_ID -> StateRegistry.STATUS; + case StateRegistry.LOGIN_ID -> StateRegistry.LOGIN; + default -> null; + }; } private void handleLogin(HandshakePacket handshake, InitialInboundConnection ic) { if (!ProtocolVersion.isSupported(handshake.getProtocolVersion())) { - ic.disconnectQuietly(Component.translatable("multiplayer.disconnect.outdated_client") - .args(Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING))); + ic.disconnectQuietly(Component.translatable() + .key("multiplayer.disconnect.outdated_client") + .arguments(Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING)) + .build()); return; } - InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress(); + final InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress(); if (!server.getIpAttemptLimiter().attempt(address)) { // Bump connection into correct protocol state so that we can send the disconnect packet. connection.setState(StateRegistry.LOGIN); @@ -151,7 +146,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { return; } - LoginInboundConnection lic = new LoginInboundConnection(ic); + final LoginInboundConnection lic = new LoginInboundConnection(ic); server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(lic)); connection.setActiveSessionHandler(StateRegistry.LOGIN, new InitialLoginSessionHandler(server, connection, lic)); @@ -213,16 +208,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { connection.close(true); } - private static class LegacyInboundConnection implements VelocityInboundConnection { - - private final MinecraftConnection connection; - private final LegacyPingPacket ping; - - private LegacyInboundConnection(MinecraftConnection connection, - LegacyPingPacket ping) { - this.connection = connection; - this.ping = ping; - } + private record LegacyInboundConnection( + MinecraftConnection connection, + LegacyPingPacket ping + ) implements VelocityInboundConnection { @Override public InetSocketAddress getRemoteAddress() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java index bb57d5629..987befc67 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java @@ -74,9 +74,9 @@ public final class InitialInboundConnection implements VelocityInboundConnection @Override public String toString() { - boolean isPlayerAddressLoggingEnabled = connection.server.getConfiguration() + final boolean isPlayerAddressLoggingEnabled = connection.server.getConfiguration() .isPlayerAddressLoggingEnabled(); - String playerIp = + final String playerIp = isPlayerAddressLoggingEnabled ? connection.getRemoteAddress().toString() : ""; return "[initial connection] " + playerIp; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index ddf3bb33d..0f7c43b84 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -95,7 +95,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler { .thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping))) .thenAcceptAsync( (event) -> { - StringBuilder json = new StringBuilder(); + final StringBuilder json = new StringBuilder(); VelocityServer.getPingGsonInstance(connection.getProtocolVersion()) .toJson(event.getPing(), json); connection.write(new StatusResponsePacket(json)); 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 index 26a46c1b4..67f90fd15 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java @@ -19,10 +19,14 @@ package com.velocitypowered.proxy.connection.player; import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import io.netty.buffer.ByteBufUtil; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.UUID; +import net.kyori.adventure.resource.ResourcePackRequest; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; /** * Implements {@link ResourcePackInfo}. @@ -31,13 +35,13 @@ public final class VelocityResourcePackInfo implements ResourcePackInfo { private final UUID id; private final String url; - private final @Nullable byte[] hash; + private final byte @Nullable [] hash; private final boolean shouldForce; private final @Nullable Component prompt; // 1.17+ only private final Origin origin; private Origin originalOrigin; - private VelocityResourcePackInfo(UUID id, String url, @Nullable byte[] hash, boolean shouldForce, + private VelocityResourcePackInfo(UUID id, String url, byte @Nullable [] hash, boolean shouldForce, @Nullable Component prompt, Origin origin) { this.id = id; this.url = url; @@ -69,7 +73,7 @@ public final class VelocityResourcePackInfo implements ResourcePackInfo { } @Override - public @Nullable byte[] getHash() { + public byte @Nullable[] getHash() { return hash == null ? null : hash.clone(); // Thanks spotbugs, very helpful. } @@ -105,6 +109,32 @@ public final class VelocityResourcePackInfo implements ResourcePackInfo { .setPrompt(prompt); } + @Override + public @NotNull ResourcePackRequest asResourcePackRequest() { + return ResourcePackRequest.resourcePackRequest() + .packs(net.kyori.adventure.resource.ResourcePackInfo.resourcePackInfo() + .id(this.id) + .uri(URI.create(this.url)) + .hash(this.hash == null ? "" : ByteBufUtil.hexDump(this.hash)) + .build()) + .required(this.shouldForce) + .prompt(this.prompt) + .build(); + } + + @SuppressWarnings("checkstyle:MissingJavadocMethod") + public static ResourcePackInfo fromAdventureRequest( + final ResourcePackRequest request, + final net.kyori.adventure.resource.ResourcePackInfo pack + ) { + return new BuilderImpl(pack.uri().toString()) + .setHash(pack.hash().isEmpty() ? null : ByteBufUtil.decodeHexDump(pack.hash())) + .setId(pack.id()) + .setShouldForce(request.required()) + .setPrompt(request.prompt()) + .build(); + } + /** * Implements the builder for {@link ResourcePackInfo} instances. */ @@ -113,7 +143,7 @@ public final class VelocityResourcePackInfo implements ResourcePackInfo { private UUID id; private final String url; private boolean shouldForce; - private @Nullable byte[] hash; + private byte @Nullable [] hash; private @Nullable Component prompt; private Origin origin = Origin.PLUGIN_ON_PROXY; @@ -135,7 +165,7 @@ public final class VelocityResourcePackInfo implements ResourcePackInfo { } @Override - public BuilderImpl setHash(@Nullable byte[] hash) { + public BuilderImpl setHash(final byte @Nullable [] hash) { if (hash != null) { Preconditions.checkArgument(hash.length == 20, "Hash length is not 20"); this.hash = hash.clone(); // Thanks spotbugs, very helpful. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java new file mode 100644 index 000000000..385bdce4c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 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.resourcepack; + +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; + +/** + * Legacy (Minecraft 1.17-1.20.2) ResourcePackHandler. + */ +public final class Legacy117ResourcePackHandler extends LegacyResourcePackHandler { + Legacy117ResourcePackHandler(final ConnectedPlayer player, final VelocityServer server) { + super(player, server); + } + + @Override + protected boolean shouldDisconnectForForcePack(final PlayerResourcePackStatusEvent event) { + return super.shouldDisconnectForForcePack(event) && !event.isOverwriteKick(); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java new file mode 100644 index 000000000..3b211bca5 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2024 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.resourcepack; + +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.List; +import java.util.Queue; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; + +/** + * Legacy (Minecraft <1.17) ResourcePackHandler. + */ +public sealed class LegacyResourcePackHandler extends ResourcePackHandler + permits Legacy117ResourcePackHandler { + protected @MonotonicNonNull Boolean previousResourceResponse; + protected final Queue outstandingResourcePacks = new ArrayDeque<>(); + private @Nullable ResourcePackInfo pendingResourcePack; + private @Nullable ResourcePackInfo appliedResourcePack; + + LegacyResourcePackHandler(final ConnectedPlayer player, final VelocityServer server) { + super(player, server); + } + + @Override + @Nullable + public ResourcePackInfo getFirstAppliedPack() { + return appliedResourcePack; + } + + @Override + @Nullable + public ResourcePackInfo getFirstPendingPack() { + return pendingResourcePack; + } + + @Override + public @NotNull Collection getAppliedResourcePacks() { + if (appliedResourcePack == null) { + return List.of(); + } + return List.of(appliedResourcePack); + } + + @Override + public @NotNull Collection getPendingResourcePacks() { + if (pendingResourcePack == null) { + return List.of(); + } + return List.of(pendingResourcePack); + } + + @Override + public void clearAppliedResourcePacks() { + // This is valid only for players with 1.20.2 versions + this.appliedResourcePack = null; + } + + @Override + public boolean remove(final @NotNull UUID id) throws UnsupportedOperationException { + throw new UnsupportedOperationException("Cannot remove a ResourcePack from a legacy client"); + } + + @Override + public void queueResourcePack(@NotNull 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() && player.getProtocolVersion() + .noLessThan(ProtocolVersion.MINECRAFT_1_17)) { + break; + } + onResourcePackResponse(new ResourcePackResponseBundle(queued.getId(), + PlayerResourcePackStatusEvent.Status.DECLINED)); + queued = null; + } + if (queued == null) { + // Exit as the queue was cleared + return; + } + } + + sendResourcePackRequestPacket(queued); + } + } + + @Override + public boolean onResourcePackResponse( + final @NotNull ResourcePackResponseBundle bundle + ) { + final boolean peek = bundle.status().isIntermediate(); + final ResourcePackInfo queued = peek + ? outstandingResourcePacks.peek() : outstandingResourcePacks.poll(); + + server.getEventManager() + .fire(new PlayerResourcePackStatusEvent( + this.player, bundle.uuid(), bundle.status(), queued)) + .thenAcceptAsync(event -> { + if (shouldDisconnectForForcePack(event)) { + event.getPlayer().disconnect(Component + .translatable("multiplayer.requiredTexturePrompt.disconnect")); + } + }); + + switch (bundle.status()) { + case ACCEPTED -> { + previousResourceResponse = true; + pendingResourcePack = queued; + } + case DECLINED -> previousResourceResponse = false; + case SUCCESSFUL -> { + appliedResourcePack = queued; + pendingResourcePack = null; + } + case FAILED_DOWNLOAD -> pendingResourcePack = null; + case DISCARDED -> { + if (queued != null && queued.getId() != null + && appliedResourcePack != null + && appliedResourcePack.getId().equals(queued.getId())) { + appliedResourcePack = null; + } + } + default -> { + } + } + + if (!peek) { + player.getConnection().eventLoop().execute(this::tickResourcePackQueue); + } + + return queued != null + && queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER; + } + + protected boolean shouldDisconnectForForcePack(final PlayerResourcePackStatusEvent event) { + return event.getStatus() == PlayerResourcePackStatusEvent.Status.DECLINED + && event.getPackInfo() != null && event.getPackInfo().getShouldForce(); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java new file mode 100644 index 000000000..1fb5aef91 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2024 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.resourcepack; + +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.kyori.adventure.resource.ResourcePackRequest; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Modern (Minecraft 1.20.3+) ResourcePackHandler + */ +public final class ModernResourcePackHandler extends ResourcePackHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(ModernResourcePackHandler.class); + private final ListMultimap outstandingResourcePacks = + Multimaps.newListMultimap(new ConcurrentHashMap<>(), LinkedList::new); + private final Map pendingResourcePacks = new ConcurrentHashMap<>(); + private final Map appliedResourcePacks = new ConcurrentHashMap<>(); + + ModernResourcePackHandler(final ConnectedPlayer player, final VelocityServer server) { + super(player, server); + } + + @Override + public @Nullable ResourcePackInfo getFirstAppliedPack() { + if (appliedResourcePacks.isEmpty()) { + return null; + } + return appliedResourcePacks.values().iterator().next(); + } + + @Override + public @Nullable ResourcePackInfo getFirstPendingPack() { + if (pendingResourcePacks.isEmpty()) { + return null; + } + return pendingResourcePacks.values().iterator().next(); + } + + @Override + public @NotNull Collection getAppliedResourcePacks() { + return List.copyOf(appliedResourcePacks.values()); + } + + @Override + public @NotNull Collection getPendingResourcePacks() { + return List.copyOf(pendingResourcePacks.values()); + } + + @Override + public void clearAppliedResourcePacks() { + this.outstandingResourcePacks.clear(); + this.pendingResourcePacks.clear(); + this.appliedResourcePacks.clear(); + } + + @Override + public boolean remove(final @NotNull UUID uuid) { + outstandingResourcePacks.removeAll(uuid); + return appliedResourcePacks.remove(uuid) != null | pendingResourcePacks.remove(uuid) != null; + } + + @Override + public void queueResourcePack(final @NotNull ResourcePackInfo info) { + final List outstandingResourcePacks = + this.outstandingResourcePacks.get(info.getId()); + outstandingResourcePacks.add(info); + if (outstandingResourcePacks.size() == 1) { + tickResourcePackQueue(outstandingResourcePacks.get(0).getId()); + } + } + + @Override + public void queueResourcePack(final @NotNull ResourcePackRequest request) { + if (request.packs().size() > 1) { + player.getBundleHandler().bundlePackets(() -> { + super.queueResourcePack(request); + }); + } else { + super.queueResourcePack(request); + } + } + + private void tickResourcePackQueue(final @NotNull UUID uuid) { + final List outstandingResourcePacks = + this.outstandingResourcePacks.get(uuid); + if (!outstandingResourcePacks.isEmpty()) { + sendResourcePackRequestPacket(outstandingResourcePacks.get(0)); + } + } + + @Override + public boolean onResourcePackResponse( + final @NotNull ResourcePackResponseBundle bundle + ) { + final UUID uuid = bundle.uuid(); + final List outstandingResourcePacks = + this.outstandingResourcePacks.get(uuid); + final boolean peek = bundle.status().isIntermediate(); + final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null : + peek ? outstandingResourcePacks.get(0) : outstandingResourcePacks.remove(0); + + server.getEventManager() + .fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued)) + .thenAcceptAsync(event -> { + if (event.getStatus() == PlayerResourcePackStatusEvent.Status.DECLINED + && event.getPackInfo() != null && event.getPackInfo().getShouldForce() + && !event.isOverwriteKick() + ) { + player.disconnect(Component + .translatable("multiplayer.requiredTexturePrompt.disconnect")); + } + }); + + switch (bundle.status()) { + // The player has accepted the resource pack and will proceed to download it. + case ACCEPTED -> { + if (queued != null) { + pendingResourcePacks.put(uuid, queued); + } + } + // The resource pack has been applied correctly. + case SUCCESSFUL -> { + if (queued != null) { + appliedResourcePacks.put(uuid, queued); + } + pendingResourcePacks.remove(uuid); + } + // An error occurred while trying to download the resource pack to the client, + // so the resource pack cannot be applied. + case DISCARDED, DECLINED, FAILED_RELOAD, FAILED_DOWNLOAD, INVALID_URL -> { + pendingResourcePacks.remove(uuid); + appliedResourcePacks.remove(uuid); + } + // The other cases in which no action is taken are documented in the javadocs. + default -> { + } + } + + if (!peek) { + player.getConnection().eventLoop().execute(() -> tickResourcePackQueue(uuid)); + } + + return queued != null + && queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java new file mode 100644 index 000000000..d779ca297 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2024 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.resourcepack; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; +import io.netty.buffer.ByteBufUtil; +import java.util.Collection; +import java.util.UUID; +import net.kyori.adventure.resource.ResourcePackRequest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * ResourcePackHandler. + */ +public abstract sealed class ResourcePackHandler + permits LegacyResourcePackHandler, ModernResourcePackHandler { + protected final ConnectedPlayer player; + protected final VelocityServer server; + + protected ResourcePackHandler(final ConnectedPlayer player, final VelocityServer server) { + this.player = player; + this.server = server; + } + + /** + * Creates a new ResourcePackHandler. + * + * @param player the player. + * @param server the velocity server + * + * @return a new ResourcePackHandler + */ + public static @NotNull ResourcePackHandler create(final ConnectedPlayer player, + final VelocityServer server) { + final ProtocolVersion protocolVersion = player.getProtocolVersion(); + if (protocolVersion.lessThan(ProtocolVersion.MINECRAFT_1_17)) { + return new LegacyResourcePackHandler(player, server); + } + if (protocolVersion.lessThan(ProtocolVersion.MINECRAFT_1_20_3)) { + return new Legacy117ResourcePackHandler(player, server); + } + return new ModernResourcePackHandler(player, server); + } + + public abstract @Nullable ResourcePackInfo getFirstAppliedPack(); + + public abstract @Nullable ResourcePackInfo getFirstPendingPack(); + + public abstract @NotNull Collection getAppliedResourcePacks(); + + public abstract @NotNull Collection getPendingResourcePacks(); + + /** + * Clears the applied resource pack field. + */ + public abstract void clearAppliedResourcePacks(); + + public abstract boolean remove(final UUID id); + + /** + * Queues a resource-pack for sending to the player and sends it immediately if the queue is + * empty. + */ + public abstract void queueResourcePack(final @NotNull ResourcePackInfo info); + + /** + * Queues a resource-request for sending to the player and sends it immediately if the queue is + * empty. + */ + public void queueResourcePack(final @NotNull ResourcePackRequest request) { + for (final net.kyori.adventure.resource.ResourcePackInfo pack : request.packs()) { + queueResourcePack(VelocityResourcePackInfo.fromAdventureRequest(request, pack)); + } + } + + protected void sendResourcePackRequestPacket(final @NotNull ResourcePackInfo queued) { + final ResourcePackRequestPacket request = new ResourcePackRequestPacket(); + request.setId(queued.getId()); + 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() == null ? null : + new ComponentHolder(player.getProtocolVersion(), queued.getPrompt())); + + player.getConnection().write(request); + } + + /** + * Processes a client response to a sent resource-pack. + *
    + *

    Cases in which no action will be taken:

    + * + *
  • DOWNLOADED + *

    In this case the resource pack is downloaded and will be applied to the client, + * no action is required in Velocity.

    + * + *
  • INVALID_URL + *

    In this case, the client has received a resource pack request + * and the first check it performs is if the URL is valid, if not, + * it will return this value

    + * + *
  • FAILED_RELOAD + *

    In this case, when trying to reload the client's resources, + * an error occurred while reloading a resource pack

    + *
+ * + *
  • DECLINED + *

    Only in modern versions, as the resource pack has already been rejected, + * there is nothing to do, if the resource pack is required, + * the client will be kicked out of the server.

    + * + * @param bundle the resource pack response bundle + */ + public abstract boolean onResourcePackResponse( + final @NotNull ResourcePackResponseBundle bundle); +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackResponseBundle.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackResponseBundle.java new file mode 100644 index 000000000..9465c3be3 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackResponseBundle.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.resourcepack; + +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import java.util.UUID; + +@SuppressWarnings("checkstyle:MissingJavadocType") +public record ResourcePackResponseBundle(UUID uuid, PlayerResourcePackStatusEvent.Status status) { +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/crypto/IdentifiedKeyImpl.java b/proxy/src/main/java/com/velocitypowered/proxy/crypto/IdentifiedKeyImpl.java index d164d2f19..748fe4640 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/crypto/IdentifiedKeyImpl.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/crypto/IdentifiedKeyImpl.java @@ -119,7 +119,7 @@ public class IdentifiedKeyImpl implements IdentifiedKey { if (revision == Revision.GENERIC_V1) { String pemKey = EncryptionUtils.pemEncodeRsaKey(publicKey); long expires = expiryTemporal.toEpochMilli(); - byte[] toVerify = ("" + expires + pemKey).getBytes(StandardCharsets.US_ASCII); + byte[] toVerify = (expires + pemKey).getBytes(StandardCharsets.US_ASCII); return EncryptionUtils.verifySignature( EncryptionUtils.SHA1_WITH_RSA, EncryptionUtils.getYggdrasilSessionKey(), signature, toVerify); @@ -166,12 +166,10 @@ public class IdentifiedKeyImpl implements IdentifiedKey { if (this == o) { return true; } - if (!(o instanceof IdentifiedKey)) { + if (!(o instanceof final IdentifiedKey that)) { return false; } - IdentifiedKey that = (IdentifiedKey) o; - return Objects.equal(this.getSignedPublicKey(), that.getSignedPublicKey()) && Objects.equal(this.getExpiryTemporal(), that.getExpiryTemporal()) && Arrays.equals(this.getSignature(), that.getSignature()) 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 93344a4e5..87f80f9de 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -48,6 +48,7 @@ import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.SERVERB import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket; import com.velocitypowered.proxy.protocol.packet.BossBarPacket; +import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; @@ -144,7 +145,7 @@ public enum StateRegistry { PluginMessagePacket.class, PluginMessagePacket::new, map(0x01, MINECRAFT_1_20_2, false)); serverbound.register( - FinishedUpdatePacket.class, FinishedUpdatePacket::new, + FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE, map(0x02, MINECRAFT_1_20_2, false)); serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new, map(0x03, MINECRAFT_1_20_2, false)); @@ -163,7 +164,7 @@ public enum StateRegistry { DisconnectPacket.class, () -> new DisconnectPacket(false), map(0x01, MINECRAFT_1_20_2, false)); clientbound.register( - FinishedUpdatePacket.class, FinishedUpdatePacket::new, + FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE, map(0x02, MINECRAFT_1_20_2, false)); clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new, map(0x03, MINECRAFT_1_20_2, false)); @@ -289,7 +290,7 @@ public enum StateRegistry { map(0x27, MINECRAFT_1_20_2, false), map(0x28, MINECRAFT_1_20_3, false)); serverbound.register( - FinishedUpdatePacket.class, FinishedUpdatePacket::new, + FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE, map(0x0B, MINECRAFT_1_20_2, false)); clientbound.register( @@ -569,9 +570,13 @@ public enum StateRegistry { map(0x49, MINECRAFT_1_20_3, false)); clientbound.register( StartUpdatePacket.class, - StartUpdatePacket::new, + () -> StartUpdatePacket.INSTANCE, map(0x65, MINECRAFT_1_20_2, false), map(0x67, MINECRAFT_1_20_3, false)); + clientbound.register( + BundleDelimiterPacket.class, + () -> BundleDelimiterPacket.INSTANCE, + map(0x00, MINECRAFT_1_19_4, false)); } }, LOGIN { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingEncoder.java index ad06fa5a2..25814b156 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingEncoder.java @@ -39,7 +39,7 @@ public class LegacyPingEncoder extends MessageToByteEncoder { protected void encode(ChannelHandlerContext ctx, LegacyDisconnect msg, ByteBuf out) throws Exception { out.writeByte(0xff); - writeLegacyString(out, msg.getReason()); + writeLegacyString(out, msg.reason()); } private static void writeLegacyString(ByteBuf out, String string) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/BundleDelimiterPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/BundleDelimiterPacket.java new file mode 100644 index 000000000..4da691c76 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/BundleDelimiterPacket.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018-2021 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; + +public final class BundleDelimiterPacket implements MinecraftPacket { + public static final BundleDelimiterPacket INSTANCE = new BundleDelimiterPacket(); + + private BundleDelimiterPacket() { + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyDisconnect.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyDisconnect.java index f3033b021..e8f8deef2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyDisconnect.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyDisconnect.java @@ -25,19 +25,14 @@ import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; -public class LegacyDisconnect { +@SuppressWarnings("checkstyle:MissingJavadocType") +public record LegacyDisconnect(String reason) { private static final ServerPing.Players FAKE_PLAYERS = new ServerPing.Players(0, 0, ImmutableList.of()); private static final String LEGACY_COLOR_CODE = Character .toString(LegacyComponentSerializer.SECTION_CHAR); - private final String reason; - - private LegacyDisconnect(String reason) { - this.reason = reason; - } - /** * Converts a modern server list ping response into an legacy disconnect packet. * @@ -47,22 +42,21 @@ public class LegacyDisconnect { */ public static LegacyDisconnect fromServerPing(ServerPing response, LegacyMinecraftPingVersion version) { - Players players = response.getPlayers().orElse(FAKE_PLAYERS); + final Players players = response.getPlayers().orElse(FAKE_PLAYERS); - switch (version) { - case MINECRAFT_1_3: + return switch (version) { + case MINECRAFT_1_3 -> // Minecraft 1.3 and below use the section symbol as a delimiter. Accordingly, we must // remove all section symbols, along with fetching just the first line of an (unformatted) // MOTD. - return new LegacyDisconnect(String.join(LEGACY_COLOR_CODE, + new LegacyDisconnect(String.join(LEGACY_COLOR_CODE, cleanSectionSymbol(getFirstLine(PlainTextComponentSerializer.plainText().serialize( response.getDescriptionComponent()))), Integer.toString(players.getOnline()), Integer.toString(players.getMax()))); - case MINECRAFT_1_4: - case MINECRAFT_1_6: + case MINECRAFT_1_4, MINECRAFT_1_6 -> // Minecraft 1.4-1.6 provide support for more fields, and additionally support color codes. - return new LegacyDisconnect(String.join("\0", + new LegacyDisconnect(String.join("\0", LEGACY_COLOR_CODE + "1", Integer.toString(response.getVersion().getProtocol()), response.getVersion().getName(), @@ -71,9 +65,8 @@ public class LegacyDisconnect { Integer.toString(players.getOnline()), Integer.toString(players.getMax()) )); - default: - throw new IllegalArgumentException("Unknown version " + version); - } + default -> throw new IllegalArgumentException("Unknown version " + version); + }; } private static String cleanSectionSymbol(String string) { @@ -81,7 +74,7 @@ public class LegacyDisconnect { } private static String getFirstLine(String legacyMotd) { - int newline = legacyMotd.indexOf('\n'); + final int newline = legacyMotd.indexOf('\n'); return newline == -1 ? legacyMotd : legacyMotd.substring(0, newline); } @@ -93,11 +86,7 @@ public class LegacyDisconnect { */ public static LegacyDisconnect from(TextComponent component) { // We intentionally use the legacy serializers, because the old clients can't understand JSON. - String serialized = LegacyComponentSerializer.legacySection().serialize(component); + final String serialized = LegacyComponentSerializer.legacySection().serialize(component); return new LegacyDisconnect(serialized); } - - public String getReason() { - return reason; - } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RemoveResourcePackPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RemoveResourcePackPacket.java index 0f774a629..d003a0a95 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RemoveResourcePackPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RemoveResourcePackPacket.java @@ -23,7 +23,6 @@ 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 java.util.UUID; public class RemoveResourcePackPacket implements MinecraftPacket { @@ -37,6 +36,10 @@ public class RemoveResourcePackPacket implements MinecraftPacket { this.id = id; } + public UUID getId() { + return id; + } + @Override public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { if (buf.readBoolean()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java index 8f6cfe6e3..07f9f776a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java @@ -28,11 +28,10 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - import java.util.UUID; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; public class ResourcePackRequestPacket implements MinecraftPacket { @@ -80,7 +79,7 @@ public class ResourcePackRequestPacket implements MinecraftPacket { return prompt; } - public void setPrompt(ComponentHolder prompt) { + public void setPrompt(@Nullable ComponentHolder prompt) { this.prompt = prompt; } @@ -126,7 +125,7 @@ public class ResourcePackRequestPacket implements MinecraftPacket { } public VelocityResourcePackInfo toServerPromptedPack() { - ResourcePackInfo.Builder builder = + final ResourcePackInfo.Builder builder = new VelocityResourcePackInfo.BuilderImpl(Preconditions.checkNotNull(url)) .setId(id).setPrompt(prompt == null ? null : prompt.getComponent()) .setShouldForce(isRequired).setOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponsePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponsePacket.java index 4ef08a009..020c3530d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponsePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponsePacket.java @@ -54,6 +54,10 @@ public class ResourcePackResponsePacket implements MinecraftPacket { return hash; } + public UUID getId() { + return id; + } + @Override public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdatePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdatePacket.java index 22c99ee54..b4d93ec45 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdatePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdatePacket.java @@ -24,6 +24,10 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; public class FinishedUpdatePacket implements MinecraftPacket { + public static final FinishedUpdatePacket INSTANCE = new FinishedUpdatePacket(); + + private FinishedUpdatePacket() { + } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdatePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdatePacket.java index 9711126db..8d4585d89 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdatePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdatePacket.java @@ -24,6 +24,10 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; public class StartUpdatePacket implements MinecraftPacket { + public static final StartUpdatePacket INSTANCE = new StartUpdatePacket(); + + private StartUpdatePacket() { + } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java b/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java index a7b9ad4b6..edc067613 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java @@ -44,8 +44,8 @@ public enum TranslatableMapper implements BiConsumer componentConsumer ) { for (final Translator source : GlobalTranslator.translator().sources()) { - if (source instanceof TranslationRegistry - && ((TranslationRegistry) source).contains(translatableComponent.key())) { + if (source instanceof TranslationRegistry registry + && registry.contains(translatableComponent.key())) { componentConsumer.accept(GlobalTranslator.render(translatableComponent, ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()))); return; @@ -56,8 +56,7 @@ public enum TranslatableMapper implements BiConsumer