diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java index a92acccb3..1bec9af29 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java @@ -27,6 +27,9 @@ package org.geysermc.geyser.api.pack; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; + +import java.util.Collection; /** * Represents a resource pack sent to Bedrock clients @@ -63,12 +66,15 @@ public interface ResourcePack { String contentKey(); /** - * The default subpack to tell Bedrock clients to load. Lack of a subpack to load is represented by an empty string. + * Gets the currently set default options of this resource pack. + * These can be a priority defining how the Bedrock client applies multiple packs, + * or a default subpack. + *

+ * These can be overridden in the {@link org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent} * - * @return the subpack name, or an empty string if not set. + * @return a collection of default {@link ResourcePackOption}s */ - @NonNull - String defaultSubpackName(); + Collection defaultOptions(); /** * Creates a resource pack with the given {@link PackCodec}. @@ -98,18 +104,44 @@ public interface ResourcePack { */ interface Builder { + /** + * @return the {@link ResourcePackManifest} of this resource pack + */ ResourcePackManifest manifest(); + /** + * @return the {@link PackCodec} of this resource pack + */ PackCodec codec(); + /** + * @return the current content key, or an empty string if not set + */ String contentKey(); - String defaultSubpackName(); - + /** + * Sets a content key for this resource pack. + * + * @param contentKey the content key + * @return this builder + */ Builder contentKey(@NonNull String contentKey); - Builder defaultSubpackName(@NonNull String subpackName); + /** + * @return the current default {@link ResourcePackOption}s + */ + Collection defaultOptions(); + /** + * Sets default options for this resource pack. + * + * @return this builder + */ + Builder defaultOptions(ResourcePackOption... defaultOptions); + + /** + * @return the resource pack + */ ResourcePack build(); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java index ae08cee76..0287dc26a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java @@ -29,7 +29,8 @@ import org.geysermc.geyser.api.GeyserApi; /** * Allows specifying a pack priority that decides the order on how packs are sent to the client. - * Multiple resource packs can override each other. The higher the priority, the + * Multiple resource packs can override each other. The higher the priority, the "higher" in the stack + * a pack is, and the more a pack can override other packs. */ public interface PriorityOption extends ResourcePackOption { @@ -37,8 +38,19 @@ public interface PriorityOption extends ResourcePackOption { PriorityOption NORMAL = PriorityOption.priority(5); PriorityOption LOW = PriorityOption.priority(0); + /** + * The priority of the resource pack + * + * @return priority + */ int priority(); + /** + * Constructs a priority option based on a value between 0 and 10 + * + * @param priority an integer that is above 0, but smaller than 10 + * @return the priority option + */ static PriorityOption priority(int priority) { if (priority < 0 || priority > 10) { throw new IllegalArgumentException("Priority must be between 0 and 10 inclusive!"); 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 917544b4c..40e142f79 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 @@ -26,15 +26,21 @@ package org.geysermc.geyser.event.type; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; import org.geysermc.geyser.api.pack.option.PriorityOption; import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import org.geysermc.geyser.api.pack.option.SubpackOption; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; +import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -42,28 +48,72 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent { + @Getter private final Map packs; - private final Map> options; + private final Map> options = new HashMap<>(); public SessionLoadResourcePacksEventImpl(GeyserSession session) { super(session); this.packs = new Object2ObjectLinkedOpenHashMap<>(Registries.RESOURCE_PACKS.get()); - this.options = new HashMap<>(); - } - - public @NonNull Map getPacks() { - return packs; + this.packs.values().forEach( + pack -> options.put(pack.manifest().header().uuid().toString(), pack.defaultOptions()) + ); } public LinkedList orderedPacks() { - // TODO sort by priority here - - return new LinkedList<>(); + return packs.values().stream() + // Map each ResourcePack to a pair of (ResourcePack, Priority) + .map(pack -> new AbstractMap.SimpleEntry<>(pack, getPriority(pack))) + // Sort by priority in descending order (higher priority first) + .sorted((entry1, entry2) -> Integer.compare(entry2.getValue(), entry1.getValue())) + // Extract the ResourcePack from the sorted entries + .map(entry -> { + ResourcePack pack = entry.getKey(); + ResourcePackManifest.Header header = pack.manifest().header(); + return new ResourcePackStackPacket.Entry(header.uuid().toString(), header.version().toString(), getSubpackName(header.uuid())); + }) + // Collect to a LinkedList + .collect(Collectors.toCollection(LinkedList::new)); } + // Helper method to get the priority of a ResourcePack + private int getPriority(ResourcePack pack) { + return options.get(pack.manifest().header().uuid().toString()).stream() + // Filter to find the PriorityOption + .filter(option -> option instanceof PriorityOption) + // Map to the priority value + .mapToInt(option -> ((PriorityOption) option).priority()) + // Get the highest priority (or a default value, if none found) + .max().orElse(PriorityOption.NORMAL.priority()); + } + + public List infoPacketEntries() { + List entries = new ArrayList<>(); + + for (ResourcePack pack : packs.values()) { + ResourcePackManifest.Header header = pack.manifest().header(); + entries.add(new ResourcePacksInfoPacket.Entry( + header.uuid().toString(), header.version().toString(), pack.codec().size(), pack.contentKey(), + getSubpackName(header.uuid()), header.uuid().toString(), false, false) + ); + } + + return entries; + } + + private String getSubpackName(UUID uuid) { + return options.get(uuid.toString()).stream() + .filter(option -> option instanceof SubpackOption) + .map(option -> ((SubpackOption) option).subpackName()) + .findFirst() + .orElse(""); // Return an empty string if none is found + } + + @Override public @NonNull List resourcePacks() { return List.copyOf(packs.values()); @@ -92,9 +142,8 @@ public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksE } @Override - public Collection options(UUID resourcePack) { - Collection packOptions = options.get(resourcePack.toString()); - return packOptions == null ? List.of() : Collections.unmodifiableCollection(packOptions); + public Collection options(UUID uuid) { + return Collections.unmodifiableCollection(options.get(uuid.toString())); } @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 43a4e414a..fde7e5d70 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -203,12 +203,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { this.geyser.eventBus().fire(this.resourcePackLoadEvent); ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); - for (ResourcePack pack : this.resourcePackLoadEvent.resourcePacks()) { - ResourcePackManifest.Header header = pack.manifest().header(); - resourcePacksInfo.getResourcePackInfos().add(new ResourcePacksInfoPacket.Entry( - header.uuid().toString(), header.version().toString(), pack.codec().size(), pack.contentKey(), - pack.defaultSubpackName(), header.uuid().toString(), false, false)); - } + resourcePacksInfo.getResourcePackInfos().addAll(this.resourcePackLoadEvent.infoPacketEntries()); resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); session.sendUpstreamPacket(resourcePacksInfo); @@ -235,7 +230,6 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { break; case HAVE_ALL_PACKS: - // TODO apply pack order here ResourcePackStackPacket stackPacket = new ResourcePackStackPacket(); stackPacket.setExperimentsPreviouslyToggled(false); stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not 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 b0464c71f..8f209ff66 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java +++ b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java @@ -25,16 +25,25 @@ package org.geysermc.geyser.pack; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.api.pack.option.PriorityOption; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; public record GeyserResourcePack( PackCodec codec, ResourcePackManifest manifest, String contentKey, - String defaultSubpackName + Collection defaultOptions ) implements ResourcePack { /** @@ -43,10 +52,9 @@ public record GeyserResourcePack( public static final int CHUNK_SIZE = 102400; public GeyserResourcePack(PackCodec codec, ResourcePackManifest manifest, String contentKey) { - this(codec, manifest, contentKey, ""); + this(codec, manifest, contentKey, new ArrayList<>(List.of(PriorityOption.NORMAL))); } - public static class Builder implements ResourcePack.Builder { public Builder(PackCodec codec, ResourcePackManifest manifest) { @@ -62,8 +70,8 @@ public record GeyserResourcePack( private final PackCodec codec; private final ResourcePackManifest manifest; - private String contentKey; - private String defaultSubpackName; + private String contentKey = ""; + private final Collection defaultOptions = new ArrayList<>(List.of(PriorityOption.NORMAL)); @Override public ResourcePackManifest manifest() { @@ -77,38 +85,29 @@ public record GeyserResourcePack( @Override public String contentKey() { - return this.contentKey == null ? "" : this.contentKey; + return contentKey; } @Override - public String defaultSubpackName() { - return this.defaultSubpackName == null ? "" : this.defaultSubpackName; + public Collection defaultOptions() { + return Collections.unmodifiableCollection(defaultOptions); } - public Builder contentKey(@Nullable String contentKey) { + public Builder contentKey(@NonNull String contentKey) { + Objects.requireNonNull(contentKey); this.contentKey = contentKey; return this; } - public Builder defaultSubpackName(@Nullable String subpackName) { - if (manifest.subpacks().stream().anyMatch(subpack -> subpack.name().equals(subpackName))) { - this.defaultSubpackName = subpackName; - } else { - throw new IllegalArgumentException("A subpack with the name '" + subpackName + "' does not exist!"); - } + public Builder defaultOptions(ResourcePackOption... defaultOptions) { + this.defaultOptions.addAll(Arrays.stream(defaultOptions).toList()); return this; } public GeyserResourcePack build() { - if (contentKey == null) { - contentKey = ""; - } - if (defaultSubpackName == null) { - defaultSubpackName = ""; - } - - return new GeyserResourcePack(codec, manifest, contentKey, defaultSubpackName); + GeyserResourcePack pack = new GeyserResourcePack(codec, manifest, contentKey, defaultOptions); + defaultOptions.forEach(option -> option.validate(pack)); + return pack; } } - }