From 7a154504cd721d6389e70dc326c97f70f637720d Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Wed, 23 Jul 2025 23:46:43 +0200 Subject: [PATCH] Resolve issues related to re-downloading of failed remote resource packs --- .../geyser/network/UpstreamPacketHandler.java | 15 ++--- .../geyser/pack/GeyserResourcePack.java | 2 + .../geyser/pack/ResourcePackHolder.java | 18 +++++ .../geyser/pack/url/GeyserUrlPackCodec.java | 49 ++++++++++++++ .../registry/loader/ResourcePackLoader.java | 67 ++----------------- .../org/geysermc/geyser/util/WebUtils.java | 5 +- 6 files changed, 85 insertions(+), 71 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index e04b23e96..e21429b32 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -60,11 +60,11 @@ import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePackManifest; -import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.api.pack.option.ResourcePackOption; import org.geysermc.geyser.event.type.SessionLoadResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.ResourcePackHolder; +import org.geysermc.geyser.pack.url.GeyserUrlPackCodec; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.loader.ResourcePackLoader; @@ -344,21 +344,18 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { return; } - ResourcePack pack = holder.pack(); - ResourcePackChunkDataPacket data = new ResourcePackChunkDataPacket(); - PackCodec codec = pack.codec(); - + PackCodec codec = holder.codec(); // If a remote pack ends up here, that usually implies that a client was not able to download the pack - if (codec instanceof UrlPackCodec urlPackCodec) { - ResourcePackLoader.testRemotePack(session, urlPackCodec, packet.getPackId(), packet.getPackVersion()); - - if (!resourcePackLoadEvent.value(pack.uuid(), ResourcePackOption.Type.FALLBACK, true)) { + if (codec instanceof GeyserUrlPackCodec urlPackCodec) { + ResourcePackLoader.testRemotePack(session, urlPackCodec, holder); + if (!resourcePackLoadEvent.value(holder.uuid(), ResourcePackOption.Type.FALLBACK, true)) { session.disconnect("Unable to provide downloaded resource pack. Contact an administrator!"); currentlySendingChunks = false; return; } } + ResourcePackChunkDataPacket data = new ResourcePackChunkDataPacket(); data.setChunkIndex(packet.getChunkIndex()); data.setProgress((long) packet.getChunkIndex() * GeyserResourcePack.CHUNK_SIZE); data.setPackVersion(packet.getPackVersion()); diff --git a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java index 1afd1a05a..61180b7dd 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java +++ b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.pack; +import lombok.With; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.ResourcePack; @@ -32,6 +33,7 @@ import org.geysermc.geyser.api.pack.ResourcePackManifest; import java.util.Objects; +@With public record GeyserResourcePack( @NonNull PackCodec codec, @NonNull ResourcePackManifest manifest, diff --git a/core/src/main/java/org/geysermc/geyser/pack/ResourcePackHolder.java b/core/src/main/java/org/geysermc/geyser/pack/ResourcePackHolder.java index f475127e7..6bed91342 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/ResourcePackHolder.java +++ b/core/src/main/java/org/geysermc/geyser/pack/ResourcePackHolder.java @@ -25,11 +25,17 @@ package org.geysermc.geyser.pack; +import lombok.With; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.api.pack.option.PriorityOption; import org.geysermc.geyser.pack.option.OptionHolder; +import java.util.UUID; + +@With public record ResourcePackHolder( @NonNull GeyserResourcePack pack, @NonNull OptionHolder optionHolder @@ -42,4 +48,16 @@ public record ResourcePackHolder( public ResourcePack resourcePack() { return this.pack; } + + public PackCodec codec() { + return this.pack.codec(); + } + + public UUID uuid() { + return this.pack.uuid(); + } + + public ResourcePackManifest.Version version() { + return pack.manifest().header().version(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java b/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java index ea0810493..47c099540 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java +++ b/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java @@ -27,10 +27,14 @@ package org.geysermc.geyser.pack.url; import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.ResourcePackHolder; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.loader.ResourcePackLoader; +import org.geysermc.geyser.text.GeyserLocale; import java.io.IOException; import java.nio.channels.SeekableByteChannel; @@ -46,6 +50,12 @@ public class GeyserUrlPackCodec extends UrlPackCodec { this.url = url; } + private GeyserUrlPackCodec(@NonNull String url, PathPackCodec fallback) { + Objects.requireNonNull(url); + this.fallback = fallback; + this.url = url; + } + @Override public byte @NonNull [] sha256() { Objects.requireNonNull(fallback, "must call #create() before attempting to get the sha256!"); @@ -87,4 +97,43 @@ public class GeyserUrlPackCodec extends UrlPackCodec { public @NonNull String url() { return this.url; } + + /** + * Tests whether Geyser's "mirror" of the remote pack needs to be updated. + * This is triggered if a Bedrock client is unable to download a pack + * @param holder the current resource pack holder with the "originally" known manifest + */ + public void testForChanges(ResourcePackHolder holder) { + ResourcePackLoader.downloadPack(url, true) + .thenAccept(backingPathCodec -> { + GeyserResourcePack updatedPack = ResourcePackLoader.readPack(backingPathCodec.path()) + .contentKey(holder.pack().contentKey()) + .build(); + if (updatedPack.uuid().equals(holder.uuid())) { + var currentVersion = holder.version().toString(); + var updatedVersion = updatedPack.manifest().header().version().toString(); + if (currentVersion.equals(updatedVersion)) { + GeyserImpl.getInstance().getLogger().info("No version or pack change detected: Was the resource pack server down?"); + return; + } else { + GeyserImpl.getInstance().getLogger().info("Detected a new resource pack version (%s, old version %s) for pack at %s!" + .formatted(currentVersion, updatedVersion, url)); + } + } else { + GeyserImpl.getInstance().getLogger().info("Detected a new resource pack at the url %s!".formatted(url)); + Registries.RESOURCE_PACKS.get().remove(holder.uuid()); + } + + // Update to new url pack codec (same url, updated fallback), and keep content key + GeyserResourcePack pack = updatedPack.withCodec(new GeyserUrlPackCodec(url, backingPathCodec)); + // Keep the pack options that were previously set + Registries.RESOURCE_PACKS.get().put(holder.uuid(), holder.withPack(pack)); + + }) + .exceptionally(throwable -> { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), throwable); + Registries.RESOURCE_PACKS.get().remove(holder.uuid()); + return null; + }); + } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java index 1a35ccadb..45d00de85 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java @@ -62,7 +62,6 @@ import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -80,7 +79,7 @@ public class ResourcePackLoader implements RegistryLoader CACHED_FAILED_PACKS = CacheBuilder.newBuilder() - .expireAfterWrite(1, TimeUnit.HOURS) + .expireAfterWrite(15, TimeUnit.MINUTES) .build(); static final PathMatcher PACK_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**.{zip,mcpack}"); @@ -268,77 +267,23 @@ public class ResourcePackLoader implements RegistryLoader { - if (e != null) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); - if (GeyserImpl.getInstance().getLogger().isDebug()) { - e.printStackTrace(); - if (pathPackCodec != null) { - deleteFile(pathPackCodec.path()); - } - return; - } - } - - GeyserResourcePack newPack = readPack(pathPackCodec.path()).build(); - if (newPack.uuid().equals(packId)) { - if (packVersion.equals(newPack.manifest().header().version().toString())) { - GeyserImpl.getInstance().getLogger().info("No version or pack change detected: Was the resource pack server down?"); - } else { - GeyserImpl.getInstance().getLogger().info("Detected a new resource pack version (%s, old version %s) for pack at %s!" - .formatted(packVersion, newPack.manifest().header().version().toString(), url)); - } - } else { - GeyserImpl.getInstance().getLogger().info("Detected a new resource pack at the url %s!".formatted(url)); - } - - // This should be safe to do as we're not directly using registries to read packs. - // Instead, they're cached per-session in the SessionLoadResourcePacks event - Registries.RESOURCE_PACKS.get().remove(packId); - Registries.RESOURCE_PACKS.get().put(newPack.uuid(), ResourcePackHolder.of(newPack)); - - if (codec instanceof GeyserUrlPackCodec geyserUrlPackCodec - && geyserUrlPackCodec.getFallback() != null) { - Path path = geyserUrlPackCodec.getFallback().path(); - try { - GeyserImpl.getInstance().getScheduledThread().schedule(() -> { - CACHED_FAILED_PACKS.invalidate(codec.url()); - deleteFile(path); - }, 5, TimeUnit.MINUTES); - } catch (RejectedExecutionException exception) { - // No scheduling here, probably because we're shutting down? - deleteFile(path); - } - } - }); + codec.testForChanges(holder); } } - private static void deleteFile(Path path) { - if (path.toFile().exists()) { - try { - Files.delete(path); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Unable to delete old pack! " + e.getMessage()); - e.printStackTrace(); - } - } - } - - public static CompletableFuture<@NonNull PathPackCodec> downloadPack(String url, boolean testing) throws IllegalArgumentException { + public static CompletableFuture<@NonNull PathPackCodec> downloadPack(String url, boolean force) throws IllegalArgumentException { return CompletableFuture.supplyAsync(() -> { Path path; try { - path = WebUtils.downloadRemotePack(url, testing); + path = WebUtils.downloadRemotePack(url, force); } catch (Throwable e) { throw new CompletionException(e); } diff --git a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java index 8bac97b1b..41925b809 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -50,6 +50,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -223,7 +224,9 @@ public class WebUtils { con.getHeaderField("ETag"), String.valueOf(con.getLastModified()), downloadLocation.getFileName().toString() - )); + ), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); packMetadata.toFile().setLastModified(System.currentTimeMillis()); } catch (IOException e) { Files.delete(packMetadata);