From 68516a8b0b102a300c9c385f23e6a7002f5db7be Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 10 Oct 2023 15:26:13 +0200 Subject: [PATCH 01/35] Initial stab at implementing 1.20.30's new CDN feature for resource packs --- .../SessionLoadResourcePacksEvent.java | 17 ++++++++- .../geyser/api/pack/ResourcePackCDNEntry.java | 35 +++++++++++++++++++ .../configuration/GeyserConfiguration.java | 3 ++ .../GeyserJacksonConfiguration.java | 17 +++++++-- .../SessionLoadResourcePacksEventImpl.java | 33 +++++++++++++++-- .../geysermc/geyser/network/GameProtocol.java | 4 +++ .../geyser/network/UpstreamPacketHandler.java | 27 +++++++------- core/src/main/resources/config.yml | 9 +++++ 8 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index c2f1cd42770..2fa9582d7a4 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -29,6 +29,7 @@ import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import java.util.List; import java.util.UUID; @@ -48,6 +49,13 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { */ public abstract @NonNull List resourcePacks(); + /** + * Gets an unmodifiable list of {@link ResourcePackCDNEntry}s that will be sent to the client. + * + * @return an unmodifiable list of resource pack CDN entries that will be sent to the client. + */ + public abstract @NonNull List cdnEntries(); + /** * Registers a {@link ResourcePack} to be sent to the client. * @@ -58,7 +66,14 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { public abstract boolean register(@NonNull ResourcePack resourcePack); /** - * Unregisters a resource pack from being sent to the client. + * Registers a {@link ResourcePackCDNEntry} to be sent to the client. + * + * @param entry a resource pack CDN entry that will be sent to the client. + */ + public abstract boolean register(@NonNull ResourcePackCDNEntry entry); + + /** + * Unregisters a {@link ResourcePack} or {@link ResourcePackCDNEntry} from being sent to the client. * * @param uuid the UUID of the resource pack * @return true whether the resource pack was removed from the list of resource packs. diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java new file mode 100644 index 00000000000..81fa56bab33 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack; + +import java.util.UUID; + +public record ResourcePackCDNEntry( + String url, + UUID uuid +) { +} + diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java index e36ec819b5e..1b3b895952e 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -30,6 +30,7 @@ import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.BedrockListener; import org.geysermc.geyser.api.network.RemoteServer; +import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import org.geysermc.geyser.network.CIDRMatcher; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.text.GeyserLocale; @@ -96,6 +97,8 @@ public interface GeyserConfiguration { boolean isForceResourcePacks(); + List getCDNResourcePacks(); + boolean isXboxAchievementsEnabled(); int getCacheImages(); diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java index 268304844f8..bee04119621 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -36,15 +36,14 @@ import lombok.Setter; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.network.AuthType; +import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import org.geysermc.geyser.network.CIDRMatcher; import org.geysermc.geyser.text.AsteriskSerializer; import org.geysermc.geyser.text.GeyserLocale; import java.io.IOException; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; @Getter @@ -136,6 +135,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("force-resource-packs") private boolean forceResourcePacks = true; + @JsonProperty("cdn-resource-packs") + private Map cdnResourcePacks = new HashMap<>(); + @JsonProperty("xbox-achievements-enabled") private boolean xboxAchievementsEnabled = false; @@ -343,4 +345,13 @@ public AuthType deserialize(JsonParser p, DeserializationContext ctxt) throws IO return AuthType.getByName(p.getValueAsString()); } } + + @Override + public List getCDNResourcePacks() { + List entries = new ArrayList<>(); + for (Map.Entry entry : cdnResourcePacks.entrySet()) { + entries.add(new ResourcePackCDNEntry(entry.getValue(), entry.getKey())); + } + return entries; + } } diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java index 5ed0f8d225a..b82f7b98d98 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -28,6 +28,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import org.geysermc.geyser.session.GeyserSession; import java.util.List; @@ -38,9 +39,12 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE private final Map packs; - public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap) { + private final List cdnEntries; + + public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap, List cdnEntries) { super(session); this.packs = packMap; + this.cdnEntries = cdnEntries; } public @NonNull Map getPacks() { @@ -52,18 +56,41 @@ public SessionLoadResourcePacksEventImpl(GeyserSession session, Map cdnEntries() { + return List.copyOf(cdnEntries); + } + @Override public boolean register(@NonNull ResourcePack resourcePack) { String packID = resourcePack.manifest().header().uuid().toString(); - if (packs.containsValue(resourcePack) || packs.containsKey(packID)) { + if (packs.containsValue(resourcePack) || packs.containsKey(packID) + || !cdnEntries.isEmpty() && cdnEntries.stream().anyMatch(entry -> entry.uuid().toString().equals(packID))) { return false; } packs.put(resourcePack.manifest().header().uuid().toString(), resourcePack); return true; } + @Override + public boolean register(@NonNull ResourcePackCDNEntry entry) { + UUID packID = entry.uuid(); + if (packs.containsKey(packID.toString()) || cdnEntries.contains(entry) + || !cdnEntries.isEmpty() && cdnEntries.stream().anyMatch(cdnEntry -> cdnEntry.uuid().equals(packID))) { + return false; + } + cdnEntries.add(entry); + return true; + } + @Override public boolean unregister(@NonNull UUID uuid) { - return packs.remove(uuid.toString()) != null; + if (packs.containsKey(uuid.toString())) { + return packs.remove(uuid.toString()) != null; + } else if (!cdnEntries.isEmpty() && cdnEntries.stream().anyMatch(entry -> entry.uuid().equals(uuid))) { + return cdnEntries.removeIf(entry -> entry.uuid().equals(uuid)); + } else { + return false; + } } } diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index fb9684f77d2..c960b788fd3 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -93,6 +93,10 @@ public static boolean isPre1_20_10(GeyserSession session) { return session.getUpstream().getProtocolVersion() < Bedrock_v594.CODEC.getProtocolVersion(); } + public static boolean isPre1_20_30(GeyserSession session) { + return session.getUpstream().getProtocolVersion() < Bedrock_v618.CODEC.getProtocolVersion(); + } + /** * Gets the {@link PacketCodec} for Minecraft: Java Edition. * 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 361aaffb98d..2f5961c391a 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -31,26 +31,14 @@ import org.cloudburstmc.protocol.bedrock.data.ExperimentData; import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm; import org.cloudburstmc.protocol.bedrock.data.ResourcePackType; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; -import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; -import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; -import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; -import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkDataPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkRequestPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackDataInfoPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; -import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; +import org.cloudburstmc.protocol.bedrock.packet.*; import org.cloudburstmc.protocol.common.PacketSignal; import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; 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.ResourcePackCDNEntry; import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.event.type.SessionLoadResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; @@ -178,7 +166,8 @@ public PacketSignal handle(LoginPacket loginPacket) { geyser.getSessionManager().addPendingSession(session); - this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get())); + GeyserImpl.getInstance().getLogger().error(geyser.getConfig().getCDNResourcePacks().toString()); + this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get()), geyser.getConfig().getCDNResourcePacks()); this.geyser.eventBus().fire(this.resourcePackLoadEvent); ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); @@ -189,6 +178,14 @@ public PacketSignal handle(LoginPacket loginPacket) { header.uuid().toString(), header.version().toString(), codec.size(), pack.contentKey(), "", header.uuid().toString(), false, false)); } + + // Add CDN entries if the client supports it + if (!GameProtocol.isPre1_20_30(session)) { + for (ResourcePackCDNEntry entry : this.resourcePackLoadEvent.cdnEntries()) { + GeyserImpl.getInstance().getLogger().error("Adding CDN entry: " + entry.url() + " for " + entry.uuid()); + resourcePacksInfo.getCDNEntries().add(new ResourcePacksInfoPacket.CDNEntry(entry.uuid().toString(), entry.url())); + } + } resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); session.sendUpstreamPacket(resourcePacksInfo); diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 8e4db5e382e..c088fc285da 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -175,6 +175,15 @@ above-bedrock-nether-building: false # want to download the resource packs. force-resource-packs: true +# A list of links to send to the client to download resource packs from. +# These must be direct links, and you need to include the resource pack uuid +# You can find the uuid in the manifest.json file inside the resource pack zip. +cdn-resource-packs: + # Example: GeyserOptionalPack + { + e5f5c938-a701-11eb-b2a3-047d7bb283ba : "https://ci.opencollab.dev/job/GeyserMC/job/GeyserOptionalPack/job/master/lastSuccessfulBuild/artifact/GeyserOptionalPack.mcpack" + } + # Allows Xbox achievements to be unlocked. # THIS DISABLES ALL COMMANDS FROM SUCCESSFULLY RUNNING FOR BEDROCK IN-GAME, as otherwise Bedrock thinks you are cheating. xbox-achievements-enabled: false From 02d6473dc52558bea9811c2b3b3fe7a3571c1a8c Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 10 Oct 2023 15:48:25 +0200 Subject: [PATCH 02/35] Small tweaks: record formatting/javadocs --- .../geyser/api/pack/ResourcePackCDNEntry.java | 13 +++++++++---- .../geyser/network/UpstreamPacketHandler.java | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java index 81fa56bab33..620147f85c9 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java @@ -27,9 +27,14 @@ import java.util.UUID; -public record ResourcePackCDNEntry( - String url, - UUID uuid -) { +/** + * Represents a CDN entry for a resource pack. + * The URL must be a direct download link to a Bedrock edition resource pack. + * The UUID must be the UUID of the resource pack. + * + * @param url URL from which the pack should be downloaded + * @param uuid UUID of the pack + */ +public record ResourcePackCDNEntry(String url, UUID uuid) { } 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 2f5961c391a..f11f938b4ed 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -31,7 +31,20 @@ import org.cloudburstmc.protocol.bedrock.data.ExperimentData; import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm; import org.cloudburstmc.protocol.bedrock.data.ResourcePackType; -import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; +import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; +import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; +import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket; +import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; +import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkDataPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkRequestPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackDataInfoPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; import org.cloudburstmc.protocol.common.PacketSignal; import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; @@ -182,10 +195,10 @@ public PacketSignal handle(LoginPacket loginPacket) { // Add CDN entries if the client supports it if (!GameProtocol.isPre1_20_30(session)) { for (ResourcePackCDNEntry entry : this.resourcePackLoadEvent.cdnEntries()) { - GeyserImpl.getInstance().getLogger().error("Adding CDN entry: " + entry.url() + " for " + entry.uuid()); resourcePacksInfo.getCDNEntries().add(new ResourcePacksInfoPacket.CDNEntry(entry.uuid().toString(), entry.url())); } } + resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); session.sendUpstreamPacket(resourcePacksInfo); From dbfc153b042f81a2633ebfc45aef9a3782781b9c Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 10 Oct 2023 21:45:42 +0200 Subject: [PATCH 03/35] - Don't require configuring the packId, just the link instead - Deprecate GeyserLoadResourcePacksEvent in favor of GeyserDefineResourcePacksEvent - Load CDNentries properly --- .../SessionLoadResourcePacksEvent.java | 4 +- .../GeyserDefineResourcePacksEvent.java | 79 ++++++++++++++++ .../GeyserLoadResourcePacksEvent.java | 5 +- .../configuration/GeyserConfiguration.java | 3 +- .../GeyserJacksonConfiguration.java | 17 +--- .../GeyserDefineResourcePacksEventImpl.java | 93 +++++++++++++++++++ .../SessionLoadResourcePacksEventImpl.java | 23 +++-- .../geyser/network/UpstreamPacketHandler.java | 4 +- .../registry/loader/ResourcePackLoader.java | 40 ++++++++ core/src/main/resources/config.yml | 7 +- 10 files changed, 239 insertions(+), 36 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java create mode 100644 core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index 2fa9582d7a4..43251d74b62 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -75,8 +75,8 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { /** * Unregisters a {@link ResourcePack} or {@link ResourcePackCDNEntry} from being sent to the client. * - * @param uuid the UUID of the resource pack - * @return true whether the resource pack was removed from the list of resource packs. + * @param uuid the UUID of the resource pack/CDN entry to remove. + * @return true whether the resource pack/CDN entry was removed successfully. */ public abstract boolean unregister(@NonNull UUID uuid); } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java new file mode 100644 index 00000000000..dc673338f81 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.Event; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; + +import java.util.List; +import java.util.UUID; + +/** + * Called when {@link ResourcePack}'s and {@link ResourcePackCDNEntry}'s are loaded within Geyser. + * + */ +public abstract class GeyserDefineResourcePacksEvent implements Event { + + /** + * Gets an unmodifiable list of {@link ResourcePack}s that will be sent to clients. + * + * @return an unmodifiable list of resource packs that will be sent to clients. + */ + public abstract @NonNull List resourcePacks(); + + /** + * Gets an unmodifiable list of {@link ResourcePackCDNEntry}s that will be sent to clients. + * + * @return an unmodifiable list of resource pack CDN entries that will be sent to clients. + */ + public abstract @NonNull List cdnEntries(); + + /** + * Registers a {@link ResourcePack} to be sent to clients. + * + * @param resourcePack a resource pack that will be sent to clients. + * @return true if the resource pack was added successfully, + * or false if already present + */ + public abstract boolean register(@NonNull ResourcePack resourcePack); + + /** + * Registers a {@link ResourcePackCDNEntry} to be sent to clients. + * + * @param entry a resource pack CDN entry that will be sent to clients. + */ + public abstract boolean register(@NonNull ResourcePackCDNEntry entry); + + /** + * Unregisters a {@link ResourcePack} or {@link ResourcePackCDNEntry} from being sent to clients. + * + * @param uuid the UUID of the resource pack/CDN entry to remove. + * @return true whether the resource pack/CDN entry was removed successfully. + */ + public abstract boolean unregister(@NonNull UUID uuid); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java index e9b283ecbbd..d75e0de08ff 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java @@ -33,8 +33,11 @@ /** * Called when resource packs are loaded within Geyser. + * @deprecated Use {@link GeyserDefineResourcePacksEvent} instead. * * @param resourcePacks a mutable list of the currently listed resource packs */ -public record GeyserLoadResourcePacksEvent(@NonNull List resourcePacks) implements Event { + +@Deprecated(forRemoval = true) +public record GeyserLoadResourcePacksEvent(@Deprecated @NonNull List resourcePacks) implements Event { } diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java index 1b3b895952e..c16cc494c23 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -30,7 +30,6 @@ import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.BedrockListener; import org.geysermc.geyser.api.network.RemoteServer; -import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import org.geysermc.geyser.network.CIDRMatcher; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.text.GeyserLocale; @@ -97,7 +96,7 @@ public interface GeyserConfiguration { boolean isForceResourcePacks(); - List getCDNResourcePacks(); + List getCdnResourcePacks(); boolean isXboxAchievementsEnabled(); diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java index bee04119621..8211645ef2f 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -36,14 +36,16 @@ import lombok.Setter; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.network.AuthType; -import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import org.geysermc.geyser.network.CIDRMatcher; import org.geysermc.geyser.text.AsteriskSerializer; import org.geysermc.geyser.text.GeyserLocale; import java.io.IOException; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; @Getter @@ -136,7 +138,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration private boolean forceResourcePacks = true; @JsonProperty("cdn-resource-packs") - private Map cdnResourcePacks = new HashMap<>(); + private List cdnResourcePacks = new ArrayList<>(); @JsonProperty("xbox-achievements-enabled") private boolean xboxAchievementsEnabled = false; @@ -345,13 +347,4 @@ public AuthType deserialize(JsonParser p, DeserializationContext ctxt) throws IO return AuthType.getByName(p.getValueAsString()); } } - - @Override - public List getCDNResourcePacks() { - List entries = new ArrayList<>(); - for (Map.Entry entry : cdnResourcePacks.entrySet()) { - entries.add(new ResourcePackCDNEntry(entry.getValue(), entry.getKey())); - } - return entries; - } } diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java new file mode 100644 index 00000000000..cf7f024fea8 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.event.type; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent { + + private final Map packs; + private final Map cdnEntries; + + public GeyserDefineResourcePacksEventImpl(Map packMap, List cdnEntries) { + this.packs = packMap; + this.cdnEntries = new HashMap<>(); + cdnEntries.forEach(entry -> this.cdnEntries.put(entry.uuid().toString(), entry)); + } + + public @NonNull Map getPacks() { + return packs; + } + + @Override + public @NonNull List resourcePacks() { + return List.copyOf(packs.values()); + } + + @Override + public @NonNull List cdnEntries() { + return List.copyOf(cdnEntries.values()); + } + + @Override + public boolean register(@NonNull ResourcePack resourcePack) { + String packID = resourcePack.manifest().header().uuid().toString(); + if (packs.containsValue(resourcePack) || packs.containsKey(packID) || cdnEntries.containsKey(packID)) { + return false; + } + packs.put(resourcePack.manifest().header().uuid().toString(), resourcePack); + return true; + } + + @Override + public boolean register(@NonNull ResourcePackCDNEntry entry) { + String packID = entry.uuid().toString(); + if (packs.containsKey(packID) || cdnEntries.containsValue(entry) || cdnEntries.containsKey(packID)) { + return false; + } + cdnEntries.put(packID, entry); + return true; + } + + @Override + public boolean unregister(@NonNull UUID uuid) { + if (packs.containsKey(uuid.toString())) { + return packs.remove(uuid.toString()) != null; + } else if (cdnEntries.containsKey(uuid.toString())) { + return cdnEntries.remove(uuid.toString()) != null; + } else { + return false; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java index b82f7b98d98..974e704fd7b 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -31,6 +31,7 @@ import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import org.geysermc.geyser.session.GeyserSession; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -38,13 +39,13 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent { private final Map packs; - - private final List cdnEntries; + private final Map cdnEntries; public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap, List cdnEntries) { super(session); this.packs = packMap; - this.cdnEntries = cdnEntries; + this.cdnEntries = new HashMap<>(); + cdnEntries.forEach(entry -> this.cdnEntries.put(entry.uuid().toString(), entry)); } public @NonNull Map getPacks() { @@ -58,14 +59,13 @@ public SessionLoadResourcePacksEventImpl(GeyserSession session, Map cdnEntries() { - return List.copyOf(cdnEntries); + return List.copyOf(cdnEntries.values()); } @Override public boolean register(@NonNull ResourcePack resourcePack) { String packID = resourcePack.manifest().header().uuid().toString(); - if (packs.containsValue(resourcePack) || packs.containsKey(packID) - || !cdnEntries.isEmpty() && cdnEntries.stream().anyMatch(entry -> entry.uuid().toString().equals(packID))) { + if (packs.containsValue(resourcePack) || packs.containsKey(packID) || cdnEntries.containsKey(packID)) { return false; } packs.put(resourcePack.manifest().header().uuid().toString(), resourcePack); @@ -74,12 +74,11 @@ public boolean register(@NonNull ResourcePack resourcePack) { @Override public boolean register(@NonNull ResourcePackCDNEntry entry) { - UUID packID = entry.uuid(); - if (packs.containsKey(packID.toString()) || cdnEntries.contains(entry) - || !cdnEntries.isEmpty() && cdnEntries.stream().anyMatch(cdnEntry -> cdnEntry.uuid().equals(packID))) { + String packID = entry.uuid().toString(); + if (packs.containsKey(packID) || cdnEntries.containsKey(packID) || cdnEntries.containsValue(entry)) { return false; } - cdnEntries.add(entry); + cdnEntries.put(packID, entry); return true; } @@ -87,8 +86,8 @@ public boolean register(@NonNull ResourcePackCDNEntry entry) { public boolean unregister(@NonNull UUID uuid) { if (packs.containsKey(uuid.toString())) { return packs.remove(uuid.toString()) != null; - } else if (!cdnEntries.isEmpty() && cdnEntries.stream().anyMatch(entry -> entry.uuid().equals(uuid))) { - return cdnEntries.removeIf(entry -> entry.uuid().equals(uuid)); + } else if (cdnEntries.containsKey(uuid.toString())) { + return cdnEntries.remove(uuid.toString()) != null; } else { return false; } 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 f11f938b4ed..f4afc551a5d 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -57,6 +57,7 @@ import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.text.GeyserLocale; @@ -179,8 +180,7 @@ public PacketSignal handle(LoginPacket loginPacket) { geyser.getSessionManager().addPendingSession(session); - GeyserImpl.getInstance().getLogger().error(geyser.getConfig().getCDNResourcePacks().toString()); - this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get()), geyser.getConfig().getCDNResourcePacks()); + this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get()), ResourcePackLoader.RESOURCE_PACK_CDN_ENTRY_LIST); this.geyser.eventBus().fire(this.resourcePackLoadEvent); ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); 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 800a3d22c6e..f9b962a64d9 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 @@ -28,12 +28,15 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; +import org.geysermc.geyser.event.type.GeyserDefineResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePackManifest; import org.geysermc.geyser.pack.SkullResourcePackManager; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; +import org.geysermc.geyser.util.WebUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -45,6 +48,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -58,8 +62,12 @@ public class ResourcePackLoader implements RegistryLoader RESOURCE_PACK_CDN_ENTRY_LIST = new ArrayList<>(); + /** * Loop through the packs directory and locate valid resource pack files */ @@ -94,6 +102,33 @@ public Map load(Path directory) { resourcePacks.add(skullResourcePack); } + // Download CDN packs to get the pack uuid's + if (!Files.exists(CACHED_CDN_PACKS_DIRECTORY)) { + try { + Files.createDirectories(CACHED_CDN_PACKS_DIRECTORY); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not create cached packs directory", e); + } + } + + List cdnPacks = GeyserImpl.getInstance().getConfig().getCdnResourcePacks(); + for (String url: cdnPacks) { + int packHash = url.hashCode(); + Path cachedPath = CACHED_CDN_PACKS_DIRECTORY.resolve(packHash + ".zip"); + WebUtils.downloadFile(url, cachedPath.toString()); + + ResourcePack cdnpack = readPack(cachedPath); + UUID uuid = cdnpack.manifest().header().uuid(); + + RESOURCE_PACK_CDN_ENTRY_LIST.add(new ResourcePackCDNEntry(url, uuid)); + + try { + Files.delete(cachedPath); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not delete cached pack", e); + } + } + GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); GeyserImpl.getInstance().eventBus().fire(event); @@ -105,6 +140,11 @@ public Map load(Path directory) { e.printStackTrace(); } } + + GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap, RESOURCE_PACK_CDN_ENTRY_LIST); + packMap = defineEvent.getPacks(); + RESOURCE_PACK_CDN_ENTRY_LIST = defineEvent.cdnEntries(); + return packMap; } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index c088fc285da..575fed0df2c 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -176,13 +176,10 @@ above-bedrock-nether-building: false force-resource-packs: true # A list of links to send to the client to download resource packs from. -# These must be direct links, and you need to include the resource pack uuid -# You can find the uuid in the manifest.json file inside the resource pack zip. +# These must be direct links to the resource pack, not a link to a page containing the resource pack. cdn-resource-packs: # Example: GeyserOptionalPack - { - e5f5c938-a701-11eb-b2a3-047d7bb283ba : "https://ci.opencollab.dev/job/GeyserMC/job/GeyserOptionalPack/job/master/lastSuccessfulBuild/artifact/GeyserOptionalPack.mcpack" - } + - "https://ci.opencollab.dev/job/GeyserMC/job/GeyserOptionalPack/job/master/lastSuccessfulBuild/artifact/GeyserOptionalPack.mcpack" # Allows Xbox achievements to be unlocked. # THIS DISABLES ALL COMMANDS FROM SUCCESSFULLY RUNNING FOR BEDROCK IN-GAME, as otherwise Bedrock thinks you are cheating. From f74d36a1f9413581a2f4784c1151041e4120897d Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 10 Oct 2023 22:08:46 +0200 Subject: [PATCH 04/35] Remove forRemoval --- .../geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java | 2 +- .../api/event/lifecycle/GeyserDefineResourcePacksEvent.java | 2 +- .../api/event/lifecycle/GeyserLoadResourcePacksEvent.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index 43251d74b62..8b05270d555 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -68,7 +68,7 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { /** * Registers a {@link ResourcePackCDNEntry} to be sent to the client. * - * @param entry a resource pack CDN entry that will be sent to the client. + * @param entry CDN entry that will be sent to the client to download a resource pack from. */ public abstract boolean register(@NonNull ResourcePackCDNEntry entry); diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java index dc673338f81..ba71985fd4a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -65,7 +65,7 @@ public abstract class GeyserDefineResourcePacksEvent implements Event { /** * Registers a {@link ResourcePackCDNEntry} to be sent to clients. * - * @param entry a resource pack CDN entry that will be sent to clients. + * @param entry CDN entry that will be sent to the client to download a resource pack from. */ public abstract boolean register(@NonNull ResourcePackCDNEntry entry); diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java index d75e0de08ff..52dcda46116 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java @@ -38,6 +38,6 @@ * @param resourcePacks a mutable list of the currently listed resource packs */ -@Deprecated(forRemoval = true) +@Deprecated public record GeyserLoadResourcePacksEvent(@Deprecated @NonNull List resourcePacks) implements Event { } From 94f2ea9b579b0350c97c669dd76eef8d784f1d78 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 10 Oct 2023 23:02:37 +0200 Subject: [PATCH 05/35] Rename cdn-resource-packs to resource-pack-urls, fix test, remove duplicate deprecation annotation --- .../lifecycle/GeyserDefineResourcePacksEvent.java | 1 - .../lifecycle/GeyserLoadResourcePacksEvent.java | 2 +- .../geyser/configuration/GeyserConfiguration.java | 2 +- .../configuration/GeyserJacksonConfiguration.java | 4 ++-- .../geyser/registry/loader/ResourcePackLoader.java | 12 ++++++------ core/src/main/resources/config.yml | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java index ba71985fd4a..62f02e8d25f 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -35,7 +35,6 @@ /** * Called when {@link ResourcePack}'s and {@link ResourcePackCDNEntry}'s are loaded within Geyser. - * */ public abstract class GeyserDefineResourcePacksEvent implements Event { diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java index 52dcda46116..98b34126e2b 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java @@ -39,5 +39,5 @@ */ @Deprecated -public record GeyserLoadResourcePacksEvent(@Deprecated @NonNull List resourcePacks) implements Event { +public record GeyserLoadResourcePacksEvent(@NonNull List resourcePacks) implements Event { } diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java index c16cc494c23..dae94cd2958 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -96,7 +96,7 @@ public interface GeyserConfiguration { boolean isForceResourcePacks(); - List getCdnResourcePacks(); + List getResourcePackUrls(); boolean isXboxAchievementsEnabled(); diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java index 8211645ef2f..7a99d4368a8 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -137,8 +137,8 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("force-resource-packs") private boolean forceResourcePacks = true; - @JsonProperty("cdn-resource-packs") - private List cdnResourcePacks = new ArrayList<>(); + @JsonProperty("resource-pack-urls") + private List resourcePackUrls = new ArrayList<>(); @JsonProperty("xbox-achievements-enabled") private boolean xboxAchievementsEnabled = false; 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 f9b962a64d9..1de225d3abd 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,8 +62,6 @@ public class ResourcePackLoader implements RegistryLoader RESOURCE_PACK_CDN_ENTRY_LIST = new ArrayList<>(); @@ -102,19 +100,21 @@ public Map load(Path directory) { resourcePacks.add(skullResourcePack); } + final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs"); + // Download CDN packs to get the pack uuid's - if (!Files.exists(CACHED_CDN_PACKS_DIRECTORY)) { + if (!Files.exists(cachedCdnPacksDirectory)) { try { - Files.createDirectories(CACHED_CDN_PACKS_DIRECTORY); + Files.createDirectories(cachedCdnPacksDirectory); } catch (IOException e) { GeyserImpl.getInstance().getLogger().error("Could not create cached packs directory", e); } } - List cdnPacks = GeyserImpl.getInstance().getConfig().getCdnResourcePacks(); + List cdnPacks = GeyserImpl.getInstance().getConfig().getResourcePackUrls(); for (String url: cdnPacks) { int packHash = url.hashCode(); - Path cachedPath = CACHED_CDN_PACKS_DIRECTORY.resolve(packHash + ".zip"); + Path cachedPath = cachedCdnPacksDirectory.resolve(packHash + ".zip"); WebUtils.downloadFile(url, cachedPath.toString()); ResourcePack cdnpack = readPack(cachedPath); diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 575fed0df2c..07f9d496043 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -177,7 +177,7 @@ force-resource-packs: true # A list of links to send to the client to download resource packs from. # These must be direct links to the resource pack, not a link to a page containing the resource pack. -cdn-resource-packs: +resource-pack-urls: # Example: GeyserOptionalPack - "https://ci.opencollab.dev/job/GeyserMC/job/GeyserOptionalPack/job/master/lastSuccessfulBuild/artifact/GeyserOptionalPack.mcpack" From fd69b0c381cd752b6808d08ef5adf918ccbfc798 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 10 Oct 2023 23:04:19 +0200 Subject: [PATCH 06/35] Rename CDN entry list --- .../geysermc/geyser/network/UpstreamPacketHandler.java | 2 +- .../geyser/registry/loader/ResourcePackLoader.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 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 f4afc551a5d..1ef6170a16c 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -180,7 +180,7 @@ public PacketSignal handle(LoginPacket loginPacket) { geyser.getSessionManager().addPendingSession(session); - this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get()), ResourcePackLoader.RESOURCE_PACK_CDN_ENTRY_LIST); + this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get()), ResourcePackLoader.RESOURCE_PACK_CDN_ENTRIES); this.geyser.eventBus().fire(this.resourcePackLoadEvent); ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); 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 1de225d3abd..32900d9c7aa 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 @@ -64,7 +64,7 @@ public class ResourcePackLoader implements RegistryLoader RESOURCE_PACK_CDN_ENTRY_LIST = new ArrayList<>(); + public static List RESOURCE_PACK_CDN_ENTRIES = new ArrayList<>(); /** * Loop through the packs directory and locate valid resource pack files @@ -120,7 +120,7 @@ public Map load(Path directory) { ResourcePack cdnpack = readPack(cachedPath); UUID uuid = cdnpack.manifest().header().uuid(); - RESOURCE_PACK_CDN_ENTRY_LIST.add(new ResourcePackCDNEntry(url, uuid)); + RESOURCE_PACK_CDN_ENTRIES.add(new ResourcePackCDNEntry(url, uuid)); try { Files.delete(cachedPath); @@ -141,9 +141,9 @@ public Map load(Path directory) { } } - GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap, RESOURCE_PACK_CDN_ENTRY_LIST); + GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap, RESOURCE_PACK_CDN_ENTRIES); packMap = defineEvent.getPacks(); - RESOURCE_PACK_CDN_ENTRY_LIST = defineEvent.cdnEntries(); + RESOURCE_PACK_CDN_ENTRIES = defineEvent.cdnEntries(); return packMap; } From c0227d3da1a427203dfbee98de045733df59ab1a Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Wed, 11 Oct 2023 21:09:14 +0200 Subject: [PATCH 07/35] Move loading cdn entries to separate function --- .../registry/loader/ResourcePackLoader.java | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) 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 32900d9c7aa..8a2f5abdfe5 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 @@ -100,34 +100,8 @@ public Map load(Path directory) { resourcePacks.add(skullResourcePack); } - final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs"); - - // Download CDN packs to get the pack uuid's - if (!Files.exists(cachedCdnPacksDirectory)) { - try { - Files.createDirectories(cachedCdnPacksDirectory); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not create cached packs directory", e); - } - } - - List cdnPacks = GeyserImpl.getInstance().getConfig().getResourcePackUrls(); - for (String url: cdnPacks) { - int packHash = url.hashCode(); - Path cachedPath = cachedCdnPacksDirectory.resolve(packHash + ".zip"); - WebUtils.downloadFile(url, cachedPath.toString()); - - ResourcePack cdnpack = readPack(cachedPath); - UUID uuid = cdnpack.manifest().header().uuid(); - - RESOURCE_PACK_CDN_ENTRIES.add(new ResourcePackCDNEntry(url, uuid)); - - try { - Files.delete(cachedPath); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not delete cached pack", e); - } - } + // Load CDN entries + loadCdnEntries(); GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); GeyserImpl.getInstance().eventBus().fire(event); @@ -200,4 +174,36 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", path.getFileName()), e); } } + + public void loadCdnEntries() { + final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs"); + + // Download CDN packs to get the pack uuid's + if (!Files.exists(cachedCdnPacksDirectory)) { + try { + Files.createDirectories(cachedCdnPacksDirectory); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not create cached packs directory", e); + return; + } + } + + List cdnPacks = GeyserImpl.getInstance().getConfig().getResourcePackUrls(); + for (String url: cdnPacks) { + int packHash = url.hashCode(); + Path cachedPath = cachedCdnPacksDirectory.resolve(packHash + ".zip"); + WebUtils.downloadFile(url, cachedPath.toString()); + + ResourcePack cdnpack = readPack(cachedPath); + UUID uuid = cdnpack.manifest().header().uuid(); + + RESOURCE_PACK_CDN_ENTRIES.add(new ResourcePackCDNEntry(url, uuid)); + + try { + Files.delete(cachedPath); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not delete cached pack", e); + } + } + } } From b9c5bddb975a04f5655ad7d56b12cbf444adf072 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 17 Oct 2023 19:06:11 +0200 Subject: [PATCH 08/35] Remove CDNEntry; those do not work as expected. Instead: - add UrlPackCodec & GeyserUrlPackCodec - try and provide the resource pack via a stream from the url - either because the client does not support packs via the url, or because it failed to get the packs --- .../SessionLoadResourcePacksEvent.java | 17 +-- .../GeyserDefineResourcePacksEvent.java | 19 +-- .../geysermc/geyser/api/pack/PackCodec.java | 12 ++ ...rcePackCDNEntry.java => UrlPackCodec.java} | 29 +++-- .../GeyserDefineResourcePacksEventImpl.java | 38 +----- .../SessionLoadResourcePacksEventImpl.java | 39 +----- .../geyser/network/UpstreamPacketHandler.java | 61 ++++------ .../geyser/pack/GeyserResourcePack.java | 3 +- .../geyser/pack/url/GeyserUrlPackCodec.java | 112 ++++++++++++++++++ .../loader/ProviderRegistryLoader.java | 3 + .../registry/loader/ResourcePackLoader.java | 54 +++++---- 11 files changed, 211 insertions(+), 176 deletions(-) rename api/src/main/java/org/geysermc/geyser/api/pack/{ResourcePackCDNEntry.java => UrlPackCodec.java} (73%) create mode 100644 core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index 8b05270d555..26909f046d6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -29,7 +29,6 @@ import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.pack.ResourcePack; -import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import java.util.List; import java.util.UUID; @@ -49,13 +48,6 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { */ public abstract @NonNull List resourcePacks(); - /** - * Gets an unmodifiable list of {@link ResourcePackCDNEntry}s that will be sent to the client. - * - * @return an unmodifiable list of resource pack CDN entries that will be sent to the client. - */ - public abstract @NonNull List cdnEntries(); - /** * Registers a {@link ResourcePack} to be sent to the client. * @@ -66,14 +58,7 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { public abstract boolean register(@NonNull ResourcePack resourcePack); /** - * Registers a {@link ResourcePackCDNEntry} to be sent to the client. - * - * @param entry CDN entry that will be sent to the client to download a resource pack from. - */ - public abstract boolean register(@NonNull ResourcePackCDNEntry entry); - - /** - * Unregisters a {@link ResourcePack} or {@link ResourcePackCDNEntry} from being sent to the client. + * Unregisters a {@link ResourcePack} from being sent to the client. * * @param uuid the UUID of the resource pack/CDN entry to remove. * @return true whether the resource pack/CDN entry was removed successfully. diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java index 62f02e8d25f..6da2b1f4f20 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -28,13 +28,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.event.Event; import org.geysermc.geyser.api.pack.ResourcePack; -import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import java.util.List; import java.util.UUID; /** - * Called when {@link ResourcePack}'s and {@link ResourcePackCDNEntry}'s are loaded within Geyser. + * Called when {@link ResourcePack}'s are loaded within Geyser. */ public abstract class GeyserDefineResourcePacksEvent implements Event { @@ -45,13 +44,6 @@ public abstract class GeyserDefineResourcePacksEvent implements Event { */ public abstract @NonNull List resourcePacks(); - /** - * Gets an unmodifiable list of {@link ResourcePackCDNEntry}s that will be sent to clients. - * - * @return an unmodifiable list of resource pack CDN entries that will be sent to clients. - */ - public abstract @NonNull List cdnEntries(); - /** * Registers a {@link ResourcePack} to be sent to clients. * @@ -62,14 +54,7 @@ public abstract class GeyserDefineResourcePacksEvent implements Event { public abstract boolean register(@NonNull ResourcePack resourcePack); /** - * Registers a {@link ResourcePackCDNEntry} to be sent to clients. - * - * @param entry CDN entry that will be sent to the client to download a resource pack from. - */ - public abstract boolean register(@NonNull ResourcePackCDNEntry entry); - - /** - * Unregisters a {@link ResourcePack} or {@link ResourcePackCDNEntry} from being sent to clients. + * Unregisters a {@link ResourcePack} from being sent to clients. * * @param uuid the UUID of the resource pack/CDN entry to remove. * @return true whether the resource pack/CDN entry was removed successfully. diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index 884129fa3f5..ff9f82c5a56 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -79,4 +79,16 @@ public abstract class PackCodec { public static PackCodec path(@NonNull Path path) { return GeyserApi.api().provider(PathPackCodec.class, path); } + + /** + * Creates a new pack provider from the given url and content key. + * + * @param url the url to create the pack provider from + * @param contentKey the content key, leave empty if pack is not encrypted + * @return the new pack provider + */ + @NonNull + public static PackCodec url(@NonNull String url, @NonNull String contentKey) { + return GeyserApi.api().provider(UrlPackCodec.class, url, contentKey); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java similarity index 73% rename from api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java rename to api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java index 620147f85c9..e6d1a5a243e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackCDNEntry.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java @@ -25,16 +25,23 @@ package org.geysermc.geyser.api.pack; -import java.util.UUID; +import org.checkerframework.checker.nullness.qual.NonNull; -/** - * Represents a CDN entry for a resource pack. - * The URL must be a direct download link to a Bedrock edition resource pack. - * The UUID must be the UUID of the resource pack. - * - * @param url URL from which the pack should be downloaded - * @param uuid UUID of the pack - */ -public record ResourcePackCDNEntry(String url, UUID uuid) { -} +public abstract class UrlPackCodec extends PackCodec { + /** + * Gets the URL of the resource pack. + * + * @return the URL of the resource pack + */ + @NonNull + public abstract String url(); + + /** + * If the remote pack has an encryption key, it must be specified here. + * Otherwise, leave empty. + * + */ + @NonNull + public abstract String contentKey(); +} diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java index cf7f024fea8..2fae61b54f0 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java @@ -25,29 +25,22 @@ package org.geysermc.geyser.event.type; +import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; -import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent { + @Getter private final Map packs; - private final Map cdnEntries; - public GeyserDefineResourcePacksEventImpl(Map packMap, List cdnEntries) { + public GeyserDefineResourcePacksEventImpl(Map packMap) { this.packs = packMap; - this.cdnEntries = new HashMap<>(); - cdnEntries.forEach(entry -> this.cdnEntries.put(entry.uuid().toString(), entry)); - } - - public @NonNull Map getPacks() { - return packs; } @Override @@ -55,39 +48,18 @@ public GeyserDefineResourcePacksEventImpl(Map packMap, Lis return List.copyOf(packs.values()); } - @Override - public @NonNull List cdnEntries() { - return List.copyOf(cdnEntries.values()); - } - @Override public boolean register(@NonNull ResourcePack resourcePack) { String packID = resourcePack.manifest().header().uuid().toString(); - if (packs.containsValue(resourcePack) || packs.containsKey(packID) || cdnEntries.containsKey(packID)) { + if (packs.containsValue(resourcePack) || packs.containsKey(packID)) { return false; } packs.put(resourcePack.manifest().header().uuid().toString(), resourcePack); return true; } - @Override - public boolean register(@NonNull ResourcePackCDNEntry entry) { - String packID = entry.uuid().toString(); - if (packs.containsKey(packID) || cdnEntries.containsValue(entry) || cdnEntries.containsKey(packID)) { - return false; - } - cdnEntries.put(packID, entry); - return true; - } - @Override public boolean unregister(@NonNull UUID uuid) { - if (packs.containsKey(uuid.toString())) { - return packs.remove(uuid.toString()) != null; - } else if (cdnEntries.containsKey(uuid.toString())) { - return cdnEntries.remove(uuid.toString()) != null; - } else { - return false; - } + return packs.remove(uuid.toString()) != null; } } diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java index 974e704fd7b..e66396d6a7b 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -25,71 +25,42 @@ package org.geysermc.geyser.event.type; +import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; -import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; import org.geysermc.geyser.session.GeyserSession; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent { + @Getter private final Map packs; - private final Map cdnEntries; - public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap, List cdnEntries) { + public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap) { super(session); this.packs = packMap; - this.cdnEntries = new HashMap<>(); - cdnEntries.forEach(entry -> this.cdnEntries.put(entry.uuid().toString(), entry)); } - - public @NonNull Map getPacks() { - return packs; - } - @Override public @NonNull List resourcePacks() { return List.copyOf(packs.values()); } - @Override - public @NonNull List cdnEntries() { - return List.copyOf(cdnEntries.values()); - } - @Override public boolean register(@NonNull ResourcePack resourcePack) { String packID = resourcePack.manifest().header().uuid().toString(); - if (packs.containsValue(resourcePack) || packs.containsKey(packID) || cdnEntries.containsKey(packID)) { + if (packs.containsValue(resourcePack) || packs.containsKey(packID)) { return false; } packs.put(resourcePack.manifest().header().uuid().toString(), resourcePack); return true; } - @Override - public boolean register(@NonNull ResourcePackCDNEntry entry) { - String packID = entry.uuid().toString(); - if (packs.containsKey(packID) || cdnEntries.containsKey(packID) || cdnEntries.containsValue(entry)) { - return false; - } - cdnEntries.put(packID, entry); - return true; - } - @Override public boolean unregister(@NonNull UUID uuid) { - if (packs.containsKey(uuid.toString())) { - return packs.remove(uuid.toString()) != null; - } else if (cdnEntries.containsKey(uuid.toString())) { - return cdnEntries.remove(uuid.toString()) != null; - } else { - return false; - } + return packs.remove(uuid.toString()) != null; } } 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 1ef6170a16c..863a728a058 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -31,33 +31,19 @@ import org.cloudburstmc.protocol.bedrock.data.ExperimentData; import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm; import org.cloudburstmc.protocol.bedrock.data.ResourcePackType; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; -import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; -import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; -import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; -import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkDataPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkRequestPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackDataInfoPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; -import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; +import org.cloudburstmc.protocol.bedrock.packet.*; import org.cloudburstmc.protocol.common.PacketSignal; import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; 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.ResourcePackCDNEntry; import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.event.type.SessionLoadResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; -import org.geysermc.geyser.registry.loader.ResourcePackLoader; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.text.GeyserLocale; @@ -180,26 +166,26 @@ public PacketSignal handle(LoginPacket loginPacket) { geyser.getSessionManager().addPendingSession(session); - this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get()), ResourcePackLoader.RESOURCE_PACK_CDN_ENTRIES); + this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get())); this.geyser.eventBus().fire(this.resourcePackLoadEvent); ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); for (ResourcePack pack : this.resourcePackLoadEvent.resourcePacks()) { PackCodec codec = pack.codec(); ResourcePackManifest.Header header = pack.manifest().header(); + + if (pack.codec() instanceof UrlPackCodec urlPackCodec) { + resourcePacksInfo.getCDNEntries().add(new ResourcePacksInfoPacket.CDNEntry( + header.uuid() + "_" + header.version(), urlPackCodec.url())); + } + resourcePacksInfo.getResourcePackInfos().add(new ResourcePacksInfoPacket.Entry( header.uuid().toString(), header.version().toString(), codec.size(), pack.contentKey(), "", header.uuid().toString(), false, false)); } - // Add CDN entries if the client supports it - if (!GameProtocol.isPre1_20_30(session)) { - for (ResourcePackCDNEntry entry : this.resourcePackLoadEvent.cdnEntries()) { - resourcePacksInfo.getCDNEntries().add(new ResourcePacksInfoPacket.CDNEntry(entry.uuid().toString(), entry.url())); - } - } - resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); + GeyserImpl.getInstance().getLogger().info(resourcePacksInfo.toString()); session.sendUpstreamPacket(resourcePacksInfo); GeyserLocale.loadGeyserLocale(session.locale()); @@ -209,7 +195,7 @@ public PacketSignal handle(LoginPacket loginPacket) { @Override public PacketSignal handle(ResourcePackClientResponsePacket packet) { switch (packet.getStatus()) { - case COMPLETED: + case COMPLETED -> { if (geyser.getConfig().getRemote().authType() != AuthType.ONLINE) { session.authenticate(session.getAuthData().name()); } else if (!couldLoginUserByName(session.getAuthData().name())) { @@ -217,35 +203,27 @@ public PacketSignal handle(ResourcePackClientResponsePacket packet) { session.connect(); } geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.connect", session.getAuthData().name())); - break; - - case SEND_PACKS: + } + case SEND_PACKS -> { packsToSent.addAll(packet.getPackIds()); sendPackDataInfo(packsToSent.pop()); - break; - - case HAVE_ALL_PACKS: + } + case HAVE_ALL_PACKS -> { ResourcePackStackPacket stackPacket = new ResourcePackStackPacket(); stackPacket.setExperimentsPreviouslyToggled(false); stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not stackPacket.setGameVersion(session.getClientData().getGameVersion()); - for (ResourcePack pack : this.resourcePackLoadEvent.resourcePacks()) { ResourcePackManifest.Header header = pack.manifest().header(); stackPacket.getResourcePacks().add(new ResourcePackStackPacket.Entry(header.uuid().toString(), header.version().toString(), "")); } - if (GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { // Allow custom items to work stackPacket.getExperiments().add(new ExperimentData("data_driven_items", true)); } - session.sendUpstreamPacket(stackPacket); - break; - - default: - session.disconnect("disconnectionScreen.resourcePack"); - break; + } + default -> session.disconnect("disconnectionScreen.resourcePack"); } return PacketSignal.HANDLED; @@ -297,6 +275,10 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packet.getPackId().toString()); PackCodec codec = pack.codec(); + if (codec instanceof UrlPackCodec urlPackCodec) { + GeyserImpl.getInstance().getLogger().warning("Received ResourcePackChunkRequestPacket for URL pack " + urlPackCodec.url()); + } + data.setChunkIndex(packet.getChunkIndex()); data.setProgress((long) packet.getChunkIndex() * GeyserResourcePack.CHUNK_SIZE); data.setPackVersion(packet.getPackVersion()); @@ -331,7 +313,6 @@ private void sendPackDataInfo(String id) { ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packID[0]); PackCodec codec = pack.codec(); ResourcePackManifest.Header header = pack.manifest().header(); - data.setPackId(header.uuid()); int chunkCount = (int) Math.ceil(codec.size() / (double) GeyserResourcePack.CHUNK_SIZE); data.setChunkCount(chunkCount); 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 82408b6e731..b50696a55de 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java +++ b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java @@ -25,11 +25,12 @@ package org.geysermc.geyser.pack; +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; -public record GeyserResourcePack(PackCodec codec, ResourcePackManifest manifest, String contentKey) implements ResourcePack { +public record GeyserResourcePack(@NonNull PackCodec codec, @NonNull ResourcePackManifest manifest, @NonNull String contentKey) implements ResourcePack { /** * The size of each chunk to use when sending the resource packs to clients in bytes 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 new file mode 100644 index 00000000000..fc86656799b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.pack.url; + +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.UrlPackCodec; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.nio.channels.Channels; +import java.nio.channels.SeekableByteChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +@RequiredArgsConstructor +public class GeyserUrlPackCodec extends UrlPackCodec { + + private final String url; + private final String contentKey; + + public GeyserUrlPackCodec(String url) { + this.url = url; + this.contentKey = ""; + } + + @Override + public byte @NonNull [] sha256() { + try { + URL resourcePackURL = new URL(this.url); + InputStream inputStream = resourcePackURL.openStream(); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + digest.update(buffer, 0, bytesRead); + } + return digest.digest(); + } catch (IOException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public long size() { + URLConnection conn = null; + try { + conn = new URL(this.url).openConnection(); + if(conn instanceof HttpURLConnection) { + ((HttpURLConnection)conn).setRequestMethod("HEAD"); + } + conn.getInputStream(); + return conn.getContentLength(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if(conn instanceof HttpURLConnection) { + ((HttpURLConnection)conn).disconnect(); + } + } + } + + @Override + public @NonNull SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { + URL resourcePackURL = new URL(this.url); + URLConnection connection = resourcePackURL.openConnection(); + return (SeekableByteChannel) Channels.newChannel(connection.getInputStream()); + } + + @Override + protected @NonNull ResourcePack create() { + return ResourcePackLoader.downloadPack(this.url, this.contentKey); + } + + @Override + public @NonNull String url() { + return this.url; + } + + @Override + public @NonNull String contentKey() { + return this.contentKey; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index 13d7a4d77d8..59960098fba 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -38,6 +38,7 @@ import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.api.pack.PathPackCodec; +import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.command.GeyserCommandManager; import org.geysermc.geyser.event.GeyserEventRegistrar; import org.geysermc.geyser.item.GeyserCustomItemData; @@ -50,6 +51,7 @@ import org.geysermc.geyser.level.block.GeyserMaterialInstance; import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; +import org.geysermc.geyser.pack.url.GeyserUrlPackCodec; import org.geysermc.geyser.registry.provider.ProviderSupplier; import java.nio.file.Path; @@ -74,6 +76,7 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov providers.put(EventRegistrar.class, args -> new GeyserEventRegistrar(args[0])); providers.put(PathPackCodec.class, args -> new GeyserPathPackCodec((Path) args[0])); + providers.put(UrlPackCodec.class, args -> new GeyserUrlPackCodec((String) args[0], (String) args[1])); // items providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.CustomItemDataBuilder()); 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 8a2f5abdfe5..a465256327a 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 @@ -25,15 +25,17 @@ package org.geysermc.geyser.registry.loader; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; -import org.geysermc.geyser.api.pack.ResourcePackCDNEntry; +import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.event.type.GeyserDefineResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePackManifest; import org.geysermc.geyser.pack.SkullResourcePackManager; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; +import org.geysermc.geyser.pack.url.GeyserUrlPackCodec; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.WebUtils; @@ -45,10 +47,8 @@ import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -64,14 +64,12 @@ public class ResourcePackLoader implements RegistryLoader RESOURCE_PACK_CDN_ENTRIES = new ArrayList<>(); - /** * Loop through the packs directory and locate valid resource pack files */ @Override public Map load(Path directory) { - Map packMap = new HashMap<>(); + Map packMap = new Object2ObjectOpenHashMap<>(); if (!Files.exists(directory)) { try { @@ -100,9 +98,6 @@ public Map load(Path directory) { resourcePacks.add(skullResourcePack); } - // Load CDN entries - loadCdnEntries(); - GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); GeyserImpl.getInstance().eventBus().fire(event); @@ -115,9 +110,11 @@ public Map load(Path directory) { } } - GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap, RESOURCE_PACK_CDN_ENTRIES); + // Load CDN entries + packMap.putAll(loadCdnEntries()); + + GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap); packMap = defineEvent.getPacks(); - RESOURCE_PACK_CDN_ENTRIES = defineEvent.cdnEntries(); return packMap; } @@ -175,7 +172,7 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep } } - public void loadCdnEntries() { + public Map loadCdnEntries() { final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs"); // Download CDN packs to get the pack uuid's @@ -184,26 +181,35 @@ public void loadCdnEntries() { Files.createDirectories(cachedCdnPacksDirectory); } catch (IOException e) { GeyserImpl.getInstance().getLogger().error("Could not create cached packs directory", e); - return; + return new Object2ObjectOpenHashMap<>(); } } List cdnPacks = GeyserImpl.getInstance().getConfig().getResourcePackUrls(); + Map packMap = new Object2ObjectOpenHashMap<>(); + for (String url: cdnPacks) { - int packHash = url.hashCode(); - Path cachedPath = cachedCdnPacksDirectory.resolve(packHash + ".zip"); - WebUtils.downloadFile(url, cachedPath.toString()); + GeyserImpl.getInstance().getLogger().info("Loading CDN pack " + url); + ResourcePack pack = downloadPack(url, ""); + packMap.put(pack.manifest().header().uuid().toString(), pack); + } + return packMap; + } - ResourcePack cdnpack = readPack(cachedPath); - UUID uuid = cdnpack.manifest().header().uuid(); + public static ResourcePack downloadPack(String url, String contentKey) throws IllegalArgumentException { + int packHash = url.hashCode(); + Path cachedPath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs").resolve(packHash + ".zip"); + WebUtils.downloadFile(url, cachedPath.toString()); - RESOURCE_PACK_CDN_ENTRIES.add(new ResourcePackCDNEntry(url, uuid)); + ResourcePack temp = readPack(cachedPath); + ResourcePackManifest manifest = temp.manifest(); - try { - Files.delete(cachedPath); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not delete cached pack", e); - } + try { + Files.delete(cachedPath); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not delete cached pack", e); } + + return new GeyserResourcePack(new GeyserUrlPackCodec(url, contentKey), manifest, contentKey); } } From 27c156243800750682058491683d40617ef2d35e Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 17 Oct 2023 19:21:24 +0200 Subject: [PATCH 09/35] remove outdated javadocs --- .../api/event/bedrock/SessionLoadResourcePacksEvent.java | 4 ++-- .../api/event/lifecycle/GeyserDefineResourcePacksEvent.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index 26909f046d6..71f9e9a0d50 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -60,8 +60,8 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { /** * Unregisters a {@link ResourcePack} from being sent to the client. * - * @param uuid the UUID of the resource pack/CDN entry to remove. - * @return true whether the resource pack/CDN entry was removed successfully. + * @param uuid the UUID of the resource pack to remove. + * @return true whether the resource pack was removed successfully. */ public abstract boolean unregister(@NonNull UUID uuid); } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java index 6da2b1f4f20..b62ddb8cca0 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -56,8 +56,8 @@ public abstract class GeyserDefineResourcePacksEvent implements Event { /** * Unregisters a {@link ResourcePack} from being sent to clients. * - * @param uuid the UUID of the resource pack/CDN entry to remove. - * @return true whether the resource pack/CDN entry was removed successfully. + * @param uuid the UUID of the resource pack to remove. + * @return true whether the resource pack was removed successfully. */ public abstract boolean unregister(@NonNull UUID uuid); } From 5d06edd0d18654a5e7b7066431c986fe8c9a7aa8 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Wed, 18 Oct 2023 12:06:42 +0200 Subject: [PATCH 10/35] Fallback system - download the pack to serve the client in case cdn fails --- .../geyser/pack/path/GeyserPathPackCodec.java | 2 +- .../geyser/pack/url/GeyserUrlPackCodec.java | 63 ++++++------------- .../registry/loader/ResourcePackLoader.java | 16 +---- 3 files changed, 23 insertions(+), 58 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java b/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java index 84067600fce..13d07c800a2 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java +++ b/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java @@ -84,7 +84,7 @@ public long size() { } @Override - protected @NonNull ResourcePack create() { + public @NonNull ResourcePack create() { return ResourcePackLoader.readPack(this.path); } 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 fc86656799b..391f78c4edb 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 @@ -25,79 +25,54 @@ package org.geysermc.geyser.pack.url; -import lombok.RequiredArgsConstructor; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.UrlPackCodec; +import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.registry.loader.ResourcePackLoader; import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.nio.channels.Channels; import java.nio.channels.SeekableByteChannel; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.nio.file.Path; -@RequiredArgsConstructor public class GeyserUrlPackCodec extends UrlPackCodec { - private final String url; private final String contentKey; + private final GeyserPathPackCodec fallback; public GeyserUrlPackCodec(String url) { + this(url, ""); + } + + public GeyserUrlPackCodec(String url, String contentKey) { this.url = url; - this.contentKey = ""; + this.contentKey = contentKey; + this.fallback = new GeyserPathPackCodec(getCachePath(url)); + } + + private static Path getCachePath(String url) { + return ResourcePackLoader.downloadPack(url); } @Override public byte @NonNull [] sha256() { - try { - URL resourcePackURL = new URL(this.url); - InputStream inputStream = resourcePackURL.openStream(); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - digest.update(buffer, 0, bytesRead); - } - return digest.digest(); - } catch (IOException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + return fallback.sha256(); } @Override public long size() { - URLConnection conn = null; - try { - conn = new URL(this.url).openConnection(); - if(conn instanceof HttpURLConnection) { - ((HttpURLConnection)conn).setRequestMethod("HEAD"); - } - conn.getInputStream(); - return conn.getContentLength(); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - if(conn instanceof HttpURLConnection) { - ((HttpURLConnection)conn).disconnect(); - } - } + return fallback.size(); } @Override public @NonNull SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { - URL resourcePackURL = new URL(this.url); - URLConnection connection = resourcePackURL.openConnection(); - return (SeekableByteChannel) Channels.newChannel(connection.getInputStream()); + return fallback.serialize(resourcePack); } @Override - protected @NonNull ResourcePack create() { - return ResourcePackLoader.downloadPack(this.url, this.contentKey); + @NonNull + public ResourcePack create() { + return fallback.create(); } @Override 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 a465256327a..a302921759d 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 @@ -29,7 +29,6 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; -import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.event.type.GeyserDefineResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePackManifest; @@ -190,26 +189,17 @@ public Map loadCdnEntries() { for (String url: cdnPacks) { GeyserImpl.getInstance().getLogger().info("Loading CDN pack " + url); - ResourcePack pack = downloadPack(url, ""); + ResourcePack pack = new GeyserUrlPackCodec(url).create(); packMap.put(pack.manifest().header().uuid().toString(), pack); } return packMap; } - public static ResourcePack downloadPack(String url, String contentKey) throws IllegalArgumentException { + public static Path downloadPack(String url) throws IllegalArgumentException { int packHash = url.hashCode(); Path cachedPath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs").resolve(packHash + ".zip"); WebUtils.downloadFile(url, cachedPath.toString()); - ResourcePack temp = readPack(cachedPath); - ResourcePackManifest manifest = temp.manifest(); - - try { - Files.delete(cachedPath); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not delete cached pack", e); - } - - return new GeyserResourcePack(new GeyserUrlPackCodec(url, contentKey), manifest, contentKey); + return cachedPath; } } From 880de2d1729fd2785de9db0c8bf8cda21ef2c69e Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Wed, 18 Oct 2023 12:36:44 +0200 Subject: [PATCH 11/35] Ensure GeyserUrlPackCodec.create returns a ResourcePack with the URL codec --- .../geyser/pack/url/GeyserUrlPackCodec.java | 4 +- .../registry/loader/ResourcePackLoader.java | 45 +++++++++++++++---- 2 files changed, 38 insertions(+), 11 deletions(-) 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 391f78c4edb..26ea72c7fa5 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 @@ -38,7 +38,7 @@ public class GeyserUrlPackCodec extends UrlPackCodec { private final String url; private final String contentKey; - private final GeyserPathPackCodec fallback; + public final GeyserPathPackCodec fallback; public GeyserUrlPackCodec(String url) { this(url, ""); @@ -72,7 +72,7 @@ public long size() { @Override @NonNull public ResourcePack create() { - return fallback.create(); + return ResourcePackLoader.loadDownloadedPack(this); } @Override 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 a302921759d..d01b6d2c955 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 @@ -29,6 +29,7 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.event.type.GeyserDefineResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePackManifest; @@ -131,6 +132,35 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep throw new IllegalArgumentException("Resource pack " + path.getFileName() + " must be a .zip or .mcpack file!"); } + ResourcePackManifest manifest = readManifest(path, path.getFileName().toString()); + String contentKey; + + try { + // Check if a file exists with the same name as the resource pack suffixed by .key, + // and set this as content key. (e.g. test.zip, key file would be test.zip.key) + Path keyFile = path.resolveSibling(path.getFileName().toString() + ".key"); + contentKey = Files.exists(keyFile) ? Files.readString(keyFile, StandardCharsets.UTF_8) : ""; + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to read content key for resource pack " + path.getFileName(), e); + contentKey = ""; + } + + return new GeyserResourcePack(new GeyserPathPackCodec(path), manifest, contentKey); + } + + public static ResourcePack loadDownloadedPack(GeyserUrlPackCodec codec) { + Path path = codec.fallback.path(); + if (!path.getFileName().toString().endsWith(".mcpack") && !path.getFileName().toString().endsWith(".zip")) { + throw new IllegalArgumentException("The url " + codec.url() + " did not provide a valid resource pack! Please check the url and try again."); + } + + ResourcePackManifest manifest = readManifest(path, path.getFileName().toString()); + String contentKey = codec.contentKey(); + + return new GeyserResourcePack(codec, manifest, contentKey); + } + + private static ResourcePackManifest readManifest(Path path, String packLocation) throws IllegalArgumentException { AtomicReference manifestReference = new AtomicReference<>(); try (ZipFile zip = new ZipFile(path.toFile()); @@ -138,7 +168,7 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep stream.forEach(x -> { String name = x.getName(); if (SHOW_RESOURCE_PACK_LENGTH_WARNING && name.length() >= 80) { - GeyserImpl.getInstance().getLogger().warning("The resource pack " + path.getFileName() + GeyserImpl.getInstance().getLogger().warning("The resource pack " + packLocation + " has a file in it that meets or exceeds 80 characters in its path (" + name + ", " + name.length() + " characters long). This will cause problems on some Bedrock platforms." + " Please rename it to be shorter, or reduce the amount of folders needed to get to the file."); @@ -157,20 +187,15 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep GeyserResourcePackManifest manifest = manifestReference.get(); if (manifest == null) { - throw new IllegalArgumentException(path.getFileName() + " does not contain a valid pack_manifest.json or manifest.json"); + throw new IllegalArgumentException(packLocation + " does not contain a valid pack_manifest.json or manifest.json"); } - // Check if a file exists with the same name as the resource pack suffixed by .key, - // and set this as content key. (e.g. test.zip, key file would be test.zip.key) - Path keyFile = path.resolveSibling(path.getFileName().toString() + ".key"); - String contentKey = Files.exists(keyFile) ? Files.readString(keyFile, StandardCharsets.UTF_8) : ""; - - return new GeyserResourcePack(new GeyserPathPackCodec(path), manifest, contentKey); + return manifest; } catch (Exception e) { throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", path.getFileName()), e); } } - + public Map loadCdnEntries() { final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs"); @@ -200,6 +225,8 @@ public static Path downloadPack(String url) throws IllegalArgumentException { Path cachedPath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs").resolve(packHash + ".zip"); WebUtils.downloadFile(url, cachedPath.toString()); + // TODO: Check downloaded pack for validity regarding manifest file + return cachedPath; } } From 76a62abd2757757e67c93b5ecb3aa8bb742e92a3 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Wed, 18 Oct 2023 16:49:24 +0200 Subject: [PATCH 12/35] Check downloaded resource packs, yeet cdn naming scheme --- .../geyser/network/UpstreamPacketHandler.java | 6 ++- .../registry/loader/ResourcePackLoader.java | 52 ++++++++++++++----- 2 files changed, 44 insertions(+), 14 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 863a728a058..697fa402201 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -275,8 +275,12 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packet.getPackId().toString()); PackCodec codec = pack.codec(); + // Check for packs that the client should normally download on its own. If the client cannot find the pack, we provide it instead. if (codec instanceof UrlPackCodec urlPackCodec) { - GeyserImpl.getInstance().getLogger().warning("Received ResourcePackChunkRequestPacket for URL pack " + urlPackCodec.url()); + if (!GameProtocol.isPre1_20_30(this.session)) { + // TODO: Proper pack checking - could be that the remote url is offline, the pack changed, or.. something? + GeyserImpl.getInstance().getLogger().warning("Received ResourcePackChunkRequestPacket for URL pack " + urlPackCodec.url()); + } } data.setChunkIndex(packet.getChunkIndex()); 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 d01b6d2c955..59029cfb7f4 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 @@ -148,13 +148,13 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep return new GeyserResourcePack(new GeyserPathPackCodec(path), manifest, contentKey); } - public static ResourcePack loadDownloadedPack(GeyserUrlPackCodec codec) { + public static ResourcePack loadDownloadedPack(GeyserUrlPackCodec codec) throws IllegalArgumentException { Path path = codec.fallback.path(); if (!path.getFileName().toString().endsWith(".mcpack") && !path.getFileName().toString().endsWith(".zip")) { throw new IllegalArgumentException("The url " + codec.url() + " did not provide a valid resource pack! Please check the url and try again."); } - ResourcePackManifest manifest = readManifest(path, path.getFileName().toString()); + ResourcePackManifest manifest = readManifest(path, codec.url()); String contentKey = codec.contentKey(); return new GeyserResourcePack(codec, manifest, contentKey); @@ -192,40 +192,66 @@ private static ResourcePackManifest readManifest(Path path, String packLocation) return manifest; } catch (Exception e) { - throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", path.getFileName()), e); + throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", packLocation), e); } } - + public Map loadCdnEntries() { - final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs"); + final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); // Download CDN packs to get the pack uuid's if (!Files.exists(cachedCdnPacksDirectory)) { try { Files.createDirectories(cachedCdnPacksDirectory); } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not create cached packs directory", e); + GeyserImpl.getInstance().getLogger().error("Could not create remote pack cache directory", e); return new Object2ObjectOpenHashMap<>(); } } - List cdnPacks = GeyserImpl.getInstance().getConfig().getResourcePackUrls(); + List remotePackUrls = GeyserImpl.getInstance().getConfig().getResourcePackUrls(); Map packMap = new Object2ObjectOpenHashMap<>(); - for (String url: cdnPacks) { - GeyserImpl.getInstance().getLogger().info("Loading CDN pack " + url); - ResourcePack pack = new GeyserUrlPackCodec(url).create(); - packMap.put(pack.manifest().header().uuid().toString(), pack); + for (String url: remotePackUrls) { + try { + ResourcePack pack = new GeyserUrlPackCodec(url).create(); + packMap.put(pack.manifest().header().uuid().toString(), pack); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url)); + GeyserImpl.getInstance().getLogger().error(e.getMessage()); + if (GeyserImpl.getInstance().getLogger().isDebug()) { + e.printStackTrace(); + } + } } return packMap; } public static Path downloadPack(String url) throws IllegalArgumentException { int packHash = url.hashCode(); - Path cachedPath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("cdn-packs").resolve(packHash + ".zip"); + Path cachedPath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").resolve(packHash + ".zip"); WebUtils.downloadFile(url, cachedPath.toString()); - // TODO: Check downloaded pack for validity regarding manifest file + if (!PACK_MATCHER.matches(cachedPath)) { + throw new IllegalArgumentException("Invalid pack! Not a .zip or .mcpack file."); + } + + try { + ZipFile zip = new ZipFile(cachedPath.toFile()); + if (zip.stream().noneMatch(x -> x.getName().contains("manifest.json"))) { + throw new IllegalArgumentException(url + " does not contain a manifest file."); + } + + // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. + // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) + if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { + GeyserImpl.getInstance().getLogger().warning("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + + "This is not supported for remote packs, and will cause Bedrock clients to fall back to request the pack from the server. " + + "Please put the pack file in a subfolder, and provide that zip in the URL."); + } + } catch (IOException e) { + throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); + } return cachedPath; } From 36709143c7a312f3287ba5783667f34cfe13b9b0 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 24 Oct 2023 07:50:37 +0200 Subject: [PATCH 13/35] start on proper url checking (application type/size) --- .../geyser/network/UpstreamPacketHandler.java | 3 + .../geyser/pack/url/GeyserUrlPackCodec.java | 19 +++--- .../registry/loader/ResourcePackLoader.java | 59 +++++++++++-------- .../org/geysermc/geyser/util/WebUtils.java | 51 ++++++++++++++-- 4 files changed, 94 insertions(+), 38 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 697fa402201..a775399c13d 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -50,6 +50,7 @@ import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.MathUtils; import org.geysermc.geyser.util.VersionCheckUtils; +import org.geysermc.geyser.util.WebUtils; import java.io.IOException; import java.nio.ByteBuffer; @@ -280,6 +281,8 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { if (!GameProtocol.isPre1_20_30(this.session)) { // TODO: Proper pack checking - could be that the remote url is offline, the pack changed, or.. something? GeyserImpl.getInstance().getLogger().warning("Received ResourcePackChunkRequestPacket for URL pack " + urlPackCodec.url()); + + WebUtils.checkRemotePackUrl(urlPackCodec.url()); } } 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 26ea72c7fa5..4335187be44 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 @@ -25,6 +25,7 @@ package org.geysermc.geyser.pack.url; +import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.UrlPackCodec; @@ -33,25 +34,25 @@ import java.io.IOException; import java.nio.channels.SeekableByteChannel; -import java.nio.file.Path; public class GeyserUrlPackCodec extends UrlPackCodec { private final String url; private final String contentKey; - public final GeyserPathPackCodec fallback; + @Getter + private final GeyserPathPackCodec fallback; - public GeyserUrlPackCodec(String url) { + public GeyserUrlPackCodec(String url) throws IllegalArgumentException { this(url, ""); } - public GeyserUrlPackCodec(String url, String contentKey) { + public GeyserUrlPackCodec(String url, String contentKey) throws IllegalArgumentException { this.url = url; this.contentKey = contentKey; - this.fallback = new GeyserPathPackCodec(getCachePath(url)); - } - - private static Path getCachePath(String url) { - return ResourcePackLoader.downloadPack(url); + try { + this.fallback = new GeyserPathPackCodec(ResourcePackLoader.downloadPack(url).get()); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to download pack from " + url, e); + } } @Override 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 59029cfb7f4..0dcb2ab5f8b 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 @@ -26,6 +26,7 @@ package org.geysermc.geyser.registry.loader; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; @@ -49,6 +50,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -149,7 +151,7 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep } public static ResourcePack loadDownloadedPack(GeyserUrlPackCodec codec) throws IllegalArgumentException { - Path path = codec.fallback.path(); + Path path = codec.getFallback().path(); if (!path.getFileName().toString().endsWith(".mcpack") && !path.getFileName().toString().endsWith(".zip")) { throw new IllegalArgumentException("The url " + codec.url() + " did not provide a valid resource pack! Please check the url and try again."); } @@ -214,11 +216,11 @@ public Map loadCdnEntries() { for (String url: remotePackUrls) { try { - ResourcePack pack = new GeyserUrlPackCodec(url).create(); + GeyserUrlPackCodec codec = new GeyserUrlPackCodec(url); + ResourcePack pack = codec.create(); packMap.put(pack.manifest().header().uuid().toString(), pack); } catch (Exception e) { GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url)); - GeyserImpl.getInstance().getLogger().error(e.getMessage()); if (GeyserImpl.getInstance().getLogger().isDebug()) { e.printStackTrace(); } @@ -227,32 +229,39 @@ public Map loadCdnEntries() { return packMap; } - public static Path downloadPack(String url) throws IllegalArgumentException { - int packHash = url.hashCode(); - Path cachedPath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").resolve(packHash + ".zip"); - WebUtils.downloadFile(url, cachedPath.toString()); - - if (!PACK_MATCHER.matches(cachedPath)) { - throw new IllegalArgumentException("Invalid pack! Not a .zip or .mcpack file."); - } + public static CompletableFuture<@Nullable Path> downloadPack(String url) throws IllegalArgumentException { + CompletableFuture future = WebUtils.checkRemotePackUrl(url); + AtomicReference pathAtomicReference = new AtomicReference<>(); + future.whenCompleteAsync((cachedPath, throwable) -> { + if (cachedPath == null || throwable != null) { + return; + } - try { - ZipFile zip = new ZipFile(cachedPath.toFile()); - if (zip.stream().noneMatch(x -> x.getName().contains("manifest.json"))) { - throw new IllegalArgumentException(url + " does not contain a manifest file."); + // Check if the pack is a .zip or .mcpack file + if (!PACK_MATCHER.matches(cachedPath)) { + throw new IllegalArgumentException("Invalid pack! Not a .zip or .mcpack file."); } - // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. - // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) - if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { - GeyserImpl.getInstance().getLogger().warning("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + - "This is not supported for remote packs, and will cause Bedrock clients to fall back to request the pack from the server. " + - "Please put the pack file in a subfolder, and provide that zip in the URL."); + try { + ZipFile zip = new ZipFile(cachedPath.toFile()); + if (zip.stream().noneMatch(x -> x.getName().contains("manifest.json"))) { + throw new IllegalArgumentException(url + " does not contain a manifest file."); + } + + // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. + // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) + if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { + GeyserImpl.getInstance().getLogger().warning("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + + "This is not supported for remote packs, and will cause Bedrock clients to fall back to request the pack from the server. " + + "Please put the pack file in a subfolder, and provide that zip in the URL."); + } + } catch (IOException e) { + throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); } - } catch (IOException e) { - throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); - } - return cachedPath; + pathAtomicReference.set(cachedPath); + }); + + return future.thenApplyAsync(x -> pathAtomicReference.get()); } } 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 e4a98b3fc7b..a0351e92ad4 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -26,20 +26,23 @@ package org.geysermc.geyser.util; import com.fasterxml.jackson.databind.JsonNode; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; -import javax.annotation.Nullable; import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; import java.io.*; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Map; +import java.util.concurrent.CompletableFuture; public class WebUtils { @@ -95,6 +98,47 @@ public static void downloadFile(String reqURL, String fileLocation) { } } + /** + * Checks a remote pack URL to see if it is valid + * If it is, it will download the pack file and return a path to it + * + * @param url The URL to check + * @return Path to the downloaded pack file + */ + public static CompletableFuture<@Nullable Path> checkRemotePackUrl(String url) { + return CompletableFuture.supplyAsync(() -> { + try { + HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().toString() + "/" + GeyserImpl.VERSION); + int size = con.getContentLength(); + String type = con.getContentType(); + InputStream in = con.getInputStream(); + + if (size < 1 || !type.equals("application/zip")) { + GeyserImpl.getInstance().getLogger().error("Invalid resource pack: " + url + " (" + type + ", " + size + " bytes)"); + //return null; + } + Path fileLocation = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").resolve(url.hashCode() + ".zip"); + Files.copy(in, fileLocation, StandardCopyOption.REPLACE_EXISTING); + + if (Files.size(fileLocation) != size) { + GeyserImpl.getInstance().getLogger().error("Server sent " + Files.size(fileLocation) + " bytes, expected " + size + " bytes"); + Files.delete(fileLocation); + return null; + } + + return fileLocation; + } catch (MalformedURLException e) { + GeyserImpl.getInstance().getLogger().error("Malformed URL: " + url); + return null; + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Unable to download and save file: " + url + ")"); + return null; + } + }); + } + + /** * Post a string to the given URL * @@ -169,15 +213,14 @@ public static String postForm(String reqURL, Map fields) throws try (OutputStream out = con.getOutputStream()) { // Write the form data to the output for (Map.Entry field : fields.entrySet()) { - out.write((field.getKey() + "=" + URLEncoder.encode(field.getValue(), StandardCharsets.UTF_8.toString()) + "&").getBytes(StandardCharsets.UTF_8)); + out.write((field.getKey() + "=" + URLEncoder.encode(field.getValue(), StandardCharsets.UTF_8) + "&").getBytes(StandardCharsets.UTF_8)); } } return connectionToString(con); } - @Nullable - public static String[] findSrvRecord(GeyserImpl geyser, String remoteAddress) { + public static String @Nullable [] findSrvRecord(GeyserImpl geyser, String remoteAddress) { try { // Searches for a server address and a port from a SRV record of the specified host name InitialDirContext ctx = new InitialDirContext(); From 498a415ac52711ee35bb9ff8ab12d49398bcf32e Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 9 Nov 2023 08:34:45 +0100 Subject: [PATCH 14/35] Change fallback system --- .../geyser/network/UpstreamPacketHandler.java | 2 +- .../geyser/pack/url/GeyserUrlPackCodec.java | 31 +++++++++++++++---- .../registry/loader/ResourcePackLoader.java | 17 +++++----- .../org/geysermc/geyser/util/WebUtils.java | 16 +++++++--- 4 files changed, 46 insertions(+), 20 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 a775399c13d..2e94993e718 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -282,7 +282,7 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // TODO: Proper pack checking - could be that the remote url is offline, the pack changed, or.. something? GeyserImpl.getInstance().getLogger().warning("Received ResourcePackChunkRequestPacket for URL pack " + urlPackCodec.url()); - WebUtils.checkRemotePackUrl(urlPackCodec.url()); + WebUtils.checkUrlAndDownloadRemotePack(urlPackCodec.url()); } } 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 4335187be44..1f40408eb7c 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,6 +27,7 @@ import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; @@ -39,7 +40,7 @@ public class GeyserUrlPackCodec extends UrlPackCodec { private final String url; private final String contentKey; @Getter - private final GeyserPathPackCodec fallback; + private GeyserPathPackCodec fallback; public GeyserUrlPackCodec(String url) throws IllegalArgumentException { this(url, ""); @@ -48,31 +49,49 @@ public GeyserUrlPackCodec(String url) throws IllegalArgumentException { public GeyserUrlPackCodec(String url, String contentKey) throws IllegalArgumentException { this.url = url; this.contentKey = contentKey; - try { - this.fallback = new GeyserPathPackCodec(ResourcePackLoader.downloadPack(url).get()); - } catch (Exception e) { - throw new IllegalArgumentException("Unable to download pack from " + url, e); - } } @Override public byte @NonNull [] sha256() { + if (this.fallback == null) { + throw new IllegalStateException("Fallback pack not initialized! Needs to be created first."); + } return fallback.sha256(); } @Override public long size() { + if (this.fallback == null) { + throw new IllegalStateException("Fallback pack not initialized! Needs to be created first."); + } return fallback.size(); } @Override public @NonNull SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { + if (this.fallback == null) { + throw new IllegalStateException("Fallback pack not initialized! Needs to be created first."); + } return fallback.serialize(resourcePack); } @Override @NonNull public ResourcePack create() { + if (this.fallback == null) { + try { + this.fallback = new GeyserPathPackCodec(ResourcePackLoader.downloadPack(url).whenComplete((pack, throwable) -> { + if (throwable != null) { + GeyserImpl.getInstance().getLogger().error("Failed to download pack from " + url, throwable); + if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + throwable.printStackTrace(); + } + } + }).join()); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to download pack from " + url, e); + } + } return ResourcePackLoader.loadDownloadedPack(this); } 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 0dcb2ab5f8b..3e92bd34ffb 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 @@ -230,16 +230,20 @@ public Map loadCdnEntries() { } public static CompletableFuture<@Nullable Path> downloadPack(String url) throws IllegalArgumentException { - CompletableFuture future = WebUtils.checkRemotePackUrl(url); - AtomicReference pathAtomicReference = new AtomicReference<>(); + CompletableFuture future = WebUtils.checkUrlAndDownloadRemotePack(url); future.whenCompleteAsync((cachedPath, throwable) -> { - if (cachedPath == null || throwable != null) { + if (cachedPath == null) { + return; + } + + if (throwable != null) { + GeyserImpl.getInstance().getLogger().error("Failed to download resource pack " + url, throwable); return; } // Check if the pack is a .zip or .mcpack file if (!PACK_MATCHER.matches(cachedPath)) { - throw new IllegalArgumentException("Invalid pack! Not a .zip or .mcpack file."); + throw new IllegalArgumentException("Invalid pack format! Not a .zip or .mcpack file."); } try { @@ -258,10 +262,7 @@ public Map loadCdnEntries() { } catch (IOException e) { throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); } - - pathAtomicReference.set(cachedPath); }); - - return future.thenApplyAsync(x -> pathAtomicReference.get()); + return future; } } 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 a0351e92ad4..96372d90c80 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -105,19 +105,25 @@ public static void downloadFile(String reqURL, String fileLocation) { * @param url The URL to check * @return Path to the downloaded pack file */ - public static CompletableFuture<@Nullable Path> checkRemotePackUrl(String url) { + public static CompletableFuture<@Nullable Path> checkUrlAndDownloadRemotePack(String url) { return CompletableFuture.supplyAsync(() -> { try { HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().toString() + "/" + GeyserImpl.VERSION); int size = con.getContentLength(); String type = con.getContentType(); - InputStream in = con.getInputStream(); - if (size < 1 || !type.equals("application/zip")) { - GeyserImpl.getInstance().getLogger().error("Invalid resource pack: " + url + " (" + type + ", " + size + " bytes)"); - //return null; + if (size <= 0) { + GeyserImpl.getInstance().getLogger().error(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); + return null; } + + if (type == null || !type.equals("application/zip")) { + GeyserImpl.getInstance().getLogger().error(String.format("Invalid application type from remote pack URL: %s (type: %s)", url, type)); + return null; + } + + InputStream in = con.getInputStream(); Path fileLocation = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").resolve(url.hashCode() + ".zip"); Files.copy(in, fileLocation, StandardCopyOption.REPLACE_EXISTING); From 0004f5b051302216dc779ba8a03dce2217f936ad Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 9 Nov 2023 12:13:59 +0100 Subject: [PATCH 15/35] Testing: Don't require `application/zip` or `size` or weird zip format, since it seems to work without it !? --- .../geysermc/geyser/api/pack/UrlPackCodec.java | 16 ++++++++++++++-- .../geyser/network/UpstreamPacketHandler.java | 5 ++--- .../geyser/pack/url/GeyserUrlPackCodec.java | 2 +- .../registry/loader/ResourcePackLoader.java | 14 ++++++-------- .../java/org/geysermc/geyser/util/WebUtils.java | 10 +++++----- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java index e6d1a5a243e..b98e59355d2 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java @@ -27,10 +27,22 @@ import org.checkerframework.checker.nullness.qual.NonNull; +/** + * Represents a pack codec that creates a resource + * pack from a URL. + *

+ * Due to Bedrock limitations, the URL must: + *

    + *
  • be a direct download link to a .zip or .mcpack resource pack
  • + *
  • Use application type `application/zip` and set a correct content length
  • + *
+ * + * Additionally, the ResourcePack must be zipped in a folder enclosing the resource pack, instead of the resource pack being at the root of the zip. + */ public abstract class UrlPackCodec extends PackCodec { /** - * Gets the URL of the resource pack. + * Gets the URL to the resource pack location. * * @return the URL of the resource pack */ @@ -40,7 +52,7 @@ public abstract class UrlPackCodec extends PackCodec { /** * If the remote pack has an encryption key, it must be specified here. * Otherwise, leave empty. - * + * @return the encryption key of the resource pack */ @NonNull public abstract String contentKey(); 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 2e94993e718..2c37006f982 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -279,9 +279,8 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // Check for packs that the client should normally download on its own. If the client cannot find the pack, we provide it instead. if (codec instanceof UrlPackCodec urlPackCodec) { if (!GameProtocol.isPre1_20_30(this.session)) { - // TODO: Proper pack checking - could be that the remote url is offline, the pack changed, or.. something? - GeyserImpl.getInstance().getLogger().warning("Received ResourcePackChunkRequestPacket for URL pack " + urlPackCodec.url()); - + GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded!" + + "Is the pack at the URL " + urlPackCodec.url() + " still available?"); WebUtils.checkUrlAndDownloadRemotePack(urlPackCodec.url()); } } 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 1f40408eb7c..0a60f2ce9ea 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 @@ -92,7 +92,7 @@ public ResourcePack create() { throw new IllegalArgumentException("Unable to download pack from " + url, e); } } - return ResourcePackLoader.loadDownloadedPack(this); + return ResourcePackLoader.readPack(this); } @Override 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 3e92bd34ffb..79933264ea6 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 @@ -113,7 +113,7 @@ public Map load(Path directory) { } // Load CDN entries - packMap.putAll(loadCdnEntries()); + packMap.putAll(loadRemotePacks()); GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap); packMap = defineEvent.getPacks(); @@ -130,7 +130,7 @@ public Map load(Path directory) { * @throws IllegalArgumentException if the pack manifest was invalid or there was any processing exception */ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentException { - if (!path.getFileName().toString().endsWith(".mcpack") && !path.getFileName().toString().endsWith(".zip")) { + if (!PACK_MATCHER.matches(path)) { throw new IllegalArgumentException("Resource pack " + path.getFileName() + " must be a .zip or .mcpack file!"); } @@ -150,9 +150,9 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep return new GeyserResourcePack(new GeyserPathPackCodec(path), manifest, contentKey); } - public static ResourcePack loadDownloadedPack(GeyserUrlPackCodec codec) throws IllegalArgumentException { + public static GeyserResourcePack readPack(GeyserUrlPackCodec codec) throws IllegalArgumentException { Path path = codec.getFallback().path(); - if (!path.getFileName().toString().endsWith(".mcpack") && !path.getFileName().toString().endsWith(".zip")) { + if (!PACK_MATCHER.matches(path)) { throw new IllegalArgumentException("The url " + codec.url() + " did not provide a valid resource pack! Please check the url and try again."); } @@ -198,7 +198,7 @@ private static ResourcePackManifest readManifest(Path path, String packLocation) } } - public Map loadCdnEntries() { + public Map loadRemotePacks() { final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); // Download CDN packs to get the pack uuid's @@ -230,8 +230,7 @@ public Map loadCdnEntries() { } public static CompletableFuture<@Nullable Path> downloadPack(String url) throws IllegalArgumentException { - CompletableFuture future = WebUtils.checkUrlAndDownloadRemotePack(url); - future.whenCompleteAsync((cachedPath, throwable) -> { + return WebUtils.checkUrlAndDownloadRemotePack(url).whenCompleteAsync((cachedPath, throwable) -> { if (cachedPath == null) { return; } @@ -263,6 +262,5 @@ public Map loadCdnEntries() { throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); } }); - return future; } } 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 96372d90c80..9e2ea247b97 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -115,12 +115,12 @@ public static void downloadFile(String reqURL, String fileLocation) { if (size <= 0) { GeyserImpl.getInstance().getLogger().error(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); - return null; + //return null; } if (type == null || !type.equals("application/zip")) { GeyserImpl.getInstance().getLogger().error(String.format("Invalid application type from remote pack URL: %s (type: %s)", url, type)); - return null; + //return null; } InputStream in = con.getInputStream(); @@ -128,9 +128,9 @@ public static void downloadFile(String reqURL, String fileLocation) { Files.copy(in, fileLocation, StandardCopyOption.REPLACE_EXISTING); if (Files.size(fileLocation) != size) { - GeyserImpl.getInstance().getLogger().error("Server sent " + Files.size(fileLocation) + " bytes, expected " + size + " bytes"); - Files.delete(fileLocation); - return null; + GeyserImpl.getInstance().getLogger().error("Downloaded pack has " + Files.size(fileLocation) + " bytes, expected " + size + " bytes"); + //Files.delete(fileLocation); + //return null; } return fileLocation; From 15b8b93d4442dace4a6ef5815ecd086758c58adc Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 9 Nov 2023 12:44:27 +0100 Subject: [PATCH 16/35] We need to ensure no invalid packs end up being loaded - otherwise, chaos ensues since the client will either show bogus resource pack size values (invalid sizes), or skip resource packs altogether (if not properly zipped). --- .../geyser/network/UpstreamPacketHandler.java | 16 +++++++++------- .../geyser/pack/url/GeyserUrlPackCodec.java | 8 +++++--- .../registry/loader/ResourcePackLoader.java | 2 +- .../java/org/geysermc/geyser/util/WebUtils.java | 14 ++++++-------- 4 files changed, 21 insertions(+), 19 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 bf38b4e4944..a6b9f216e97 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -55,15 +55,13 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.HashMap; -import java.util.OptionalInt; +import java.util.*; public class UpstreamPacketHandler extends LoggingPacketHandler { private boolean networkSettingsRequested = false; private final Deque packsToSent = new ArrayDeque<>(); + private final List brokenResourcePacks = new ArrayList<>(); private SessionLoadResourcePacksEventImpl resourcePackLoadEvent; @@ -281,9 +279,13 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // Check for packs that the client should normally download on its own. If the client cannot find the pack, we provide it instead. if (codec instanceof UrlPackCodec urlPackCodec) { if (!GameProtocol.isPre1_20_30(this.session)) { - GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded!" + - "Is the pack at the URL " + urlPackCodec.url() + " still available?"); - WebUtils.checkUrlAndDownloadRemotePack(urlPackCodec.url()); + // Ensure we don't a. spam console, and b. spam download/check requests + if (!brokenResourcePacks.contains(packet.getPackId())) { + brokenResourcePacks.add(packet.getPackId()); + GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded!" + + "Is the pack at the URL " + urlPackCodec.url() + " still available?"); + WebUtils.checkUrlAndDownloadRemotePack(urlPackCodec.url()); + } } } 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 0a60f2ce9ea..f1bb8eb3af0 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 @@ -35,6 +35,7 @@ import java.io.IOException; import java.nio.channels.SeekableByteChannel; +import java.nio.file.Path; public class GeyserUrlPackCodec extends UrlPackCodec { private final String url; @@ -80,16 +81,17 @@ public long size() { public ResourcePack create() { if (this.fallback == null) { try { - this.fallback = new GeyserPathPackCodec(ResourcePackLoader.downloadPack(url).whenComplete((pack, throwable) -> { + final Path downloadedPack = ResourcePackLoader.downloadPack(url).whenComplete((pack, throwable) -> { if (throwable != null) { GeyserImpl.getInstance().getLogger().error("Failed to download pack from " + url, throwable); if (GeyserImpl.getInstance().getConfig().isDebugMode()) { throwable.printStackTrace(); } } - }).join()); + }).join(); + this.fallback = new GeyserPathPackCodec(downloadedPack); } catch (Exception e) { - throw new IllegalArgumentException("Unable to download pack from " + url, e); + throw new IllegalArgumentException("Failed to download pack from " + url, e); } } return ResourcePackLoader.readPack(this); 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 79933264ea6..8f387ba8dd5 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 @@ -254,7 +254,7 @@ public Map loadRemotePacks() { // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { - GeyserImpl.getInstance().getLogger().warning("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + + throw new IllegalArgumentException("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + "This is not supported for remote packs, and will cause Bedrock clients to fall back to request the pack from the server. " + "Please put the pack file in a subfolder, and provide that zip in the URL."); } 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 9e2ea247b97..59ad746ef57 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -115,12 +115,12 @@ public static void downloadFile(String reqURL, String fileLocation) { if (size <= 0) { GeyserImpl.getInstance().getLogger().error(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); - //return null; + return null; } if (type == null || !type.equals("application/zip")) { GeyserImpl.getInstance().getLogger().error(String.format("Invalid application type from remote pack URL: %s (type: %s)", url, type)); - //return null; + return null; } InputStream in = con.getInputStream(); @@ -129,17 +129,15 @@ public static void downloadFile(String reqURL, String fileLocation) { if (Files.size(fileLocation) != size) { GeyserImpl.getInstance().getLogger().error("Downloaded pack has " + Files.size(fileLocation) + " bytes, expected " + size + " bytes"); - //Files.delete(fileLocation); - //return null; + Files.delete(fileLocation); + return null; } return fileLocation; } catch (MalformedURLException e) { - GeyserImpl.getInstance().getLogger().error("Malformed URL: " + url); - return null; + throw new IllegalArgumentException("Malformed URL: " + url); } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Unable to download and save file: " + url + ")"); - return null; + throw new RuntimeException("Unable to download and save file: " + url + ")"); } }); } From d4f0d8a57a38a6aa74825d8d6eba4a3f650c7232 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Fri, 10 Nov 2023 20:24:04 +0100 Subject: [PATCH 17/35] Re-add debug: Apparently, not just `application/zip` works....??? --- core/src/main/java/org/geysermc/geyser/util/WebUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 59ad746ef57..6d7cb93be1e 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -115,12 +115,12 @@ public static void downloadFile(String reqURL, String fileLocation) { if (size <= 0) { GeyserImpl.getInstance().getLogger().error(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); - return null; + //return null; } if (type == null || !type.equals("application/zip")) { GeyserImpl.getInstance().getLogger().error(String.format("Invalid application type from remote pack URL: %s (type: %s)", url, type)); - return null; + //return null; } InputStream in = con.getInputStream(); From 626189fa25e51e905fbf2885e9169b3208d62cb3 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 21 Dec 2023 22:08:15 +0100 Subject: [PATCH 18/35] remove debug, ensure we fully check failed packs, merge master --- .../geysermc/geyser/network/GameProtocol.java | 4 ---- .../geyser/network/UpstreamPacketHandler.java | 22 +++++++++---------- .../registry/loader/ResourcePackLoader.java | 6 +++++ .../org/geysermc/geyser/util/WebUtils.java | 6 ++--- core/src/main/resources/config.yml | 1 + 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index 9607486e46c..42cce607b5b 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -88,10 +88,6 @@ public static boolean isPre1_20_50(GeyserSession session) { return session.getUpstream().getProtocolVersion() < Bedrock_v630.CODEC.getProtocolVersion(); } - public static boolean isPre1_20_30(GeyserSession session) { - return session.getUpstream().getProtocolVersion() < Bedrock_v618.CODEC.getProtocolVersion(); - } - /** * Gets the {@link PacketCodec} for Minecraft: Java Edition. * 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 406d2563099..8459e9f1644 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -44,13 +44,13 @@ import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.MathUtils; import org.geysermc.geyser.util.VersionCheckUtils; -import org.geysermc.geyser.util.WebUtils; import java.io.IOException; import java.nio.ByteBuffer; @@ -61,7 +61,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { private boolean networkSettingsRequested = false; private final Deque packsToSent = new ArrayDeque<>(); - private final List brokenResourcePacks = new ArrayList<>(); + private final Set brokenResourcePacks = new HashSet<>(); private SessionLoadResourcePacksEventImpl resourcePackLoadEvent; @@ -186,7 +186,6 @@ public PacketSignal handle(LoginPacket loginPacket) { } resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); - GeyserImpl.getInstance().getLogger().info(resourcePacksInfo.toString()); session.sendUpstreamPacket(resourcePacksInfo); GeyserLocale.loadGeyserLocale(session.locale()); @@ -280,16 +279,15 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packet.getPackId().toString()); PackCodec codec = pack.codec(); - // Check for packs that the client should normally download on its own. If the client cannot find the pack, we provide it instead. + // If a remote pack ends up here, that usually implies that a platform was not able to download the pack if (codec instanceof UrlPackCodec urlPackCodec) { - if (!GameProtocol.isPre1_20_30(this.session)) { - // Ensure we don't a. spam console, and b. spam download/check requests - if (!brokenResourcePacks.contains(packet.getPackId())) { - brokenResourcePacks.add(packet.getPackId()); - GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded!" + - "Is the pack at the URL " + urlPackCodec.url() + " still available?"); - WebUtils.checkUrlAndDownloadRemotePack(urlPackCodec.url()); - } + // Ensure we don't a. spam console, and b. spam download/check requests + if (!brokenResourcePacks.contains(packet.getPackId())) { + brokenResourcePacks.add(packet.getPackId()); + GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded!" + + "Is the pack at the URL " + urlPackCodec.url() + " still available?"); + // not actually interested in using the download, but this does all the checks we need + ResourcePackLoader.downloadPack(urlPackCodec.url()); } } 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 8f387ba8dd5..6680c382542 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 @@ -100,6 +100,8 @@ public Map load(Path directory) { resourcePacks.add(skullResourcePack); } + + @SuppressWarnings("deprecation") GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); GeyserImpl.getInstance().eventBus().fire(event); @@ -230,8 +232,12 @@ public Map loadRemotePacks() { } public static CompletableFuture<@Nullable Path> downloadPack(String url) throws IllegalArgumentException { + + //TODO check if our cache pack is fine (size, url hash; head req) + return WebUtils.checkUrlAndDownloadRemotePack(url).whenCompleteAsync((cachedPath, throwable) -> { if (cachedPath == null) { + // already warned about in WebUtils return; } 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 b66d3f2928c..487b9b198b4 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -115,12 +115,12 @@ public static void downloadFile(String reqURL, String fileLocation) { if (size <= 0) { GeyserImpl.getInstance().getLogger().error(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); - //return null; + return null; } if (type == null || !type.equals("application/zip")) { GeyserImpl.getInstance().getLogger().error(String.format("Invalid application type from remote pack URL: %s (type: %s)", url, type)); - //return null; + return null; } InputStream in = con.getInputStream(); @@ -137,7 +137,7 @@ public static void downloadFile(String reqURL, String fileLocation) { } catch (MalformedURLException e) { throw new IllegalArgumentException("Malformed URL: " + url); } catch (IOException e) { - throw new RuntimeException("Unable to download and save file: " + url + ")"); + throw new RuntimeException("Unable to download and save remote resource pack from: " + url + ")"); } }); } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 6358a00a518..6f282dbf563 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -174,6 +174,7 @@ force-resource-packs: true # A list of links to send to the client to download resource packs from. # These must be direct links to the resource pack, not a link to a page containing the resource pack. +# If you enter a link here, Geyser will download the resource pack once to check if it's in a valid format. resource-pack-urls: # Example: GeyserOptionalPack - "https://ci.opencollab.dev/job/GeyserMC/job/GeyserOptionalPack/job/master/lastSuccessfulBuild/artifact/GeyserOptionalPack.mcpack" From f12129986e444197716d4b3dc451937e1ee07c78 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Fri, 22 Dec 2023 14:39:47 +0100 Subject: [PATCH 19/35] More robust downloading/caching - better exception catching - proper error handling - caching of packs if size, etag, and last modified are the same --- .../geyser/network/UpstreamPacketHandler.java | 4 +- .../geyser/pack/url/GeyserUrlPackCodec.java | 2 +- .../registry/loader/ResourcePackLoader.java | 9 +-- .../org/geysermc/geyser/util/WebUtils.java | 63 ++++++++++++++++--- 4 files changed, 60 insertions(+), 18 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 8459e9f1644..8d8a4411878 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -284,10 +284,10 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // Ensure we don't a. spam console, and b. spam download/check requests if (!brokenResourcePacks.contains(packet.getPackId())) { brokenResourcePacks.add(packet.getPackId()); - GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded!" + + GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded! " + "Is the pack at the URL " + urlPackCodec.url() + " still available?"); // not actually interested in using the download, but this does all the checks we need - ResourcePackLoader.downloadPack(urlPackCodec.url()); + ResourcePackLoader.downloadPack(urlPackCodec.url(), true); } } 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 f1bb8eb3af0..224ed49208a 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 @@ -81,7 +81,7 @@ public long size() { public ResourcePack create() { if (this.fallback == null) { try { - final Path downloadedPack = ResourcePackLoader.downloadPack(url).whenComplete((pack, throwable) -> { + final Path downloadedPack = ResourcePackLoader.downloadPack(url, false).whenComplete((pack, throwable) -> { if (throwable != null) { GeyserImpl.getInstance().getLogger().error("Failed to download pack from " + url, throwable); if (GeyserImpl.getInstance().getConfig().isDebugMode()) { 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 6680c382542..e41ae76086e 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 @@ -200,7 +200,7 @@ private static ResourcePackManifest readManifest(Path path, String packLocation) } } - public Map loadRemotePacks() { + private Map loadRemotePacks() { final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); // Download CDN packs to get the pack uuid's @@ -231,11 +231,8 @@ public Map loadRemotePacks() { return packMap; } - public static CompletableFuture<@Nullable Path> downloadPack(String url) throws IllegalArgumentException { - - //TODO check if our cache pack is fine (size, url hash; head req) - - return WebUtils.checkUrlAndDownloadRemotePack(url).whenCompleteAsync((cachedPath, throwable) -> { + public static CompletableFuture<@Nullable Path> downloadPack(String url, boolean force) throws IllegalArgumentException { + return WebUtils.checkUrlAndDownloadRemotePack(url, force).whenCompleteAsync((cachedPath, throwable) -> { if (cachedPath == null) { // already warned about in WebUtils return; 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 487b9b198b4..46fc160afdc 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -34,6 +34,7 @@ import java.io.*; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -41,11 +42,15 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; public class WebUtils { + private static final Path REMOTE_PACK_CACHE = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); + /** * Makes a web request to the given URL and returns the body as a string * @@ -103,13 +108,25 @@ public static void downloadFile(String reqURL, String fileLocation) { * If it is, it will download the pack file and return a path to it * * @param url The URL to check + * @param force If true, the pack will be downloaded even if it is cached * @return Path to the downloaded pack file */ - public static CompletableFuture<@Nullable Path> checkUrlAndDownloadRemotePack(String url) { + public static CompletableFuture<@Nullable Path> checkUrlAndDownloadRemotePack(String url, boolean force) { return CompletableFuture.supplyAsync(() -> { try { HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); - con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().toString() + "/" + GeyserImpl.VERSION); + + con.setConnectTimeout(10000); + con.setReadTimeout(10000); + con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION); + con.setInstanceFollowRedirects(false); // TODO verify + + int responseCode = con.getResponseCode(); + if (responseCode >= 400) { + GeyserImpl.getInstance().getLogger().error(String.format("Invalid response code from remote pack URL: %s (code: %d)", url, responseCode)); + return null; + } + int size = con.getContentLength(); String type = con.getContentType(); @@ -123,21 +140,49 @@ public static void downloadFile(String reqURL, String fileLocation) { return null; } + Path packLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".zip"); + Path packMetadata = packLocation.resolveSibling(url.hashCode() + ".metadata"); + + if (Files.exists(packLocation) && Files.exists(packMetadata)) { + try { + List metadataLines = Files.readAllLines(packMetadata, StandardCharsets.UTF_8); + int cachedSize = Integer.parseInt(metadataLines.get(0)); + String cachedEtag = metadataLines.get(1); + long cachedLastModified = Long.parseLong(metadataLines.get(2)); + + if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified()) { + GeyserImpl.getInstance().getLogger().debug("Using cached pack for " + url); + return packLocation; + } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to read cached pack metadata: " + e.getMessage()); + } + } + InputStream in = con.getInputStream(); - Path fileLocation = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").resolve(url.hashCode() + ".zip"); - Files.copy(in, fileLocation, StandardCopyOption.REPLACE_EXISTING); + Files.copy(in, packLocation, StandardCopyOption.REPLACE_EXISTING); - if (Files.size(fileLocation) != size) { - GeyserImpl.getInstance().getLogger().error("Downloaded pack has " + Files.size(fileLocation) + " bytes, expected " + size + " bytes"); - Files.delete(fileLocation); + if (Files.size(packLocation) != size) { + GeyserImpl.getInstance().getLogger().error("Downloaded pack has " + Files.size(packLocation) + " bytes, expected " + size + " bytes"); + Files.delete(packLocation); return null; } - return fileLocation; + try { + Files.write(packMetadata, Arrays.asList(String.valueOf(size), con.getHeaderField("ETag"), String.valueOf(con.getLastModified()))); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); + } + + return packLocation; } catch (MalformedURLException e) { throw new IllegalArgumentException("Malformed URL: " + url); + } catch (SocketTimeoutException | ConnectException e) { + GeyserImpl.getInstance().getLogger().error("Unable to reach URL: " + url + " (" + e.getMessage() + ")"); + return null; } catch (IOException e) { - throw new RuntimeException("Unable to download and save remote resource pack from: " + url + ")"); + e.printStackTrace(); // TODO yeeeeeeeet + throw new RuntimeException("Unable to download and save remote resource pack from: " + url + " (" + e.getMessage() + ")"); } }); } From 303327a388dbda06aaacc49274b468aaee0203ac Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Fri, 22 Dec 2023 14:40:15 +0100 Subject: [PATCH 20/35] oops --- core/src/main/java/org/geysermc/geyser/util/WebUtils.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 46fc160afdc..3a039054f84 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -31,7 +31,12 @@ import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.ConnectException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; From d2622a477bab7453ee0cd60128914aef5426aabb Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 16 Jan 2024 00:47:52 +0100 Subject: [PATCH 21/35] Add a registerAll method to register a collection of resource packs easily --- .../event/bedrock/SessionLoadResourcePacksEvent.java | 9 +++++++++ .../lifecycle/GeyserDefineResourcePacksEvent.java | 10 ++++++++++ .../type/GeyserDefineResourcePacksEventImpl.java | 12 ++++++++++++ .../type/SessionLoadResourcePacksEventImpl.java | 12 ++++++++++++ 4 files changed, 43 insertions(+) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index 71f9e9a0d50..127a1ee8bc9 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -30,6 +30,7 @@ import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import java.util.Collection; import java.util.List; import java.util.UUID; @@ -57,6 +58,14 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { */ public abstract boolean register(@NonNull ResourcePack resourcePack); + /** + * Registers a collection of {@link ResourcePack} to be sent to clients. + * + * @param resourcePacks collection of resourcePack's that will be sent to clients. + * @return true if resource packs were added successfully, false if at least one failed + */ + public abstract boolean registerAll(@NonNull Collection resourcePacks); + /** * Unregisters a {@link ResourcePack} from being sent to the client. * diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java index b62ddb8cca0..b5cb5900123 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -29,6 +29,7 @@ import org.geysermc.event.Event; import org.geysermc.geyser.api.pack.ResourcePack; +import java.util.Collection; import java.util.List; import java.util.UUID; @@ -53,6 +54,15 @@ public abstract class GeyserDefineResourcePacksEvent implements Event { */ public abstract boolean register(@NonNull ResourcePack resourcePack); + /** + * Registers a collection of {@link ResourcePack} to be sent to clients. + * + * @param resourcePacks collection of resourcePack's that will be sent to clients. + * @return true if resource packs were added successfully, false if at least one failed + */ + public abstract boolean registerAll(@NonNull Collection resourcePacks); + + /** * Unregisters a {@link ResourcePack} from being sent to clients. * diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java index 2fae61b54f0..2775b80db65 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java @@ -30,6 +30,7 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; @@ -58,6 +59,17 @@ public boolean register(@NonNull ResourcePack resourcePack) { return true; } + @Override + public boolean registerAll(@NonNull Collection resourcePacks) { + boolean successful = true; + for (ResourcePack pack : resourcePacks) { + if (!this.register(pack)) { + successful = false; + } + } + return successful; + } + @Override public boolean unregister(@NonNull UUID uuid) { return packs.remove(uuid.toString()) != null; diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java index e66396d6a7b..4e71e6b13ac 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -31,6 +31,7 @@ import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.session.GeyserSession; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; @@ -59,6 +60,17 @@ public boolean register(@NonNull ResourcePack resourcePack) { return true; } + @Override + public boolean registerAll(@NonNull Collection resourcePacks) { + boolean successful = true; + for (ResourcePack pack : resourcePacks) { + if (!this.register(pack)) { + successful = false; + } + } + return successful; + } + @Override public boolean unregister(@NonNull UUID uuid) { return packs.remove(uuid.toString()) != null; From 2e776c456106d6cf4f176963c87dc6749005408e Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Fri, 26 Jan 2024 19:01:12 +0100 Subject: [PATCH 22/35] Allow null content key --- .../java/org/geysermc/geyser/api/pack/PackCodec.java | 5 +++-- .../java/org/geysermc/geyser/api/pack/UrlPackCodec.java | 7 ++++--- .../org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java | 9 +++++++-- .../src/main/java/org/geysermc/geyser/util/WebUtils.java | 5 ++--- core/src/main/resources/config.yml | 1 + 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index ff9f82c5a56..7c123b62442 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.api.pack; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.api.GeyserApi; import java.io.IOException; @@ -84,11 +85,11 @@ public static PackCodec path(@NonNull Path path) { * Creates a new pack provider from the given url and content key. * * @param url the url to create the pack provider from - * @param contentKey the content key, leave empty if pack is not encrypted + * @param contentKey the content key, leave empty or null if pack is not encrypted * @return the new pack provider */ @NonNull - public static PackCodec url(@NonNull String url, @NonNull String contentKey) { + public static PackCodec url(@NonNull String url, @Nullable String contentKey) { return GeyserApi.api().provider(UrlPackCodec.class, url, contentKey); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java index b98e59355d2..ee449aa7f32 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java @@ -34,10 +34,10 @@ * Due to Bedrock limitations, the URL must: *
    *
  • be a direct download link to a .zip or .mcpack resource pack
  • - *
  • Use application type `application/zip` and set a correct content length
  • + *
  • use the application type `application/zip` and set a correct content length
  • *
* - * Additionally, the ResourcePack must be zipped in a folder enclosing the resource pack, instead of the resource pack being at the root of the zip. + * Additionally, the resource pack must be zipped in a folder enclosing the resource pack, instead of the resource pack being at the root of the zip. */ public abstract class UrlPackCodec extends PackCodec { @@ -51,7 +51,8 @@ public abstract class UrlPackCodec extends PackCodec { /** * If the remote pack has an encryption key, it must be specified here. - * Otherwise, leave empty. + * This will return empty if none is specified. + * * @return the encryption key of the resource pack */ @NonNull 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 224ed49208a..04bb7066388 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,6 +27,7 @@ import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.UrlPackCodec; @@ -47,9 +48,13 @@ public GeyserUrlPackCodec(String url) throws IllegalArgumentException { this(url, ""); } - public GeyserUrlPackCodec(String url, String contentKey) throws IllegalArgumentException { + public GeyserUrlPackCodec(@NonNull String url, @Nullable String contentKey) throws IllegalArgumentException { + //noinspection ConstantValue - need to enforce + if (url == null) { + throw new IllegalArgumentException("Url cannot be nulL!"); + } this.url = url; - this.contentKey = contentKey; + this.contentKey = contentKey == null ? "" : contentKey; } @Override 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 3a039054f84..47f9ec22e4e 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -155,7 +155,7 @@ public static void downloadFile(String reqURL, String fileLocation) { String cachedEtag = metadataLines.get(1); long cachedLastModified = Long.parseLong(metadataLines.get(2)); - if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified()) { + if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified() && !force) { GeyserImpl.getInstance().getLogger().debug("Using cached pack for " + url); return packLocation; } @@ -168,7 +168,7 @@ public static void downloadFile(String reqURL, String fileLocation) { Files.copy(in, packLocation, StandardCopyOption.REPLACE_EXISTING); if (Files.size(packLocation) != size) { - GeyserImpl.getInstance().getLogger().error("Downloaded pack has " + Files.size(packLocation) + " bytes, expected " + size + " bytes"); + GeyserImpl.getInstance().getLogger().error(String.format("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes!", url, Files.size(packLocation), size)); Files.delete(packLocation); return null; } @@ -186,7 +186,6 @@ public static void downloadFile(String reqURL, String fileLocation) { GeyserImpl.getInstance().getLogger().error("Unable to reach URL: " + url + " (" + e.getMessage() + ")"); return null; } catch (IOException e) { - e.printStackTrace(); // TODO yeeeeeeeet throw new RuntimeException("Unable to download and save remote resource pack from: " + url + " (" + e.getMessage() + ")"); } }); diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 912d514590a..52e0d1ac902 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -178,6 +178,7 @@ force-resource-packs: true # A list of links to send to the client to download resource packs from. # These must be direct links to the resource pack, not a link to a page containing the resource pack. # If you enter a link here, Geyser will download the resource pack once to check if it's in a valid format. +# See https://wiki.geysermc.org/geyser/packs for more info. resource-pack-urls: # Example: GeyserOptionalPack - "https://ci.opencollab.dev/job/GeyserMC/job/GeyserOptionalPack/job/master/lastSuccessfulBuild/artifact/GeyserOptionalPack.mcpack" From b8fa18a155ef3c07517eeb0f97e5dd53ddd0cdf8 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Mon, 19 Feb 2024 21:26:35 +0100 Subject: [PATCH 23/35] start: don't try to delete broken packs while we are still delivering them --- .../geyser/network/UpstreamPacketHandler.java | 9 ++++----- .../geyser/pack/url/GeyserUrlPackCodec.java | 4 ++-- .../geyser/registry/loader/ResourcePackLoader.java | 8 ++++---- .../java/org/geysermc/geyser/util/WebUtils.java | 13 ++++++++----- 4 files changed, 18 insertions(+), 16 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 da3f0c07183..f52ec378e8b 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -77,16 +77,15 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; -import java.util.HashSet; +import java.util.Map; import java.util.OptionalInt; -import java.util.Set; import java.util.UUID; public class UpstreamPacketHandler extends LoggingPacketHandler { private boolean networkSettingsRequested = false; private final Deque packsToSent = new ArrayDeque<>(); - private final Set brokenResourcePacks = new HashSet<>(); + private final Map brokenResourcePacks = new HashMap<>(); private final CompressionStrategy compressionStrategy; private SessionLoadResourcePacksEventImpl resourcePackLoadEvent; @@ -315,8 +314,8 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // If a remote pack ends up here, that usually implies that a platform was not able to download the pack if (codec instanceof UrlPackCodec urlPackCodec) { // Ensure we don't a. spam console, and b. spam download/check requests - if (!brokenResourcePacks.contains(packet.getPackId())) { - brokenResourcePacks.add(packet.getPackId()); + if (!brokenResourcePacks.containsKey(packet.getPackId())) { + brokenResourcePacks.put(packet.getPackId(), ""); GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded! " + "Is the pack at the URL " + urlPackCodec.url() + " still available?"); // not actually interested in using the download, but this does all the checks we need 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 04bb7066388..0b0ce32e3cf 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 @@ -88,9 +88,9 @@ public ResourcePack create() { try { final Path downloadedPack = ResourcePackLoader.downloadPack(url, false).whenComplete((pack, throwable) -> { if (throwable != null) { - GeyserImpl.getInstance().getLogger().error("Failed to download pack from " + url, throwable); + GeyserImpl.getInstance().getLogger().error("Failed to download pack from " + url + " due to " + throwable.getMessage()); if (GeyserImpl.getInstance().getConfig().isDebugMode()) { - throwable.printStackTrace(); + GeyserImpl.getInstance().getLogger().error("full error: " + throwable); } } }).join(); 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 e41ae76086e..bc784b85dfb 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 @@ -231,8 +231,8 @@ private Map loadRemotePacks() { return packMap; } - public static CompletableFuture<@Nullable Path> downloadPack(String url, boolean force) throws IllegalArgumentException { - return WebUtils.checkUrlAndDownloadRemotePack(url, force).whenCompleteAsync((cachedPath, throwable) -> { + public static CompletableFuture<@Nullable Path> downloadPack(String url, boolean checking) throws IllegalArgumentException { + return WebUtils.checkUrlAndDownloadRemotePack(url, checking).whenCompleteAsync((cachedPath, throwable) -> { if (cachedPath == null) { // already warned about in WebUtils return; @@ -257,9 +257,9 @@ private Map loadRemotePacks() { // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { - throw new IllegalArgumentException("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + + /*throw new IllegalArgumentException("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + "This is not supported for remote packs, and will cause Bedrock clients to fall back to request the pack from the server. " + - "Please put the pack file in a subfolder, and provide that zip in the URL."); + "Please put the pack file in a subfolder, and provide that zip in the URL."); */ } } catch (IOException e) { throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), 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 47f9ec22e4e..5bf77477a39 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -113,7 +113,7 @@ public static void downloadFile(String reqURL, String fileLocation) { * If it is, it will download the pack file and return a path to it * * @param url The URL to check - * @param force If true, the pack will be downloaded even if it is cached + * @param force If true, the pack will be downloaded even if it is cached to a separate location. * @return Path to the downloaded pack file */ public static CompletableFuture<@Nullable Path> checkUrlAndDownloadRemotePack(String url, boolean force) { @@ -124,7 +124,7 @@ public static void downloadFile(String reqURL, String fileLocation) { con.setConnectTimeout(10000); con.setReadTimeout(10000); con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION); - con.setInstanceFollowRedirects(false); // TODO verify + con.setInstanceFollowRedirects(true); int responseCode = con.getResponseCode(); if (responseCode >= 400) { @@ -140,11 +140,13 @@ public static void downloadFile(String reqURL, String fileLocation) { return null; } + // This doesn't seem to be a requirement (anymore?) Logging to debug might be interesting though. if (type == null || !type.equals("application/zip")) { - GeyserImpl.getInstance().getLogger().error(String.format("Invalid application type from remote pack URL: %s (type: %s)", url, type)); - return null; + GeyserImpl.getInstance().getLogger().debug(String.format("Application type from remote pack URL: %s (type: %s)", url, type)); } + // TODO: add logic here to *not* delete the cached pack (and only at shutdown). + Path packLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".zip"); Path packMetadata = packLocation.resolveSibling(url.hashCode() + ".metadata"); @@ -170,7 +172,7 @@ public static void downloadFile(String reqURL, String fileLocation) { if (Files.size(packLocation) != size) { GeyserImpl.getInstance().getLogger().error(String.format("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes!", url, Files.size(packLocation), size)); Files.delete(packLocation); - return null; + //return null; } try { @@ -179,6 +181,7 @@ public static void downloadFile(String reqURL, String fileLocation) { GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); } + GeyserImpl.getInstance().getLogger().info("debug: pack downloaded"); return packLocation; } catch (MalformedURLException e) { throw new IllegalArgumentException("Malformed URL: " + url); From c6511a0549c9c586b936da5ee2e3f08339e65a24 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 22 Feb 2024 17:46:47 +0100 Subject: [PATCH 24/35] update to "new" pack requirements --- .../java/org/geysermc/geyser/GeyserImpl.java | 32 +++++-- .../geyser/network/UpstreamPacketHandler.java | 9 +- .../registry/loader/ResourcePackLoader.java | 88 +++++++++++++++---- .../org/geysermc/geyser/util/WebUtils.java | 48 +++++----- 4 files changed, 126 insertions(+), 51 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 5ed0c3947e3..f87acc2d46e 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -44,9 +44,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.geysermc.api.Geyser; -import org.geysermc.geyser.api.command.CommandSource; -import org.geysermc.geyser.api.util.MinecraftVersion; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.cumulus.form.Form; import org.geysermc.cumulus.form.util.FormBuilder; import org.geysermc.erosion.packet.Packets; @@ -56,12 +53,19 @@ import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.news.NewsItemAction; import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.api.event.EventBus; import org.geysermc.geyser.api.event.EventRegistrar; -import org.geysermc.geyser.api.event.lifecycle.*; +import org.geysermc.geyser.api.event.lifecycle.GeyserPostInitializeEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPostReloadEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPreInitializeEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPreReloadEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserShutdownEvent; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.BedrockListener; import org.geysermc.geyser.api.network.RemoteServer; +import org.geysermc.geyser.api.util.MinecraftVersion; +import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.GeyserCommandManager; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.entity.EntityDefinitions; @@ -74,6 +78,7 @@ import org.geysermc.geyser.network.netty.GeyserServer; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; import org.geysermc.geyser.registry.provider.ProviderSupplier; import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.session.GeyserSession; @@ -85,7 +90,13 @@ import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.util.*; +import org.geysermc.geyser.util.AssetUtils; +import org.geysermc.geyser.util.CooldownUtils; +import org.geysermc.geyser.util.DimensionUtils; +import org.geysermc.geyser.util.Metrics; +import org.geysermc.geyser.util.NewsHandler; +import org.geysermc.geyser.util.VersionCheckUtils; +import org.geysermc.geyser.util.WebUtils; import java.io.File; import java.io.FileWriter; @@ -96,7 +107,14 @@ import java.nio.file.Path; import java.security.Key; import java.text.DecimalFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; @@ -637,7 +655,7 @@ public void disable() { this.erosionUnixListener.close(); } - Registries.RESOURCE_PACKS.get().clear(); + ResourcePackLoader.clear(); bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.done")); } 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 f52ec378e8b..04f59e3aa55 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -313,14 +313,7 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // If a remote pack ends up here, that usually implies that a platform was not able to download the pack if (codec instanceof UrlPackCodec urlPackCodec) { - // Ensure we don't a. spam console, and b. spam download/check requests - if (!brokenResourcePacks.containsKey(packet.getPackId())) { - brokenResourcePacks.put(packet.getPackId(), ""); - GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded! " + - "Is the pack at the URL " + urlPackCodec.url() + " still available?"); - // not actually interested in using the download, but this does all the checks we need - ResourcePackLoader.downloadPack(urlPackCodec.url(), true); - } + ResourcePackLoader.checkPack(urlPackCodec); } data.setChunkIndex(packet.getChunkIndex()); 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 bc784b85dfb..97ed256f90f 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 @@ -31,12 +31,14 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; 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.event.type.GeyserDefineResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePackManifest; import org.geysermc.geyser.pack.SkullResourcePackManager; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.pack.url.GeyserUrlPackCodec; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.WebUtils; @@ -48,8 +50,10 @@ import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -58,10 +62,16 @@ import java.util.zip.ZipFile; /** - * Loads {@link ResourcePack}s within a {@link Path} directory, firing the {@link GeyserLoadResourcePacksEvent}. + * Loads {@link ResourcePack}s within a {@link Path} directory, firing the {@link GeyserDefineResourcePacksEventImpl}. */ public class ResourcePackLoader implements RegistryLoader> { + /** + * Used to keep track of remote resource packs that the client rejected. + * If a client rejects such a pack, it falls back to the old method, and Geyser serves a cached variant. + */ + private static final Set brokenPacks = new HashSet<>(); + static final PathMatcher PACK_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**.{zip,mcpack}"); private static final boolean SHOW_RESOURCE_PACK_LENGTH_WARNING = Boolean.parseBoolean(System.getProperty("Geyser.ShowResourcePackLengthWarning", "true")); @@ -100,7 +110,6 @@ public Map load(Path directory) { resourcePacks.add(skullResourcePack); } - @SuppressWarnings("deprecation") GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); GeyserImpl.getInstance().eventBus().fire(event); @@ -116,11 +125,9 @@ public Map load(Path directory) { // Load CDN entries packMap.putAll(loadRemotePacks()); - GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap); - packMap = defineEvent.getPacks(); - return packMap; + return defineEvent.getPacks(); } /** @@ -152,6 +159,14 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep return new GeyserResourcePack(new GeyserPathPackCodec(path), manifest, contentKey); } + /** + * Reads a Resource pack from a URL codec, and returns a resource pack. Unlike {@link ResourcePackLoader#readPack(Path)} + * this method reads content keys differently. + * + * @param codec the URL pack codec with the url to download the pack from + * @return a {@link GeyserResourcePack} representation + * @throws IllegalArgumentException if there was an error reading the pack. + */ public static GeyserResourcePack readPack(GeyserUrlPackCodec codec) throws IllegalArgumentException { Path path = codec.getFallback().path(); if (!PACK_MATCHER.matches(path)) { @@ -231,6 +246,15 @@ private Map loadRemotePacks() { return packMap; } + public static void checkPack(UrlPackCodec codec) { + if (!brokenPacks.contains(codec)) { + brokenPacks.add(codec); + GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded! " + + "Is the pack at the URL " + codec.url() + " still available?"); + downloadPack(codec.url(), true); + } + } + public static CompletableFuture<@Nullable Path> downloadPack(String url, boolean checking) throws IllegalArgumentException { return WebUtils.checkUrlAndDownloadRemotePack(url, checking).whenCompleteAsync((cachedPath, throwable) -> { if (cachedPath == null) { @@ -239,7 +263,7 @@ private Map loadRemotePacks() { } if (throwable != null) { - GeyserImpl.getInstance().getLogger().error("Failed to download resource pack " + url, throwable); + GeyserImpl.getInstance().getLogger().error("Failed to download resource pack! ", throwable); return; } @@ -248,22 +272,54 @@ private Map loadRemotePacks() { throw new IllegalArgumentException("Invalid pack format! Not a .zip or .mcpack file."); } - try { - ZipFile zip = new ZipFile(cachedPath.toFile()); - if (zip.stream().noneMatch(x -> x.getName().contains("manifest.json"))) { - throw new IllegalArgumentException(url + " does not contain a manifest file."); + if (checking) { + try { + Files.delete(cachedPath); + } catch (IOException e) { + throw new IllegalArgumentException("Could not delete debug pack! " + e.getMessage(), e); } + } - // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. - // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) - if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { - /*throw new IllegalArgumentException("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + - "This is not supported for remote packs, and will cause Bedrock clients to fall back to request the pack from the server. " + - "Please put the pack file in a subfolder, and provide that zip in the URL."); */ + try { + try (ZipFile zip = new ZipFile(cachedPath.toFile())) { + if (zip.stream().noneMatch(x -> x.getName().contains("manifest.json"))) { + throw new IllegalArgumentException(url + " does not contain a manifest file."); + } + +// // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. +// // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) +// if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { +// GeyserImpl.getInstance().getLogger().debug("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + +// "This is not supported for remote packs, and will cause Bedrock clients to fall back to request the pack from the server. " + +// "Please put the pack file in a subfolder, and provide that zip in the URL."); +// } } } catch (IOException e) { throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); } }); } + + public static void clear() { + Registries.RESOURCE_PACKS.get().clear(); + + // Now: let's clean up broken remote packs, so we don't cache them + Path location = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); + brokenPacks.forEach(codec -> { + int hash = codec.url().hashCode(); + Path packLocation = location.resolve(hash + ".zip"); + Path packMetadata = packLocation.resolveSibling(hash + ".metadata"); + + try { + if (packMetadata.toFile().exists()) { + Files.delete(packMetadata); + } + if (packLocation.toFile().exists()) { + Files.delete(packLocation); + } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not delete broken cached resource packs! " + 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 5bf77477a39..a4555218ed8 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.JsonNode; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.GeyserLogger; import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; @@ -117,6 +118,7 @@ public static void downloadFile(String reqURL, String fileLocation) { * @return Path to the downloaded pack file */ public static CompletableFuture<@Nullable Path> checkUrlAndDownloadRemotePack(String url, boolean force) { + GeyserLogger logger = GeyserImpl.getInstance().getLogger(); return CompletableFuture.supplyAsync(() -> { try { HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); @@ -128,7 +130,7 @@ public static void downloadFile(String reqURL, String fileLocation) { int responseCode = con.getResponseCode(); if (responseCode >= 400) { - GeyserImpl.getInstance().getLogger().error(String.format("Invalid response code from remote pack URL: %s (code: %d)", url, responseCode)); + logger.error(String.format("Invalid response code from remote pack URL: %s (code: %d)", url, responseCode)); return null; } @@ -136,29 +138,27 @@ public static void downloadFile(String reqURL, String fileLocation) { String type = con.getContentType(); if (size <= 0) { - GeyserImpl.getInstance().getLogger().error(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); + logger.error(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); return null; } - // This doesn't seem to be a requirement (anymore?) Logging to debug might be interesting though. + // This doesn't seem to be a requirement (anymore?). Logging to debug might be interesting though. if (type == null || !type.equals("application/zip")) { - GeyserImpl.getInstance().getLogger().debug(String.format("Application type from remote pack URL: %s (type: %s)", url, type)); + logger.debug(String.format("Application type from remote pack URL: %s (type: %s)", url, type)); } - // TODO: add logic here to *not* delete the cached pack (and only at shutdown). - Path packLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".zip"); Path packMetadata = packLocation.resolveSibling(url.hashCode() + ".metadata"); - if (Files.exists(packLocation) && Files.exists(packMetadata)) { + if (Files.exists(packLocation) && Files.exists(packMetadata) && !force) { try { List metadataLines = Files.readAllLines(packMetadata, StandardCharsets.UTF_8); int cachedSize = Integer.parseInt(metadataLines.get(0)); String cachedEtag = metadataLines.get(1); long cachedLastModified = Long.parseLong(metadataLines.get(2)); - if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified() && !force) { - GeyserImpl.getInstance().getLogger().debug("Using cached pack for " + url); + if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified()) { + logger.debug("Using cached pack for " + url); return packLocation; } } catch (IOException e) { @@ -166,23 +166,31 @@ public static void downloadFile(String reqURL, String fileLocation) { } } - InputStream in = con.getInputStream(); - Files.copy(in, packLocation, StandardCopyOption.REPLACE_EXISTING); + Path downloadLocation = force ? REMOTE_PACK_CACHE.resolve(url.hashCode() + "_debug") : packLocation; + Files.copy(con.getInputStream(), downloadLocation, StandardCopyOption.REPLACE_EXISTING); - if (Files.size(packLocation) != size) { + // This needs to match as the client fails to download the pack otherwise + if (Files.size(downloadLocation) != size) { GeyserImpl.getInstance().getLogger().error(String.format("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes!", url, Files.size(packLocation), size)); - Files.delete(packLocation); - //return null; + Files.delete(downloadLocation); + return null; } - try { - Files.write(packMetadata, Arrays.asList(String.valueOf(size), con.getHeaderField("ETag"), String.valueOf(con.getLastModified()))); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); + // "Force" runs when the client rejected a pack. This is done for diagnosis of the issue. + if (force) { + if (Files.size(packLocation) != Files.size(downloadLocation)) { + logger.error("The pack size seems to have changed. If you wish to change the pack at the remote URL, restart/reload Geyser. " + + "Changing the pack mid-game can result in clients rejecting the pack, connected clients having different pack, or similar. "); + } + } else { + try { + Files.write(packMetadata, Arrays.asList(String.valueOf(size), con.getHeaderField("ETag"), String.valueOf(con.getLastModified()))); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); + } } - GeyserImpl.getInstance().getLogger().info("debug: pack downloaded"); - return packLocation; + return downloadLocation; } catch (MalformedURLException e) { throw new IllegalArgumentException("Malformed URL: " + url); } catch (SocketTimeoutException | ConnectException e) { From 86f645899f005ff01a6d4057b43cc4283ac83530 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Wed, 19 Jun 2024 23:35:36 +0200 Subject: [PATCH 25/35] Code cleanup, less futures, more exceptions when needed --- .../geysermc/geyser/api/pack/PackCodec.java | 2 +- .../geyser/api/pack/UrlPackCodec.java | 2 - .../java/org/geysermc/geyser/GeyserImpl.java | 2 - .../geyser/network/UpstreamPacketHandler.java | 2 +- .../geyser/pack/url/GeyserUrlPackCodec.java | 48 +++--- .../registry/loader/ResourcePackLoader.java | 97 +++++------ .../org/geysermc/geyser/util/WebUtils.java | 153 +++++++++--------- 7 files changed, 147 insertions(+), 159 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index 7c123b62442..ba062535d5f 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -68,7 +68,7 @@ public abstract class PackCodec { * @return the new resource pack */ @NonNull - protected abstract ResourcePack create(); + public abstract ResourcePack create(); /** * Creates a new pack provider from the given path. diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java index ee449aa7f32..8f279ae0da4 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java @@ -36,8 +36,6 @@ *
  • be a direct download link to a .zip or .mcpack resource pack
  • *
  • use the application type `application/zip` and set a correct content length
  • * - * - * Additionally, the resource pack must be zipped in a folder enclosing the resource pack, instead of the resource pack being at the root of the zip. */ public abstract class UrlPackCodec extends PackCodec { diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index cc31de9c506..0d47560b8cf 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -650,8 +650,6 @@ public void disable() { this.erosionUnixListener.close(); } - // todo check - //Registries.RESOURCE_PACKS.get().clear(); ResourcePackLoader.clear(); this.setEnabled(false); 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 5061a7ef2c9..1f1d69136be 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -319,7 +319,7 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // If a remote pack ends up here, that usually implies that a platform was not able to download the pack if (codec instanceof UrlPackCodec urlPackCodec) { - ResourcePackLoader.checkPack(urlPackCodec); + ResourcePackLoader.testUrlPack(urlPackCodec); } data.setChunkIndex(packet.getChunkIndex()); 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 0b0ce32e3cf..2c561fe1ad6 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 @@ -28,56 +28,46 @@ import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.UrlPackCodec; -import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.registry.loader.ResourcePackLoader; import java.io.IOException; import java.nio.channels.SeekableByteChannel; -import java.nio.file.Path; +import java.util.Objects; public class GeyserUrlPackCodec extends UrlPackCodec { - private final String url; - private final String contentKey; + private final @NonNull String url; + private final @Nullable String contentKey; @Getter - private GeyserPathPackCodec fallback; + private PathPackCodec fallback; public GeyserUrlPackCodec(String url) throws IllegalArgumentException { - this(url, ""); + this(url, null); } public GeyserUrlPackCodec(@NonNull String url, @Nullable String contentKey) throws IllegalArgumentException { - //noinspection ConstantValue - need to enforce - if (url == null) { - throw new IllegalArgumentException("Url cannot be nulL!"); - } + Objects.requireNonNull(url, "url cannot be null"); this.url = url; - this.contentKey = contentKey == null ? "" : contentKey; + this.contentKey = contentKey; } @Override public byte @NonNull [] sha256() { - if (this.fallback == null) { - throw new IllegalStateException("Fallback pack not initialized! Needs to be created first."); - } + Objects.requireNonNull(fallback, "must call #create() before attempting to get the sha256!"); return fallback.sha256(); } @Override public long size() { - if (this.fallback == null) { - throw new IllegalStateException("Fallback pack not initialized! Needs to be created first."); - } + Objects.requireNonNull(fallback, "must call #create() before attempting to get the size!"); return fallback.size(); } @Override public @NonNull SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { - if (this.fallback == null) { - throw new IllegalStateException("Fallback pack not initialized! Needs to be created first."); - } + Objects.requireNonNull(fallback, "must call #create() before attempting to serialize!!"); return fallback.serialize(resourcePack); } @@ -86,17 +76,15 @@ public long size() { public ResourcePack create() { if (this.fallback == null) { try { - final Path downloadedPack = ResourcePackLoader.downloadPack(url, false).whenComplete((pack, throwable) -> { + ResourcePackLoader.downloadPack(url, false).whenComplete((pack, throwable) -> { if (throwable != null) { - GeyserImpl.getInstance().getLogger().error("Failed to download pack from " + url + " due to " + throwable.getMessage()); - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { - GeyserImpl.getInstance().getLogger().error("full error: " + throwable); - } + throw new IllegalArgumentException(throwable); + } else if (pack != null) { + this.fallback = pack; } - }).join(); - this.fallback = new GeyserPathPackCodec(downloadedPack); + }); } catch (Exception e) { - throw new IllegalArgumentException("Failed to download pack from " + url, e); + throw new IllegalArgumentException("Failed to download pack from the url %s (reason: %s)!".formatted(url, e.getMessage())); } } return ResourcePackLoader.readPack(this); @@ -109,6 +97,6 @@ public ResourcePack create() { @Override public @NonNull String contentKey() { - return this.contentKey; + return this.contentKey != null ? contentKey : ""; } } 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 97ed256f90f..2e8fe49cfb5 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 @@ -25,10 +25,13 @@ package org.geysermc.geyser.registry.loader; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; +import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.api.pack.UrlPackCodec; @@ -49,12 +52,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -70,7 +70,9 @@ public class ResourcePackLoader implements RegistryLoader brokenPacks = new HashSet<>(); + private static final Cache CACHED_FAILED_PACKS = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(); static final PathMatcher PACK_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**.{zip,mcpack}"); @@ -169,9 +171,6 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep */ public static GeyserResourcePack readPack(GeyserUrlPackCodec codec) throws IllegalArgumentException { Path path = codec.getFallback().path(); - if (!PACK_MATCHER.matches(path)) { - throw new IllegalArgumentException("The url " + codec.url() + " did not provide a valid resource pack! Please check the url and try again."); - } ResourcePackManifest manifest = readManifest(path, codec.url()); String contentKey = codec.contentKey(); @@ -216,9 +215,9 @@ private static ResourcePackManifest readManifest(Path path, String packLocation) } private Map loadRemotePacks() { + // Unable to make this a static variable, as the test would fail final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); - // Download CDN packs to get the pack uuid's if (!Files.exists(cachedCdnPacksDirectory)) { try { Files.createDirectories(cachedCdnPacksDirectory); @@ -231,7 +230,7 @@ private Map loadRemotePacks() { List remotePackUrls = GeyserImpl.getInstance().getConfig().getResourcePackUrls(); Map packMap = new Object2ObjectOpenHashMap<>(); - for (String url: remotePackUrls) { + for (String url : remotePackUrls) { try { GeyserUrlPackCodec codec = new GeyserUrlPackCodec(url); ResourcePack pack = codec.create(); @@ -246,57 +245,65 @@ private Map loadRemotePacks() { return packMap; } - public static void checkPack(UrlPackCodec codec) { - if (!brokenPacks.contains(codec)) { - brokenPacks.add(codec); - GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded! " + - "Is the pack at the URL " + codec.url() + " still available?"); + /** + * Used when a Bedrock client requests a Bedrock resource pack from the server when it should be downloading it + * from a remote provider. Since this would be called each time a Bedrock client requests a piece of the Bedrock pack, + * this uses a cache to ensure we aren't re-checking a dozen times. + * + * @param codec the codec of the resource pack that wasn't successfully downloaded by a Bedrock client. + */ + public static void testUrlPack(UrlPackCodec codec) { + if (CACHED_FAILED_PACKS.getIfPresent(codec.url()) == null) { + CACHED_FAILED_PACKS.put(codec.url(), codec); + GeyserImpl.getInstance().getLogger().warning("A client was not able to download the resource pack at %s. Is it still available? Running check now:"); downloadPack(codec.url(), true); } } - public static CompletableFuture<@Nullable Path> downloadPack(String url, boolean checking) throws IllegalArgumentException { - return WebUtils.checkUrlAndDownloadRemotePack(url, checking).whenCompleteAsync((cachedPath, throwable) -> { - if (cachedPath == null) { - // already warned about in WebUtils - return; - } + public static CompletableFuture<@Nullable PathPackCodec> downloadPack(String url, boolean testing) throws IllegalArgumentException { + return CompletableFuture.supplyAsync(() -> { + Path path = WebUtils.checkUrlAndDownloadRemotePack(url, testing); - if (throwable != null) { - GeyserImpl.getInstance().getLogger().error("Failed to download resource pack! ", throwable); - return; + // Already warned about these above + if (path == null) { + return null; } // Check if the pack is a .zip or .mcpack file - if (!PACK_MATCHER.matches(cachedPath)) { + if (!PACK_MATCHER.matches(path)) { throw new IllegalArgumentException("Invalid pack format! Not a .zip or .mcpack file."); } - if (checking) { - try { - Files.delete(cachedPath); - } catch (IOException e) { - throw new IllegalArgumentException("Could not delete debug pack! " + e.getMessage(), e); - } - } - try { - try (ZipFile zip = new ZipFile(cachedPath.toFile())) { + try (ZipFile zip = new ZipFile(path.toFile())) { if (zip.stream().noneMatch(x -> x.getName().contains("manifest.json"))) { - throw new IllegalArgumentException(url + " does not contain a manifest file."); + throw new IllegalArgumentException("The pack at the url " + url + " does not contain a manifest file!"); } -// // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. -// // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) -// if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { -// GeyserImpl.getInstance().getLogger().debug("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + -// "This is not supported for remote packs, and will cause Bedrock clients to fall back to request the pack from the server. " + -// "Please put the pack file in a subfolder, and provide that zip in the URL."); -// } + // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. + // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) + if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { + if (testing) { + GeyserImpl.getInstance().getLogger().info("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + + "This may not work for remote packs, and could cause Bedrock clients to fall back to request the pack from the server. " + + "Please put the pack file in a subfolder, and provide that zip in the URL."); + } + } } } catch (IOException e) { throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); } + + if (testing) { + try { + Files.delete(path); + return null; + } catch (IOException e) { + throw new IllegalStateException("Could not delete debug pack! " + e.getMessage(), e); + } + } + + return new GeyserPathPackCodec(path); }); } @@ -305,7 +312,7 @@ public static void clear() { // Now: let's clean up broken remote packs, so we don't cache them Path location = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); - brokenPacks.forEach(codec -> { + for (UrlPackCodec codec : CACHED_FAILED_PACKS.asMap().values()) { int hash = codec.url().hashCode(); Path packLocation = location.resolve(hash + ".zip"); Path packMetadata = packLocation.resolveSibling(hash + ".metadata"); @@ -320,6 +327,6 @@ public static void clear() { } catch (IOException e) { GeyserImpl.getInstance().getLogger().error("Could not delete broken cached resource packs! " + 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 4ce20e835dc..3cacdccd243 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -33,11 +33,7 @@ import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; import java.io.*; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.net.URLEncoder; +import java.net.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -47,7 +43,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; -import java.util.concurrent.CompletableFuture; public class WebUtils { @@ -111,91 +106,93 @@ public static void downloadFile(String reqURL, String fileLocation) { * * @param url The URL to check * @param force If true, the pack will be downloaded even if it is cached to a separate location. - * @return Path to the downloaded pack file + * @return Path to the downloaded pack file, or null if it was unable to be loaded */ - public static CompletableFuture<@Nullable Path> checkUrlAndDownloadRemotePack(String url, boolean force) { + public static @Nullable Path checkUrlAndDownloadRemotePack(String url, boolean force) { GeyserLogger logger = GeyserImpl.getInstance().getLogger(); - return CompletableFuture.supplyAsync(() -> { - try { - HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); - - con.setConnectTimeout(10000); - con.setReadTimeout(10000); - con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION); - con.setInstanceFollowRedirects(true); - - int responseCode = con.getResponseCode(); - if (responseCode >= 400) { - logger.error(String.format("Invalid response code from remote pack URL: %s (code: %d)", url, responseCode)); - return null; - } + try { + HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); - int size = con.getContentLength(); - String type = con.getContentType(); + con.setConnectTimeout(10000); + con.setReadTimeout(10000); + con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION); + con.setInstanceFollowRedirects(true); - if (size <= 0) { - logger.error(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); - return null; - } + int responseCode = con.getResponseCode(); + if (responseCode >= 400) { + throw new IllegalStateException(String.format("Invalid response code from remote pack URL: %s (code: %d)", url, responseCode)); + } - // This doesn't seem to be a requirement (anymore?). Logging to debug might be interesting though. - if (type == null || !type.equals("application/zip")) { - logger.debug(String.format("Application type from remote pack URL: %s (type: %s)", url, type)); - } + int size = con.getContentLength(); + String type = con.getContentType(); - Path packLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".zip"); - Path packMetadata = packLocation.resolveSibling(url.hashCode() + ".metadata"); - - if (Files.exists(packLocation) && Files.exists(packMetadata) && !force) { - try { - List metadataLines = Files.readAllLines(packMetadata, StandardCharsets.UTF_8); - int cachedSize = Integer.parseInt(metadataLines.get(0)); - String cachedEtag = metadataLines.get(1); - long cachedLastModified = Long.parseLong(metadataLines.get(2)); - - if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified()) { - logger.debug("Using cached pack for " + url); - return packLocation; - } - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Failed to read cached pack metadata: " + e.getMessage()); - } - } + if (size <= 0) { + throw new IllegalArgumentException(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); + } - Path downloadLocation = force ? REMOTE_PACK_CACHE.resolve(url.hashCode() + "_debug") : packLocation; - Files.copy(con.getInputStream(), downloadLocation, StandardCopyOption.REPLACE_EXISTING); + // This doesn't seem to be a requirement (anymore?). Logging to debug might be interesting though. + if (type == null || !type.equals("application/zip")) { + logger.debug(String.format("Application type from remote pack URL: %s (type: %s)", url, type)); + } - // This needs to match as the client fails to download the pack otherwise - if (Files.size(downloadLocation) != size) { - GeyserImpl.getInstance().getLogger().error(String.format("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes!", url, Files.size(packLocation), size)); - Files.delete(downloadLocation); - return null; - } + Path packLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".zip"); + Path packMetadata = packLocation.resolveSibling(url.hashCode() + ".metadata"); - // "Force" runs when the client rejected a pack. This is done for diagnosis of the issue. - if (force) { - if (Files.size(packLocation) != Files.size(downloadLocation)) { - logger.error("The pack size seems to have changed. If you wish to change the pack at the remote URL, restart/reload Geyser. " + - "Changing the pack mid-game can result in clients rejecting the pack, connected clients having different pack, or similar. "); - } - } else { - try { - Files.write(packMetadata, Arrays.asList(String.valueOf(size), con.getHeaderField("ETag"), String.valueOf(con.getLastModified()))); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); + // If we downloaded this pack before, reuse it if the ETag matches. + if (Files.exists(packLocation) && Files.exists(packMetadata) && !force) { + try { + List metadataLines = Files.readAllLines(packMetadata, StandardCharsets.UTF_8); + int cachedSize = Integer.parseInt(metadataLines.get(0)); + String cachedEtag = metadataLines.get(1); + long cachedLastModified = Long.parseLong(metadataLines.get(2)); + + if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified()) { + logger.debug("Using cached pack for " + url); + return packLocation; } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to read cached pack metadata: " + e.getMessage()); } + } - return downloadLocation; - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Malformed URL: " + url); - } catch (SocketTimeoutException | ConnectException e) { - GeyserImpl.getInstance().getLogger().error("Unable to reach URL: " + url + " (" + e.getMessage() + ")"); - return null; - } catch (IOException e) { - throw new RuntimeException("Unable to download and save remote resource pack from: " + url + " (" + e.getMessage() + ")"); + Path downloadLocation = force ? REMOTE_PACK_CACHE.resolve(url.hashCode() + "_debug") : packLocation; + Files.copy(con.getInputStream(), downloadLocation, StandardCopyOption.REPLACE_EXISTING); + + // This needs to match as the client fails to download the pack otherwise + long downloadSize = Files.size(downloadLocation); + if (downloadSize != size) { + Files.delete(downloadLocation); + throw new IllegalStateException("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes" + .formatted(url, downloadSize, size)); } - }); + + // "Force" runs when the client rejected a pack. This is done for diagnosis of the issue. + if (force) { + // Check whether existing pack's size matches the newly downloaded packs' size + if (Files.size(packLocation) != Files.size(downloadLocation)) { + logger.error(""" + The pack size seems to have changed (%s, expected %s). If you wish to change the pack at the remote URL, restart/reload Geyser. + Changing the pack while Geyser is running can result in unexpected issues. + """.formatted(Files.size(packLocation), Files.size(downloadLocation))); + } + } else { + try { + Files.write(packMetadata, Arrays.asList(String.valueOf(size), con.getHeaderField("ETag"), String.valueOf(con.getLastModified()))); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); + } + } + + return downloadLocation; + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Unable to download resource pack from malformed URL %s! ".formatted(url)); + } catch (SocketTimeoutException | ConnectException e) { + logger.error("Unable to download pack from url %s due to network error! ( %s )".formatted(url, e.getMessage())); + logger.debug(e); + } catch (IOException e) { + throw new IllegalStateException("Unable to download and save remote resource pack from: %s ( %s )!".formatted(url, e.getMessage())); + } + return null; } From 6053b7dae866b7bd801ecdc9aacbaa6a51ea07b4 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Wed, 19 Jun 2024 23:41:58 +0200 Subject: [PATCH 26/35] Yeet unused, update optionalpack link --- .../geyser/network/UpstreamPacketHandler.java | 20 ++----------------- .../registry/loader/ResourcePackLoader.java | 5 +---- core/src/main/resources/config.yml | 4 ++-- 3 files changed, 5 insertions(+), 24 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 1f1d69136be..bf87f1c87f1 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -36,20 +36,7 @@ import org.cloudburstmc.protocol.bedrock.netty.codec.compression.CompressionStrategy; import org.cloudburstmc.protocol.bedrock.netty.codec.compression.SimpleCompressionStrategy; import org.cloudburstmc.protocol.bedrock.netty.codec.compression.ZlibCompression; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; -import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; -import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; -import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; -import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkDataPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkRequestPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackDataInfoPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; -import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; +import org.cloudburstmc.protocol.bedrock.packet.*; import org.cloudburstmc.protocol.common.PacketSignal; import org.cloudburstmc.protocol.common.util.Zlib; import org.geysermc.geyser.Constants; @@ -78,15 +65,12 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; -import java.util.Map; import java.util.OptionalInt; -import java.util.UUID; public class UpstreamPacketHandler extends LoggingPacketHandler { private boolean networkSettingsRequested = false; private final Deque packsToSent = new ArrayDeque<>(); - private final Map brokenResourcePacks = new HashMap<>(); private final CompressionStrategy compressionStrategy; private SessionLoadResourcePacksEventImpl resourcePackLoadEvent; @@ -317,7 +301,7 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packet.getPackId().toString()); PackCodec codec = pack.codec(); - // If a remote pack ends up here, that usually implies that a platform was not able to download the pack + // 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.testUrlPack(urlPackCodec); } 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 2e8fe49cfb5..50783e77f94 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 @@ -171,11 +171,8 @@ public static GeyserResourcePack readPack(Path path) throws IllegalArgumentExcep */ public static GeyserResourcePack readPack(GeyserUrlPackCodec codec) throws IllegalArgumentException { Path path = codec.getFallback().path(); - ResourcePackManifest manifest = readManifest(path, codec.url()); - String contentKey = codec.contentKey(); - - return new GeyserResourcePack(codec, manifest, contentKey); + return new GeyserResourcePack(codec, manifest, codec.contentKey()); } private static ResourcePackManifest readManifest(Path path, String packLocation) throws IllegalArgumentException { diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 13dbf39970d..eb867c31537 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -172,8 +172,8 @@ force-resource-packs: true # If you enter a link here, Geyser will download the resource pack once to check if it's in a valid format. # See https://wiki.geysermc.org/geyser/packs for more info. resource-pack-urls: - # Example: GeyserOptionalPack - - "https://ci.opencollab.dev/job/GeyserMC/job/GeyserOptionalPack/job/master/lastSuccessfulBuild/artifact/GeyserOptionalPack.mcpack" + # GeyserOptionalPack + - "https://download.geysermc.org/v2/projects/geyseroptionalpack/versions/latest/builds/latest/downloads/geyseroptionalpack" # Allows Xbox achievements to be unlocked. # THIS DISABLES ALL COMMANDS FROM SUCCESSFULLY RUNNING FOR BEDROCK IN-GAME, as otherwise Bedrock thinks you are cheating. From 2683b594069af22a53a0a953407d7c1b4bfeb82b Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 20 Jun 2024 12:05:17 +0200 Subject: [PATCH 27/35] Minor cleanup --- .../java/org/geysermc/geyser/api/pack/PackCodec.java | 2 +- .../geyser/network/UpstreamPacketHandler.java | 2 ++ .../geyser/pack/path/GeyserPathPackCodec.java | 2 +- .../geyser/registry/loader/ResourcePackLoader.java | 11 ++++++----- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index ba062535d5f..7c123b62442 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -68,7 +68,7 @@ public abstract class PackCodec { * @return the new resource pack */ @NonNull - public abstract ResourcePack create(); + protected abstract ResourcePack create(); /** * Creates a new pack provider from the given path. 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 bf87f1c87f1..ca8c6cd1115 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -235,10 +235,12 @@ public PacketSignal handle(ResourcePackClientResponsePacket packet) { stackPacket.setExperimentsPreviouslyToggled(false); stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not stackPacket.setGameVersion(session.getClientData().getGameVersion()); + for (ResourcePack pack : this.resourcePackLoadEvent.resourcePacks()) { ResourcePackManifest.Header header = pack.manifest().header(); stackPacket.getResourcePacks().add(new ResourcePackStackPacket.Entry(header.uuid().toString(), header.version().toString(), "")); } + if (GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { // Allow custom items to work stackPacket.getExperiments().add(new ExperimentData("data_driven_items", true)); diff --git a/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java b/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java index 13d07c800a2..84067600fce 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java +++ b/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java @@ -84,7 +84,7 @@ public long size() { } @Override - public @NonNull ResourcePack create() { + protected @NonNull ResourcePack create() { return ResourcePackLoader.readPack(this.path); } 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 50783e77f94..a790c3c6e64 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 @@ -212,19 +212,20 @@ private static ResourcePackManifest readManifest(Path path, String packLocation) } private Map loadRemotePacks() { + GeyserImpl instance = GeyserImpl.getInstance(); // Unable to make this a static variable, as the test would fail - final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); + final Path cachedCdnPacksDirectory = instance.getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); if (!Files.exists(cachedCdnPacksDirectory)) { try { Files.createDirectories(cachedCdnPacksDirectory); } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not create remote pack cache directory", e); + instance.getLogger().error("Could not create remote pack cache directory", e); return new Object2ObjectOpenHashMap<>(); } } - List remotePackUrls = GeyserImpl.getInstance().getConfig().getResourcePackUrls(); + List remotePackUrls = instance.getConfig().getResourcePackUrls(); Map packMap = new Object2ObjectOpenHashMap<>(); for (String url : remotePackUrls) { @@ -233,8 +234,8 @@ private Map loadRemotePacks() { ResourcePack pack = codec.create(); packMap.put(pack.manifest().header().uuid().toString(), pack); } catch (Exception e) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url)); - if (GeyserImpl.getInstance().getLogger().isDebug()) { + instance.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url)); + if (instance.getLogger().isDebug()) { e.printStackTrace(); } } From ba78dbaf349b388516bb87c962ca2f1c8c44649c Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 20 Jun 2024 12:28:08 +0200 Subject: [PATCH 28/35] Add url codec creation method with no content key, remove boolean return to registerAll, fix typo --- .../event/bedrock/SessionLoadResourcePacksEvent.java | 3 +-- .../lifecycle/GeyserDefineResourcePacksEvent.java | 3 +-- .../java/org/geysermc/geyser/api/pack/PackCodec.java | 11 +++++++++++ .../type/GeyserDefineResourcePacksEventImpl.java | 10 ++-------- .../event/type/SessionLoadResourcePacksEventImpl.java | 10 ++-------- .../geyser/network/UpstreamPacketHandler.java | 10 +++++----- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index 127a1ee8bc9..6c92628d96e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -62,9 +62,8 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { * Registers a collection of {@link ResourcePack} to be sent to clients. * * @param resourcePacks collection of resourcePack's that will be sent to clients. - * @return true if resource packs were added successfully, false if at least one failed */ - public abstract boolean registerAll(@NonNull Collection resourcePacks); + public abstract void registerAll(@NonNull Collection resourcePacks); /** * Unregisters a {@link ResourcePack} from being sent to the client. diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java index b5cb5900123..378aa417e0a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -58,9 +58,8 @@ public abstract class GeyserDefineResourcePacksEvent implements Event { * Registers a collection of {@link ResourcePack} to be sent to clients. * * @param resourcePacks collection of resourcePack's that will be sent to clients. - * @return true if resource packs were added successfully, false if at least one failed */ - public abstract boolean registerAll(@NonNull Collection resourcePacks); + public abstract void registerAll(@NonNull Collection resourcePacks); /** diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index 7c123b62442..6d298f41087 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -81,6 +81,17 @@ public static PackCodec path(@NonNull Path path) { return GeyserApi.api().provider(PathPackCodec.class, path); } + /** + * Creates a new pack provider from the given url with no content key. + * + * @param url the url to create the pack provider from + * @return the new pack provider + */ + @NonNull + public static PackCodec url(@NonNull String url) { + return url(url, null); + } + /** * Creates a new pack provider from the given url and content key. * diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java index 2775b80db65..04060d46c44 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java @@ -60,14 +60,8 @@ public boolean register(@NonNull ResourcePack resourcePack) { } @Override - public boolean registerAll(@NonNull Collection resourcePacks) { - boolean successful = true; - for (ResourcePack pack : resourcePacks) { - if (!this.register(pack)) { - successful = false; - } - } - return successful; + public void registerAll(@NonNull Collection resourcePacks) { + resourcePacks.forEach(this::register); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java index 4e71e6b13ac..505c551e512 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -61,14 +61,8 @@ public boolean register(@NonNull ResourcePack resourcePack) { } @Override - public boolean registerAll(@NonNull Collection resourcePacks) { - boolean successful = true; - for (ResourcePack pack : resourcePacks) { - if (!this.register(pack)) { - successful = false; - } - } - return successful; + public void registerAll(@NonNull Collection resourcePacks) { + resourcePacks.forEach(this::register); } @Override 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 ca8c6cd1115..db55a543aba 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -70,7 +70,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { private boolean networkSettingsRequested = false; - private final Deque packsToSent = new ArrayDeque<>(); + private final Deque packsToSend = new ArrayDeque<>(); private final CompressionStrategy compressionStrategy; private SessionLoadResourcePacksEventImpl resourcePackLoadEvent; @@ -227,8 +227,8 @@ public PacketSignal handle(ResourcePackClientResponsePacket packet) { geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.connect", session.getAuthData().name())); } case SEND_PACKS -> { - packsToSent.addAll(packet.getPackIds()); - sendPackDataInfo(packsToSent.pop()); + packsToSend.addAll(packet.getPackIds()); + sendPackDataInfo(packsToSend.pop()); } case HAVE_ALL_PACKS -> { ResourcePackStackPacket stackPacket = new ResourcePackStackPacket(); @@ -329,8 +329,8 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { session.sendUpstreamPacket(data); // Check if it is the last chunk and send next pack in queue when available. - if (remainingSize <= GeyserResourcePack.CHUNK_SIZE && !packsToSent.isEmpty()) { - sendPackDataInfo(packsToSent.pop()); + if (remainingSize <= GeyserResourcePack.CHUNK_SIZE && !packsToSend.isEmpty()) { + sendPackDataInfo(packsToSend.pop()); } return PacketSignal.HANDLED; From de54a5bde9c2a51e230e0133b4e8ec8471d6326e Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 20 Jun 2024 21:17:32 +0200 Subject: [PATCH 29/35] Ensure packs actually load --- .../java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java | 3 ++- .../geysermc/geyser/registry/loader/ResourcePackLoader.java | 2 ++ .../main/java/org/geysermc/geyser/scoreboard/Scoreboard.java | 2 +- .../java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) 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 2c561fe1ad6..f8b5ba7fb67 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 @@ -82,11 +82,12 @@ public ResourcePack create() { } else if (pack != null) { this.fallback = pack; } - }); + }).join(); // Needed to ensure that we don't attempt to read a pack before downloading/checking it } catch (Exception e) { throw new IllegalArgumentException("Failed to download pack from the url %s (reason: %s)!".formatted(url, e.getMessage())); } } + return ResourcePackLoader.readPack(this); } 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 a790c3c6e64..71f139973de 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 @@ -235,11 +235,13 @@ private Map loadRemotePacks() { packMap.put(pack.manifest().header().uuid().toString(), pack); } catch (Exception e) { instance.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url)); + instance.getLogger().error(e.getMessage()); if (instance.getLogger().isDebug()) { e.printStackTrace(); } } } + return packMap; } diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java index acce86f4d95..48a45fd27c8 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java @@ -51,7 +51,7 @@ import static org.geysermc.geyser.scoreboard.UpdateType.*; public final class Scoreboard { - private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true")); + private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "false")); private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true")); private final GeyserSession session; diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java b/core/src/main/java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java index 395eb957660..c2898578493 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java @@ -48,7 +48,7 @@ public final class ScoreboardUpdater extends Thread { static { GeyserConfiguration config = GeyserImpl.getInstance().getConfig(); FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD); - DEBUG_ENABLED = config.isDebugMode(); + DEBUG_ENABLED = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "false")) && config.isDebugMode(); } private final GeyserImpl geyser = GeyserImpl.getInstance(); From 27659d0f2b16624e00e55bbe87452ea795928ef5 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Mon, 24 Jun 2024 18:30:34 +0200 Subject: [PATCH 30/35] Add device type to debug to be able to isolate platform specific requirements --- .../org/geysermc/geyser/network/UpstreamPacketHandler.java | 2 +- .../geyser/registry/loader/ResourcePackLoader.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 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 db55a543aba..b83b512d836 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -305,7 +305,7 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // 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.testUrlPack(urlPackCodec); + ResourcePackLoader.testUrlPack(session, urlPackCodec); } data.setChunkIndex(packet.getChunkIndex()); 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 71f139973de..a0805edded1 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 @@ -42,6 +42,7 @@ import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.pack.url.GeyserUrlPackCodec; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.WebUtils; @@ -252,10 +253,12 @@ private Map loadRemotePacks() { * * @param codec the codec of the resource pack that wasn't successfully downloaded by a Bedrock client. */ - public static void testUrlPack(UrlPackCodec codec) { + public static void testUrlPack(GeyserSession session, UrlPackCodec codec) { if (CACHED_FAILED_PACKS.getIfPresent(codec.url()) == null) { CACHED_FAILED_PACKS.put(codec.url(), codec); - GeyserImpl.getInstance().getLogger().warning("A client was not able to download the resource pack at %s. Is it still available? Running check now:"); + GeyserImpl.getInstance().getLogger().warning(""" + A Bedrock client (%s, playing on %s / %s) was not able to download the resource pack at %s. Is it still available? Running check now. + """.formatted(session.bedrockUsername(), session.getClientData().getDeviceOs().name(), session.getClientData().getDeviceId(), codec.url())); downloadPack(codec.url(), true); } } From 924195722847a1a07f054332f70a8222a5c8a0ff Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 25 Jun 2024 14:14:55 +0200 Subject: [PATCH 31/35] Automatically download newer pack versions from urls, properly get rid of old packs --- .../GeyserDefineResourcePacksEventImpl.java | 2 +- .../geyser/network/UpstreamPacketHandler.java | 2 +- .../registry/loader/ResourcePackLoader.java | 128 +++++++++++++----- .../geysermc/geyser/skin/SkinProvider.java | 9 +- .../org/geysermc/geyser/util/WebUtils.java | 79 ++++++----- 5 files changed, 143 insertions(+), 77 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java index 04060d46c44..b97712ca961 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java @@ -35,9 +35,9 @@ import java.util.Map; import java.util.UUID; +@Getter public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent { - @Getter private final Map packs; public GeyserDefineResourcePacksEventImpl(Map packMap) { 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 b83b512d836..1a681d0140f 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -305,7 +305,7 @@ public PacketSignal handle(ResourcePackChunkRequestPacket packet) { // 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.testUrlPack(session, urlPackCodec); + ResourcePackLoader.testRemotePack(session, urlPackCodec, packet.getPackId().toString(), packet.getPackVersion()); } data.setChunkIndex(packet.getChunkIndex()); 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 a0805edded1..641d39ff528 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 @@ -47,6 +47,7 @@ import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.WebUtils; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; @@ -55,6 +56,7 @@ import java.nio.file.PathMatcher; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -126,10 +128,9 @@ public Map load(Path directory) { } } - // Load CDN entries packMap.putAll(loadRemotePacks()); GeyserDefineResourcePacksEventImpl defineEvent = new GeyserDefineResourcePacksEventImpl(packMap); - + GeyserImpl.getInstance().eventBus().fire(defineEvent); return defineEvent.getPacks(); } @@ -234,7 +235,7 @@ private Map loadRemotePacks() { GeyserUrlPackCodec codec = new GeyserUrlPackCodec(url); ResourcePack pack = codec.create(); packMap.put(pack.manifest().header().uuid().toString(), pack); - } catch (Exception e) { + } catch (Throwable e) { instance.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url)); instance.getLogger().error(e.getMessage()); if (instance.getLogger().isDebug()) { @@ -243,6 +244,9 @@ private Map loadRemotePacks() { } } + // After loading the new resource packs: let's clean up the old + cleanupRemotePacks(); + return packMap; } @@ -253,19 +257,76 @@ private Map loadRemotePacks() { * * @param codec the codec of the resource pack that wasn't successfully downloaded by a Bedrock client. */ - public static void testUrlPack(GeyserSession session, UrlPackCodec codec) { + public static void testRemotePack(GeyserSession session, UrlPackCodec codec, String packId, String packVersion) { if (CACHED_FAILED_PACKS.getIfPresent(codec.url()) == null) { - CACHED_FAILED_PACKS.put(codec.url(), codec); - GeyserImpl.getInstance().getLogger().warning(""" - A Bedrock client (%s, playing on %s / %s) was not able to download the resource pack at %s. Is it still available? Running check now. - """.formatted(session.bedrockUsername(), session.getClientData().getDeviceOs().name(), session.getClientData().getDeviceId(), codec.url())); - downloadPack(codec.url(), true); + String url = codec.url(); + CACHED_FAILED_PACKS.put(url, codec); + GeyserImpl.getInstance().getLogger().warning( + "A Bedrock client (%s, playing on %s / %s) was not able to download the resource pack at %s. Checking for changes now:" + .formatted(session.bedrockUsername(), session.getClientData().getDeviceOs().name(), session.getClientData().getDeviceId(), 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; + } + } + + if (pathPackCodec == null) { + return; // Already warned about + } + + ResourcePack newPack = ResourcePackLoader.readPack(pathPackCodec.path()); + UUID newUUID = newPack.manifest().header().uuid(); + if (newUUID.toString().equals(packId)) { + 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(newUUID.toString(), newPack); + + if (codec instanceof GeyserUrlPackCodec geyserUrlPackCodec && geyserUrlPackCodec.getFallback() != null) { + // Other implementations could, in theory, not have a fallback + Path path = geyserUrlPackCodec.getFallback().path(); + try { + GeyserImpl.getInstance().getScheduledThread().schedule(() -> { + deleteFile(path); + CACHED_FAILED_PACKS.invalidate(packId); + }, 5, TimeUnit.MINUTES); + } catch (RejectedExecutionException exception) { + // No scheduling here, probably because we're shutting down? + deleteFile(path); + } + } + }); + } + } + + 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<@Nullable PathPackCodec> downloadPack(String url, boolean testing) throws IllegalArgumentException { return CompletableFuture.supplyAsync(() -> { - Path path = WebUtils.checkUrlAndDownloadRemotePack(url, testing); + Path path = WebUtils.downloadRemotePack(url, testing); // Already warned about these above if (path == null) { @@ -274,7 +335,7 @@ A Bedrock client (%s, playing on %s / %s) was not able to download the resource // Check if the pack is a .zip or .mcpack file if (!PACK_MATCHER.matches(path)) { - throw new IllegalArgumentException("Invalid pack format! Not a .zip or .mcpack file."); + throw new IllegalArgumentException("Invalid pack format from url %s! Not a .zip or .mcpack file.".formatted(url)); } try { @@ -286,7 +347,7 @@ A Bedrock client (%s, playing on %s / %s) was not able to download the resource // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { - if (testing) { + if (GeyserImpl.getInstance().getLogger().isDebug()) { GeyserImpl.getInstance().getLogger().info("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + "This may not work for remote packs, and could cause Bedrock clients to fall back to request the pack from the server. " + "Please put the pack file in a subfolder, and provide that zip in the URL."); @@ -297,39 +358,34 @@ A Bedrock client (%s, playing on %s / %s) was not able to download the resource throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); } - if (testing) { - try { - Files.delete(path); - return null; - } catch (IOException e) { - throw new IllegalStateException("Could not delete debug pack! " + e.getMessage(), e); - } - } - return new GeyserPathPackCodec(path); }); } public static void clear() { Registries.RESOURCE_PACKS.get().clear(); + CACHED_FAILED_PACKS.invalidateAll(); - // Now: let's clean up broken remote packs, so we don't cache them - Path location = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); - for (UrlPackCodec codec : CACHED_FAILED_PACKS.asMap().values()) { - int hash = codec.url().hashCode(); - Path packLocation = location.resolve(hash + ".zip"); - Path packMetadata = packLocation.resolveSibling(hash + ".metadata"); + } - try { - if (packMetadata.toFile().exists()) { - Files.delete(packMetadata); - } - if (packLocation.toFile().exists()) { - Files.delete(packLocation); - } - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Could not delete broken cached resource packs! " + e); + public static void cleanupRemotePacks() { + File cacheFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").toFile(); + if (!cacheFolder.exists()) { + return; + } + + int count = 0; + final long expireTime = (((long) 1000 * 60 * 60)); // one hour + for (File imageFile : Objects.requireNonNull(cacheFolder.listFiles())) { + if (imageFile.lastModified() < System.currentTimeMillis() - expireTime) { + //noinspection ResultOfMethodCallIgnored + imageFile.delete(); + count++; } } + + if (count > 0) { + GeyserImpl.getInstance().getLogger().debug(String.format("Removed %d cached resource pack files as they are no longer in use!", count)); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java index 5b16bc3a367..f3ad0be2ffa 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -29,9 +29,6 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import it.unimi.dsi.fastutil.bytes.ByteArrays; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; @@ -56,7 +53,9 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.*; import java.util.function.Predicate; @@ -168,7 +167,7 @@ public static void registerCacheImageTask(GeyserImpl geyser) { if (count > 0) { GeyserImpl.getInstance().getLogger().debug(String.format("Removed %d cached image files as they have expired", count)); } - }, 10, 1440, TimeUnit.MINUTES); + }, 10, 1, TimeUnit.DAYS); } } 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 3cacdccd243..f3f320eb4fd 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -108,7 +108,8 @@ public static void downloadFile(String reqURL, String fileLocation) { * @param force If true, the pack will be downloaded even if it is cached to a separate location. * @return Path to the downloaded pack file, or null if it was unable to be loaded */ - public static @Nullable Path checkUrlAndDownloadRemotePack(String url, boolean force) { + @SuppressWarnings("ResultOfMethodCallIgnored") + public static @Nullable Path downloadRemotePack(String url, boolean force) { GeyserLogger logger = GeyserImpl.getInstance().getLogger(); try { HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); @@ -120,69 +121,79 @@ public static void downloadFile(String reqURL, String fileLocation) { int responseCode = con.getResponseCode(); if (responseCode >= 400) { - throw new IllegalStateException(String.format("Invalid response code from remote pack URL: %s (code: %d)", url, responseCode)); + throw new IllegalStateException(String.format("Invalid response code from remote pack at URL: %s (code: %d)", url, responseCode)); } int size = con.getContentLength(); String type = con.getContentType(); if (size <= 0) { - throw new IllegalArgumentException(String.format("Invalid size from remote pack URL: %s (size: %d)", url, size)); + throw new IllegalArgumentException(String.format("Invalid content length received from remote pack at URL: %s (size: %d)", url, size)); } - // This doesn't seem to be a requirement (anymore?). Logging to debug might be interesting though. + // This doesn't seem to be a requirement (anymore?). Logging to debug as it might be interesting though. if (type == null || !type.equals("application/zip")) { - logger.debug(String.format("Application type from remote pack URL: %s (type: %s)", url, type)); + logger.debug(String.format("Application type from remote pack at URL: %s (type: %s)", url, type)); } - Path packLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".zip"); - Path packMetadata = packLocation.resolveSibling(url.hashCode() + ".metadata"); + Path packMetadata = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".metadata"); + Path downloadLocation; // If we downloaded this pack before, reuse it if the ETag matches. - if (Files.exists(packLocation) && Files.exists(packMetadata) && !force) { + if (Files.exists(packMetadata) && !force) { try { - List metadataLines = Files.readAllLines(packMetadata, StandardCharsets.UTF_8); - int cachedSize = Integer.parseInt(metadataLines.get(0)); - String cachedEtag = metadataLines.get(1); - long cachedLastModified = Long.parseLong(metadataLines.get(2)); - - if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified()) { - logger.debug("Using cached pack for " + url); - return packLocation; + List metadata = Files.readAllLines(packMetadata, StandardCharsets.UTF_8); + int cachedSize = Integer.parseInt(metadata.get(0)); + String cachedEtag = metadata.get(1); + long cachedLastModified = Long.parseLong(metadata.get(2)); + downloadLocation = REMOTE_PACK_CACHE.resolve(metadata.get(3)); + + if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && + cachedLastModified == con.getLastModified() && downloadLocation.toFile().exists()) { + logger.info("Using cached pack (%s) for %s.".formatted(downloadLocation.getFileName(), url)); + downloadLocation.toFile().setLastModified(System.currentTimeMillis()); + packMetadata.toFile().setLastModified(System.currentTimeMillis()); + return downloadLocation; } } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Failed to read cached pack metadata: " + e.getMessage()); + GeyserImpl.getInstance().getLogger().error("Failed to read cached pack metadata! " + e); + try { + Files.delete(packMetadata); + } catch (Exception exception) { + GeyserImpl.getInstance().getLogger().error("Failed to delete pack metadata!", exception); + } } } - Path downloadLocation = force ? REMOTE_PACK_CACHE.resolve(url.hashCode() + "_debug") : packLocation; + downloadLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + "_" + System.currentTimeMillis() + ".zip"); Files.copy(con.getInputStream(), downloadLocation, StandardCopyOption.REPLACE_EXISTING); // This needs to match as the client fails to download the pack otherwise long downloadSize = Files.size(downloadLocation); if (downloadSize != size) { Files.delete(downloadLocation); - throw new IllegalStateException("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes" + throw new IllegalStateException("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes!" .formatted(url, downloadSize, size)); } - // "Force" runs when the client rejected a pack. This is done for diagnosis of the issue. - if (force) { - // Check whether existing pack's size matches the newly downloaded packs' size - if (Files.size(packLocation) != Files.size(downloadLocation)) { - logger.error(""" - The pack size seems to have changed (%s, expected %s). If you wish to change the pack at the remote URL, restart/reload Geyser. - Changing the pack while Geyser is running can result in unexpected issues. - """.formatted(Files.size(packLocation), Files.size(downloadLocation))); - } - } else { - try { - Files.write(packMetadata, Arrays.asList(String.valueOf(size), con.getHeaderField("ETag"), String.valueOf(con.getLastModified()))); - } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); - } + try { + Files.write( + packMetadata, + Arrays.asList( + String.valueOf(size), + con.getHeaderField("ETag"), + String.valueOf(con.getLastModified()), + downloadLocation.getFileName().toString() + )); + packMetadata.toFile().setLastModified(System.currentTimeMillis()); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); + Files.delete(packMetadata); + Files.delete(downloadLocation); + return null; } + downloadLocation.toFile().setLastModified(System.currentTimeMillis()); return downloadLocation; } catch (MalformedURLException e) { throw new IllegalArgumentException("Unable to download resource pack from malformed URL %s! ".formatted(url)); From 8b87c08ab21b23dd6699c1c59e7f6a0c670bc1ea Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 25 Jun 2024 14:46:11 +0200 Subject: [PATCH 32/35] Fix typo's --- core/src/main/java/org/geysermc/geyser/util/WebUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f3f320eb4fd..d91f611f6f9 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -150,7 +150,7 @@ public static void downloadFile(String reqURL, String fileLocation) { if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified() && downloadLocation.toFile().exists()) { - logger.info("Using cached pack (%s) for %s.".formatted(downloadLocation.getFileName(), url)); + logger.debug("Using cached pack (%s) for %s.".formatted(downloadLocation.getFileName(), url)); downloadLocation.toFile().setLastModified(System.currentTimeMillis()); packMetadata.toFile().setLastModified(System.currentTimeMillis()); return downloadLocation; From e214fbe277f1ad1faaa3c0d36516fccdc6fa53a2 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 27 Jun 2024 17:26:49 +0200 Subject: [PATCH 33/35] javadoc typos, log application type as info for now --- .../api/event/bedrock/SessionLoadResourcePacksEvent.java | 6 +++--- .../api/event/lifecycle/GeyserDefineResourcePacksEvent.java | 6 +++--- core/src/main/java/org/geysermc/geyser/util/WebUtils.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index 6c92628d96e..568626b4cb6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -43,7 +43,7 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { } /** - * Gets an unmodifiable list of {@link ResourcePack}s that will be sent to the client. + * Gets an unmodifiable list of {@link ResourcePack}'s that will be sent to the client. * * @return an unmodifiable list of resource packs that will be sent to the client. */ @@ -59,9 +59,9 @@ public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { public abstract boolean register(@NonNull ResourcePack resourcePack); /** - * Registers a collection of {@link ResourcePack} to be sent to clients. + * Registers a collection of {@link ResourcePack}'s to be sent to clients. * - * @param resourcePacks collection of resourcePack's that will be sent to clients. + * @param resourcePacks collection of resource pack's that will be sent to clients */ public abstract void registerAll(@NonNull Collection resourcePacks); diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java index 378aa417e0a..2613f8c5b06 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -39,7 +39,7 @@ public abstract class GeyserDefineResourcePacksEvent implements Event { /** - * Gets an unmodifiable list of {@link ResourcePack}s that will be sent to clients. + * Gets an unmodifiable list of {@link ResourcePack}'s that will be sent to clients. * * @return an unmodifiable list of resource packs that will be sent to clients. */ @@ -55,9 +55,9 @@ public abstract class GeyserDefineResourcePacksEvent implements Event { public abstract boolean register(@NonNull ResourcePack resourcePack); /** - * Registers a collection of {@link ResourcePack} to be sent to clients. + * Registers a collection of {@link ResourcePack}'s to be sent to clients. * - * @param resourcePacks collection of resourcePack's that will be sent to clients. + * @param resourcePacks a collection of resource pack's that will be sent to clients. */ public abstract void registerAll(@NonNull Collection resourcePacks); 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 d91f611f6f9..6449ae73448 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -133,7 +133,7 @@ public static void downloadFile(String reqURL, String fileLocation) { // This doesn't seem to be a requirement (anymore?). Logging to debug as it might be interesting though. if (type == null || !type.equals("application/zip")) { - logger.debug(String.format("Application type from remote pack at URL: %s (type: %s)", url, type)); + logger.info(String.format("Application type from remote pack at URL: %s (type: %s)", url, type)); } Path packMetadata = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".metadata"); From 1608746afdcbabd920b196b881f6e52e589c0972 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Mon, 1 Jul 2024 14:19:21 +0200 Subject: [PATCH 34/35] Warn about content-type being null/not application/zip --- core/src/main/java/org/geysermc/geyser/util/WebUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 6449ae73448..50ff0ede2a1 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -133,7 +133,8 @@ public static void downloadFile(String reqURL, String fileLocation) { // This doesn't seem to be a requirement (anymore?). Logging to debug as it might be interesting though. if (type == null || !type.equals("application/zip")) { - logger.info(String.format("Application type from remote pack at URL: %s (type: %s)", url, type)); + logger.warning(String.format("Application type received from remote pack at URL %s uses the content type: %s! This may result in packs not loading " + + "for Bedrock players.", url, type)); } Path packMetadata = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".metadata"); From f1163828f814ee4c55df988d657d62285924f1fe Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 4 Jul 2024 00:42:11 +0200 Subject: [PATCH 35/35] Proper warning about version/uuid changes --- .../geyser/registry/loader/ResourcePackLoader.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 641d39ff528..9662f6f0bc7 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 @@ -285,8 +285,12 @@ public static void testRemotePack(GeyserSession session, UrlPackCodec codec, Str ResourcePack newPack = ResourcePackLoader.readPack(pathPackCodec.path()); UUID newUUID = newPack.manifest().header().uuid(); if (newUUID.toString().equals(packId)) { - GeyserImpl.getInstance().getLogger().info("Detected a new resource pack version (%s, old version %s) for pack at %s!" + 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)); } @@ -301,8 +305,8 @@ public static void testRemotePack(GeyserSession session, UrlPackCodec codec, Str Path path = geyserUrlPackCodec.getFallback().path(); try { GeyserImpl.getInstance().getScheduledThread().schedule(() -> { - deleteFile(path); CACHED_FAILED_PACKS.invalidate(packId); + deleteFile(path); }, 5, TimeUnit.MINUTES); } catch (RejectedExecutionException exception) { // No scheduling here, probably because we're shutting down?