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 c2f1cd427..2fa9582d7 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.checkerframework.checker.nullness.qual.NonNull; 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 abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { */ 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 abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { 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 000000000..81fa56bab --- /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 e36ec819b..1b3b89595 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.GeyserLogger; 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 268304844..bee041196 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.Getter; 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 abstract class GeyserJacksonConfiguration implements GeyserConfiguration 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 5ed0f8d22..b82f7b98d 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 @@ package org.geysermc.geyser.event.type; 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 class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE return List.copyOf(packs.values()); } + @Override + public @NonNull List 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 fb9684f77..c960b788f 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 final class GameProtocol { 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 361aaffb9..2f5961c39 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.codec.BedrockCodec; 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 class UpstreamPacketHandler extends LoggingPacketHandler { 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 class UpstreamPacketHandler extends LoggingPacketHandler { 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 8e4db5e38..c088fc285 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