From 99014e94598a4ffe246e448112794c1c8c9af685 Mon Sep 17 00:00:00 2001 From: RootBeer Date: Tue, 26 May 2026 16:26:09 -0400 Subject: [PATCH] Trigger resourcepack callbacks This targets the long-lost https://github.com/PaperMC/Velocity/issues/1369 issue. --- .../player/PlayerResourcePackStatusEvent.java | 32 ++++++++--- .../handler/LegacyResourcePackHandler.java | 8 +-- .../handler/ModernResourcePackHandler.java | 7 ++- .../handler/ResourcePackHandler.java | 57 ++++++++++++++++++- 4 files changed, 88 insertions(+), 16 deletions(-) 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 d212666b71..f6e8117584 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 @@ -13,6 +13,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import java.util.UUID; +import net.kyori.adventure.resource.ResourcePackStatus; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -144,35 +145,50 @@ public enum Status { /** * The resource pack was applied successfully. */ - SUCCESSFUL, + SUCCESSFUL(ResourcePackStatus.SUCCESSFULLY_LOADED), /** * The player declined to download the resource pack. */ - DECLINED, + DECLINED(ResourcePackStatus.DECLINED), /** * The player could not download the resource pack. */ - FAILED_DOWNLOAD, + FAILED_DOWNLOAD(ResourcePackStatus.FAILED_DOWNLOAD), /** * The player has accepted the resource pack and is now downloading it. */ - ACCEPTED, + ACCEPTED(ResourcePackStatus.ACCEPTED), /** * The player has downloaded the resource pack. */ - DOWNLOADED, + DOWNLOADED(ResourcePackStatus.DOWNLOADED), /** * The URL of the resource pack failed to load. */ - INVALID_URL, + INVALID_URL(ResourcePackStatus.INVALID_URL), /** * The player failed to reload the resource pack. */ - FAILED_RELOAD, + FAILED_RELOAD(ResourcePackStatus.FAILED_RELOAD), /** * The resource pack was discarded. */ - DISCARDED; + DISCARDED(ResourcePackStatus.DISCARDED); + + private final ResourcePackStatus adventureStatus; + + Status(ResourcePackStatus adventureStatus) { + this.adventureStatus = adventureStatus; + } + + /** + * Returns the Adventure {@link ResourcePackStatus} corresponding to this Velocity status. + * + * @return the matching Adventure status + */ + public ResourcePackStatus adventureStatus() { + return adventureStatus; + } /** * Returns true if the resource pack status is intermediate, indicating that the player has 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..df6f31e9cc 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 @@ -77,7 +77,7 @@ public ResourcePackInfo getFirstPendingPack() { } @Override - public void clearAppliedResourcePacks() { + protected void doClearAppliedResourcePacks() { // This is valid only for players with 1.20.2 versions this.appliedResourcePack = null; } @@ -132,9 +132,9 @@ public boolean onResourcePackResponse( final ResourcePackInfo queued = peek ? outstandingResourcePacks.peek() : outstandingResourcePacks.poll(); - server.getEventManager() - .fire(new PlayerResourcePackStatusEvent( - this.player, bundle.uuid(), bundle.status(), queued)) + dispatchPackCallback(bundle.uuid(), bundle.status()) + .thenCompose(v -> server.getEventManager() + .fire(new PlayerResourcePackStatusEvent(this.player, bundle.uuid(), bundle.status(), queued))) .thenAcceptAsync(event -> { if (shouldDisconnectForForcePack(event)) { event.getPlayer().disconnect(Component 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 f0fd5e084c..c85fa3bc26 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 @@ -76,7 +76,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler { } @Override - public void clearAppliedResourcePacks() { + protected void doClearAppliedResourcePacks() { this.outstandingResourcePacks.clear(); this.pendingResourcePacks.clear(); this.appliedResourcePacks.clear(); @@ -126,8 +126,9 @@ public boolean onResourcePackResponse( final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null : peek ? outstandingResourcePacks.getFirst() : outstandingResourcePacks.removeFirst(); - server.getEventManager() - .fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued)) + dispatchPackCallback(uuid, bundle.status()) + .thenCompose(v -> 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() 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 b384388534..0020813456 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 @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.connection.player.resourcepack.handler; +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; @@ -29,8 +30,14 @@ import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import io.netty.buffer.ByteBufUtil; import java.util.Collection; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import net.kyori.adventure.resource.ResourcePackCallback; import net.kyori.adventure.resource.ResourcePackRequest; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,9 +46,13 @@ */ public abstract sealed class ResourcePackHandler permits LegacyResourcePackHandler, ModernResourcePackHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourcePackHandler.class); + protected final ConnectedPlayer player; protected final VelocityServer server; + private final Map packCallbacks = new ConcurrentHashMap<>(); + protected ResourcePackHandler(final ConnectedPlayer player, final VelocityServer server) { this.player = player; this.server = server; @@ -78,7 +89,15 @@ protected ResourcePackHandler(final ConnectedPlayer player, final VelocityServer /** * Clears the applied resource pack field. */ - public abstract void clearAppliedResourcePacks(); + public final void clearAppliedResourcePacks() { + packCallbacks.clear(); + doClearAppliedResourcePacks(); + } + + /** + * Clears the applied resource pack field. + */ + protected abstract void doClearAppliedResourcePacks(); public abstract boolean remove(final UUID id); @@ -93,9 +112,14 @@ protected ResourcePackHandler(final ConnectedPlayer player, final VelocityServer * empty. */ public void queueResourcePack(final @NotNull ResourcePackRequest request) { + ResourcePackCallback callback = request.callback(); + boolean trackCallback = callback != ResourcePackCallback.noOp(); for (final net.kyori.adventure.resource.ResourcePackInfo pack : request.packs()) { final ResourcePackInfo resourcePackInfo = VelocityResourcePackInfo.fromAdventureRequest(request, pack); this.checkAlreadyAppliedPack(resourcePackInfo.getHash()); + if (trackCallback) { + packCallbacks.put(resourcePackInfo.getId(), callback); + } queueResourcePack(resourcePackInfo); } } @@ -164,6 +188,37 @@ protected boolean handleResponseResult( return handled; } + /** + * Invokes the Adventure {@link ResourcePackCallback} (if any) registered for the given pack + * UUID via {@code sendResourcePacks(ResourcePackRequest)}, then evicts the entry on a terminal + * status. Called by the per-version handlers when a {@code ResourcePackResponsePacket} arrives, + * before the {@link PlayerResourcePackStatusEvent} fire so the two cannot observe each other + * mid-flight. Callback execution is dispatched asynchronously off the player's connection event + * loop, since slow plugin callback handlers would otherwise stall the player's IO thread. + * + * @param uuid the pack UUID from the client response + * @param status the Velocity-side status reported by the client + * @return a future that completes once the registered callback returns, or an already-completed + * future when no callback was registered + */ + protected CompletableFuture dispatchPackCallback(@NotNull UUID uuid, + @NotNull PlayerResourcePackStatusEvent.Status status) { + ResourcePackCallback callback = status.isIntermediate() + ? packCallbacks.get(uuid) + : packCallbacks.remove(uuid); + if (callback == null) { + return CompletableFuture.completedFuture(null); + } + + return CompletableFuture.runAsync(() -> { + try { + callback.packEventReceived(uuid, status.adventureStatus(), player); + } catch (Throwable t) { + LOGGER.error("Couldn't pass resource pack callback for pack {} to {}", uuid, player, t); + } + }); + } + /** * Check if a pack has already been applied. *