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