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 0aea8d7786..2e4837f942 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 @@ -27,9 +27,11 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; +import com.velocitypowered.api.network.HandshakeIntent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.permission.PermissionFunction; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.UuidUtils; @@ -38,6 +40,7 @@ import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackTransfer; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledgedPacket; @@ -45,6 +48,9 @@ import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.CodecException; +import java.security.SignatureException; +import java.util.Collection; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -68,15 +74,17 @@ public class AuthSessionHandler implements MinecraftSessionHandler { private GameProfile profile; private @MonotonicNonNull ConnectedPlayer connectedPlayer; private final boolean onlineMode; + private final CompletableFuture appliedResourcePacksFuture; private State loginState = State.START; // 1.20.2+ - AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, - GameProfile profile, boolean onlineMode) { + AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, GameProfile profile, boolean onlineMode, + CompletableFuture appliedResourcePacksFuture) { this.server = Preconditions.checkNotNull(server, "server"); this.inbound = Preconditions.checkNotNull(inbound, "inbound"); this.profile = Preconditions.checkNotNull(profile, "profile"); this.onlineMode = onlineMode; this.mcConnection = inbound.delegatedConnection(); + this.appliedResourcePacksFuture = appliedResourcePacksFuture; } @Override @@ -112,7 +120,7 @@ public void activated() { return server.getEventManager() .fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS)) - .thenAcceptAsync(event -> { + .thenAcceptBothAsync(appliedResourcePacksFuture, (event, appliedResourcePacks) -> { if (!mcConnection.isClosed()) { // wait for permissions to load, then set the players permission function final PermissionFunction function = event.createFunction(player); @@ -124,6 +132,7 @@ public void activated() { } else { player.setPermissionFunction(function); } + loadAppliedResourcePacks(player, appliedResourcePacks); startLoginCompletion(player); } }, mcConnection.eventLoop()); @@ -133,6 +142,22 @@ public void activated() { }); } + private void loadAppliedResourcePacks(ConnectedPlayer player, byte[] cookieData) { + if (player.getHandshakeIntent() != HandshakeIntent.TRANSFER || cookieData == null) { + return; + } + + try { + Collection appliedResourcePacks = ResourcePackTransfer.decodeAndValidateCookieData( + server.getConfiguration().getForwardingSecret(), cookieData); + player.resourcePackHandler().loadAppliedResourcePacks(appliedResourcePacks); + } catch (SignatureException e) { + logger.warn("Signature error while loading applied resource packs of {}", player.getUsername(), e); + } catch (IndexOutOfBoundsException | CodecException e) { + logger.warn("Error while decoding applied resource packs of {}", player.getUsername(), e); + } + } + private void startLoginCompletion(ConnectedPlayer player) { int threshold = server.getConfiguration().getCompressionThreshold(); if (threshold >= 0 && mcConnection.getProtocolVersion().noLessThan(MINECRAFT_1_8)) { @@ -160,7 +185,7 @@ private void startLoginCompletion(ConnectedPlayer player) { } } } else { - logger.warn("A custom key type has been set for player " + player.getUsername()); + logger.warn("A custom key type has been set for player {}", player.getUsername()); } } else { if (!Objects.equals(playerKey.getSignatureHolder(), playerUniqueId)) { @@ -194,6 +219,11 @@ public boolean handle(LoginAcknowledgedPacket packet) { @Override public boolean handle(ServerboundCookieResponsePacket packet) { + if (packet.getKey().equals(ResourcePackTransfer.APPLIED_RESOURCE_PACKS_KEY)) { + appliedResourcePacksFuture.complete(packet.getPayload()); + return true; + } + server.getEventManager() .fire(new CookieReceiveEvent(connectedPlayer, packet.getKey(), packet.getPayload())) .thenAcceptAsync(event -> { 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 be7a82b2ae..f12bb5d44b 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 @@ -63,6 +63,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackTransfer; import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler; import com.velocitypowered.proxy.connection.util.ConnectionMessages; @@ -1028,12 +1029,21 @@ public void transferToHost(final InetSocketAddress address) { if (resultedAddress == null) { resultedAddress = address; } - connection.write(new TransferPacket( - resultedAddress.getHostName(), resultedAddress.getPort())); + storeAppliedPacks(); + connection.write(new TransferPacket(resultedAddress.getHostName(), resultedAddress.getPort())); } }); } + private void storeAppliedPacks() { + if (connection.getState() != StateRegistry.PLAY && connection.getState() != StateRegistry.CONFIG) { + return; + } + Collection appliedResourcePacks = resourcePackHandler.getAppliedResourcePacks(); + byte[] cookieData = ResourcePackTransfer.createCookieData(server.getConfiguration().getForwardingSecret(), appliedResourcePacks); + connection.write(new ClientboundStoreCookiePacket(ResourcePackTransfer.APPLIED_RESOURCE_PACKS_KEY, cookieData)); + } + @Override public void storeCookie(final Key key, final byte[] data) { Preconditions.checkNotNull(key); @@ -1042,8 +1052,7 @@ public void storeCookie(final Key key, final byte[] data) { this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_5), "Player version must be at least 1.20.5 to be able to store cookies"); - if (connection.getState() != StateRegistry.PLAY - && connection.getState() != StateRegistry.CONFIG) { + if (connection.getState() != StateRegistry.PLAY && connection.getState() != StateRegistry.CONFIG) { throw new IllegalStateException("Can only store cookie in CONFIGURATION or PLAY protocol"); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java index 00187d3470..68f1c1e4a0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java @@ -27,19 +27,23 @@ import com.google.common.primitives.Longs; import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; +import com.velocitypowered.api.network.HandshakeIntent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackTransfer; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.util.VelocityProperties; import io.netty.buffer.ByteBuf; import java.net.InetSocketAddress; @@ -52,7 +56,9 @@ import java.security.MessageDigest; import java.util.Arrays; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; @@ -77,9 +83,9 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { private byte[] verify = EMPTY_BYTE_ARRAY; private LoginState currentState = LoginState.LOGIN_PACKET_EXPECTED; private final boolean forceKeyAuthentication; + private CompletableFuture appliedResourcePacksFuture; - InitialLoginSessionHandler(VelocityServer server, MinecraftConnection mcConnection, - LoginInboundConnection inbound) { + InitialLoginSessionHandler(VelocityServer server, MinecraftConnection mcConnection, LoginInboundConnection inbound) { this.server = Preconditions.checkNotNull(server, "server"); this.mcConnection = Preconditions.checkNotNull(mcConnection, "mcConnection"); this.inbound = Preconditions.checkNotNull(inbound, "inbound"); @@ -87,6 +93,16 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { "auth.forceSecureProfiles", server.getConfiguration().isForceKeyAuthentication()); } + @Override + public void activated() { + if (inbound.getHandshakeIntent() == HandshakeIntent.TRANSFER) { + appliedResourcePacksFuture = new CompletableFuture().completeOnTimeout(null, 20, TimeUnit.SECONDS); + mcConnection.write(new ClientboundCookieRequestPacket(ResourcePackTransfer.APPLIED_RESOURCE_PACKS_KEY)); + } else { + appliedResourcePacksFuture = CompletableFuture.completedFuture(null); + } + } + @Override public boolean handle(ServerLoginPacket packet) { assertState(LoginState.LOGIN_PACKET_EXPECTED); @@ -151,8 +167,8 @@ public boolean handle(ServerLoginPacket packet) { this.currentState = LoginState.ENCRYPTION_REQUEST_SENT; } else { mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, - new AuthSessionHandler(server, inbound, - GameProfile.forOfflinePlayer(login.getUsername()), false)); + new AuthSessionHandler(server, inbound, GameProfile.forOfflinePlayer(login.getUsername()), + false, appliedResourcePacksFuture)); } }); }); @@ -254,7 +270,7 @@ public boolean handle(EncryptionResponsePacket packet) { } // All went well, initialize the session. mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, - new AuthSessionHandler(server, inbound, profile, true)); + new AuthSessionHandler(server, inbound, profile, true, appliedResourcePacksFuture)); } else if (response.statusCode() == 204) { // Apparently an offline-mode user logged onto this online-mode proxy. inbound.disconnect( @@ -285,6 +301,15 @@ public boolean handle(EncryptionResponsePacket packet) { return true; } + @Override + public boolean handle(ServerboundCookieResponsePacket packet) { + if (packet.getKey().equals(ResourcePackTransfer.APPLIED_RESOURCE_PACKS_KEY)) { + appliedResourcePacksFuture.complete(packet.getPayload()); + return true; + } + return false; + } + private EncryptionRequestPacket generateEncryptionRequest() { byte[] verify = new byte[4]; ThreadLocalRandom.current().nextBytes(verify); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackTransfer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackTransfer.java new file mode 100644 index 0000000000..8523ad87b2 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackTransfer.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2018-2023 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.base.Preconditions; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.util.NettyPreconditions; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DecoderException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; + +/** + * Utility class for handling resource pack transfer operations in Velocity. + * + *

This class provides methods for creating and validating signed cookie data for applied + * resource packs. These cookies are used to securely store and transfer information about + * resource packs between the client and server, ensuring data integrity and authenticity.

+ * + *

The signing mechanism uses the HMAC-SHA256 algorithm, with a secret key to produce and + * verify signatures. This ensures that tampered or invalid data is detected.

+ * + *

Usage:

+ *
+ * // Creating cookie data
+ * byte[] secretKey = ...; // Secret key for signing
+ * Collection<ResourcePackInfo> resourcePacks = ...; // Applied resource packs
+ * byte[] cookieData = ResourcePackTransfer.createCookieData(secretKey, resourcePacks);
+ *
+ * // Decoding and validating cookie data
+ * try {
+ *     Collection<ResourcePackInfo> validatedPacks =
+ *         ResourcePackTransfer.decodeAndValidateCookieData(secretKey, cookieData);
+ * } catch (SignatureException | DecoderException e) {
+ *     // Handle invalid or tampered cookie data
+ * }
+ * 
+ * + *

Instances of this class cannot be created. All methods are static.

+ * + * @see ResourcePackInfo + */ +public class ResourcePackTransfer { + public static Key APPLIED_RESOURCE_PACKS_KEY = Key.key("velocity", "applied_resource_packs"); + + private static final String ALGORITHM = "HmacSHA256"; + private static ResourcePackInfo.Origin[] ORIGINS = ResourcePackInfo.Origin.values(); + + private ResourcePackTransfer() { + } + + /** + * Creates a signed cookie data byte array for the given applied resource packs. + * + *

This method serializes the resource pack information into a compact binary format, signs it + * using HMAC-SHA256 with the provided secret key, and returns the resulting byte array. The + * signature ensures the integrity and authenticity of the data.

+ * + * @param secret the secret key used for signing the cookie data + * @param appliedResourcePacks a collection of {@link ResourcePackInfo} objects representing + * the applied resource packs + * @return a byte array containing the signed cookie data + * @throws RuntimeException if the HMAC-SHA256 algorithm is not available or the secret key + * is invalid + */ + public static byte[] createCookieData(final byte[] secret, final Collection appliedResourcePacks) { + if (appliedResourcePacks.isEmpty()) { + return new byte[0]; + } + + final ByteBuf buffer = Unpooled.buffer(appliedResourcePacks.size() * 256); + try { + ProtocolUtils.writeVarInt(buffer, appliedResourcePacks.size()); + for (ResourcePackInfo appliedResourcePack : appliedResourcePacks) { + ProtocolUtils.writeString(buffer, appliedResourcePack.getUrl()); + ProtocolUtils.writeUuid(buffer, appliedResourcePack.getId()); + byte[] hash = appliedResourcePack.getHash(); + buffer.writeBoolean(hash != null); + if (hash != null) { + Preconditions.checkArgument(hash.length == 20, "Hash length is not 20"); + buffer.writeBytes(hash); + } + buffer.writeBoolean(appliedResourcePack.getShouldForce()); + Component prompt = appliedResourcePack.getPrompt(); + buffer.writeBoolean(prompt != null); + if (prompt != null) { + ProtocolUtils.writeString(buffer, ProtocolUtils.getJsonChatSerializer(ProtocolVersion.MAXIMUM_VERSION).serialize(prompt)); + } + ProtocolUtils.writeVarInt(buffer, appliedResourcePack.getOrigin().ordinal()); + ProtocolUtils.writeVarInt(buffer, appliedResourcePack.getOriginalOrigin().ordinal()); + } + + final Mac mac = Mac.getInstance(ALGORITHM); + mac.init(new SecretKeySpec(secret, ALGORITHM)); + mac.update(buffer.array(), buffer.arrayOffset(), buffer.readableBytes()); + buffer.writeBytes(mac.doFinal()); + + return Arrays.copyOfRange(buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + buffer.readableBytes()); + } catch (final InvalidKeyException e) { + buffer.release(); + throw new RuntimeException("Unable to sign applied resource packs cookie data", e); + } catch (final NoSuchAlgorithmException e) { + // Should never happen + throw new AssertionError(e); + } finally { + buffer.release(); + } + } + + /** + * Decodes and validates the given cookie data byte array. + * + *

This method checks the integrity and authenticity of the cookie data by verifying its + * HMAC-SHA256 signature using the provided secret key. If the signature is valid, it deserializes + * the data back into a collection of {@link ResourcePackInfo} objects. If the data is invalid + * or tampered with, a {@link SignatureException} is thrown.

+ * + * @param secret the secret key used to verify the cookie data's signature + * @param data the cookie data byte array to decode and validate + * @return a collection of {@link ResourcePackInfo} objects representing the applied resource packs + * @throws SignatureException if the signature is missing, incomplete, or invalid + * @throws DecoderException if there is an error during decoding + * @throws RuntimeException if the HMAC-SHA256 algorithm is not available or the secret key + * is invalid + */ + public static Collection decodeAndValidateCookieData(final byte[] secret, final byte[] data) + throws SignatureException, DecoderException { + if (data == null || data.length == 0) { + return Collections.emptyList(); + } + if (data.length <= 32) { + throw new SignatureException("Applied resource packs cookie data has no or incomplete signature"); + } + + try { + final Mac mac = Mac.getInstance(ALGORITHM); + mac.init(new SecretKeySpec(secret, ALGORITHM)); + mac.update(data, 0, data.length - 32); + + if (!Arrays.equals(mac.doFinal(), 0, 32, data, data.length - 32, data.length)) { + throw new SignatureException("Applied resource packs cookie data has invalid signature"); + } + } catch (final InvalidKeyException e) { + throw new RuntimeException("Unable to verify signature of applied resource packs cookie data", e); + } catch (final NoSuchAlgorithmException e) { + // Should never happen + throw new AssertionError(e); + } + + ByteBuf buffer = Unpooled.wrappedBuffer(data); + try { + int size = ProtocolUtils.readVarInt(buffer); + NettyPreconditions.checkFrame(size <= 256, "Too many applied packs (got %s, maximum is %s)", size, 256); + List appliedResourcePacks = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + VelocityResourcePackInfo.BuilderImpl builder = new VelocityResourcePackInfo.BuilderImpl(ProtocolUtils.readString(buffer)); + builder.setId(ProtocolUtils.readUuid(buffer)); + if (buffer.readBoolean()) { + byte[] hash = new byte[20]; + buffer.readBytes(hash); + builder.setHash(hash); + } + builder.setShouldForce(buffer.readBoolean()); + if (buffer.readBoolean()) { + builder.setPrompt(ProtocolUtils.getJsonChatSerializer(ProtocolVersion.MAXIMUM_VERSION).deserialize(ProtocolUtils.readString(buffer))); + } + builder.setOrigin(ORIGINS[ProtocolUtils.readVarInt(buffer)]); + VelocityResourcePackInfo appliedResourcePack = builder.build(); + appliedResourcePack.setOriginalOrigin(ORIGINS[ProtocolUtils.readVarInt(buffer)]); + appliedResourcePacks.add(appliedResourcePack); + } + return appliedResourcePacks; + } finally { + buffer.release(); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java index 4292710e0c..e02b794d1c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java @@ -182,7 +182,7 @@ public BuilderImpl setPrompt(@Nullable Component prompt) { } @Override - public ResourcePackInfo build() { + public VelocityResourcePackInfo build() { return new VelocityResourcePackInfo(id, url, hash, shouldForce, prompt, origin); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java index 53fa2421c7..49dbc3ba7a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java @@ -68,6 +68,11 @@ public ResourcePackInfo getFirstPendingPack() { return List.of(appliedResourcePack); } + @Override + public void loadAppliedResourcePacks(Collection appliedResourcePacks) { + throw new UnsupportedOperationException(); + } + @Override public @NotNull Collection getPendingResourcePacks() { if (pendingResourcePack == null) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java index 077ce701d5..c6e2f5fc3c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java @@ -70,6 +70,14 @@ public final class ModernResourcePackHandler extends ResourcePackHandler { return List.copyOf(appliedResourcePacks.values()); } + @Override + public void loadAppliedResourcePacks(Collection appliedResourcePacks) { + this.appliedResourcePacks.clear(); + for (ResourcePackInfo appliedResourcePack : appliedResourcePacks) { + this.appliedResourcePacks.put(appliedResourcePack.getId(), appliedResourcePack); + } + } + @Override public @NotNull Collection getPendingResourcePacks() { return List.copyOf(pendingResourcePacks.values()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java index e5df9f6592..592cfe8b0b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java @@ -73,6 +73,8 @@ protected ResourcePackHandler(final ConnectedPlayer player, final VelocityServer public abstract @NotNull Collection getAppliedResourcePacks(); + public abstract void loadAppliedResourcePacks(Collection appliedResourcePacks); + public abstract @NotNull Collection getPendingResourcePacks(); /**