1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-30 20:29:19 +00:00

Resolve issues related to re-downloading of failed remote resource packs

This commit is contained in:
onebeastchris
2025-07-23 23:46:43 +02:00
parent 1f23d1d1e2
commit 7a154504cd
6 changed files with 85 additions and 71 deletions

View File

@@ -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());

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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;
});
}
}

View File

@@ -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<Path, Map<UUID, Resour
* If a client rejects such a pack, it falls back to the old method, and Geyser serves a cached variant.
*/
private static final Cache<String, UrlPackCodec> 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<Path, Map<UUID, Resour
*
* @param codec the codec of the resource pack that wasn't successfully downloaded by a Bedrock client.
*/
public static void testRemotePack(GeyserSession session, UrlPackCodec codec, UUID packId, String packVersion) {
public static void testRemotePack(GeyserSession session, GeyserUrlPackCodec codec, ResourcePackHolder holder) {
if (CACHED_FAILED_PACKS.getIfPresent(codec.url()) == null) {
String url = codec.url();
CACHED_FAILED_PACKS.put(url, codec);
CACHED_FAILED_PACKS.put(codec.url(), codec);
GeyserImpl.getInstance().getLogger().warning(
"Bedrock client (%s, playing on %s) was not able to download the resource pack at %s. Checking for changes now:"
.formatted(session.bedrockUsername(), session.getClientData().getDeviceOs().name(), codec.url())
);
downloadPack(codec.url(), true).whenComplete((pathPackCodec, e) -> {
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);
}

View File

@@ -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);