From 858a882778779b9c2cd864a03ebff344b50d4701 Mon Sep 17 00:00:00 2001 From: realpeyaj Date: Thu, 24 Sep 2026 21:36:53 +0800 Subject: [PATCH] Support heterogeneous lists in GeyserHolderSet --- .../geyser/session/cache/TagCache.java | 2 +- .../session/cache/tags/GeyserHolderSet.java | 105 ++++++++-- .../cache/tags/GeyserHolderSetTest.java | 191 ++++++++++++++++++ 3 files changed, 284 insertions(+), 14 deletions(-) create mode 100644 core/src/test/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSetTest.java diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java index c0ae71c52df..f2422e33a8a 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java @@ -138,7 +138,7 @@ public boolean is(@NonNull GeyserHolderSet holderSet, @Nullable T object) if (object == null) { return false; } - return holderSet.resolveRaw(this).contains(holderSet.getRegistry().networkId(session, object)); + return holderSet.contains(session, object); } /** diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java b/core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java index 55b8ee14ba0..0c4e42c9d61 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java @@ -39,6 +39,7 @@ import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Function; @@ -64,28 +65,46 @@ public final class GeyserHolderSet { private final @Nullable Tag tag; private final @Nullable IntList holders; private final @Nullable List inline; + private final @Nullable List> entries; + + public sealed interface HolderEntry permits HolderEntry.Direct, HolderEntry.Id { + record Direct(T value) implements HolderEntry {} + record Id(int id) implements HolderEntry {} + } private GeyserHolderSet(JavaRegistryKey registry) { this(registry, IntLists.emptyList()); } public GeyserHolderSet(JavaRegistryKey registry, @Nullable IntList holders) { - this(registry, null, holders, null); + this(registry, null, holders, null, null); } public GeyserHolderSet(JavaRegistryKey registry, @NonNull Tag tagId) { - this(registry, tagId, null, null); + this(registry, tagId, null, null, null); } public GeyserHolderSet(JavaRegistryKey registry, @NonNull List inline) { - this(registry, null, null, inline); + this(registry, null, null, inline, null); } private GeyserHolderSet(JavaRegistryKey registry, @Nullable Tag tag, @Nullable IntList holders, @Nullable List inline) { + this(registry, tag, holders, inline, null); + } + + private GeyserHolderSet(JavaRegistryKey registry, @Nullable Tag tag, @Nullable IntList holders, @Nullable List inline, @Nullable List> entries) { this.registry = registry; this.tag = tag; this.holders = holders; this.inline = inline; + this.entries = entries; + } + + /** + * Constructs a {@link GeyserHolderSet} from an ordered list of {@link HolderEntry} elements. + */ + public static GeyserHolderSet ofEntries(JavaRegistryKey registry, @NonNull List> entries) { + return new GeyserHolderSet<>(registry, null, null, null, entries); } /** @@ -101,19 +120,38 @@ public static GeyserHolderSet empty(JavaRegistryKey registry) { public static GeyserHolderSet fromHolderSet(JavaRegistryKey registry, @NonNull HolderSet holderSet) { // MCPL HolderSets don't have to support inline elements... for now (TODO CHECK ME) Tag tag = holderSet.getLocation() == null ? null : new Tag<>(registry, holderSet.getLocation()); - return new GeyserHolderSet<>(registry, tag, holderSet.getHolders(), null); + return new GeyserHolderSet<>(registry, tag, holderSet.getHolders(), null, null); } public boolean contains(@NonNull GeyserSession session, @Nullable T object) { if (object == null) { return false; } + if (inline != null) { + return inline.contains(object); + } + if (entries != null) { + int id = registry.networkId(session, object); + for (HolderEntry entry : entries) { + if (entry instanceof HolderEntry.Direct direct) { + if (Objects.equals(direct.value(), object)) { + return true; + } + } else if (entry instanceof HolderEntry.Id idEntry) { + if (id != -1 && idEntry.id() == id) { + return true; + } + } + } + return false; + } return session.getTagCache().is(this, object); } /** * Resolves the HolderSet, and automatically maps the network IDs to their respective object types. * If the HolderSet is a list of IDs, this will be returned. If it is a tag, the tag will be resolved from the tag cache. If it is an inline HolderSet, the list of inline elements will be returned. + * If it is a heterogeneous HolderSet, the inline elements and resolved registered objects are returned in order. * * @return the HolderSet turned into a list of objects. */ @@ -121,19 +159,33 @@ public List resolve(GeyserSession session) { if (inline != null) { return inline; } + if (entries != null) { + List result = new ArrayList<>(entries.size()); + for (HolderEntry entry : entries) { + if (entry instanceof HolderEntry.Direct direct) { + result.add(direct.value()); + } else if (entry instanceof HolderEntry.Id idEntry) { + T value = registry.value(session, idEntry.id()); + if (value != null) { + result.add(value); + } + } + } + return result; + } return TagCache.mapRawArray(session, resolveRaw(session.getTagCache()), registry); } /** * Resolves the HolderSet into a list of network IDs. If the HolderSet is a list of IDs, this will be returned. If it is a tag, the tag will be resolved from the tag cache. * - *

If the HolderSet is a list of inline elements, this method will throw! Inline elements are not registered and as such do not have a network ID.

+ *

If the HolderSet has inline elements, this method will throw! Inline elements are not registered and as such do not have a network ID.

* * @return the HolderSet turned into a list of network IDs. - * @throws IllegalStateException when the HolderSet is a list of inline elements. + * @throws IllegalStateException when the HolderSet contains inline elements. */ public IntList resolveRaw(TagCache tagCache) { - if (inline != null) { + if (inline != null || entries != null) { throw new IllegalStateException("Tried to resolve network IDs of a GeyserHolderSet(registry=" + registry + ") with inline elements!"); } else if (holders != null) { return holders; @@ -185,7 +237,8 @@ public static GeyserHolderSet readHolderSet(JavaRegistryKey registry, // so it works. If this ever changes, we'll have to accommodate for that here if (holderSet instanceof NbtMap singleInlineElement && reader != null) { return new GeyserHolderSet<>(registry, List.of(reader.apply(singleInlineElement))); - } if (holderSet instanceof String elementOrTag) { + } + if (holderSet instanceof String elementOrTag) { if (elementOrTag.startsWith("#")) { // Tag return new GeyserHolderSet<>(registry, new Tag<>(registry, MinecraftKey.key(elementOrTag.substring(1)))); // Remove '#' at beginning that indicates a tag @@ -196,19 +249,45 @@ public static GeyserHolderSet readHolderSet(JavaRegistryKey registry, } else if (holderSet instanceof List list) { if (list.isEmpty()) { return new GeyserHolderSet<>(registry); - } else if (list.getFirst() instanceof NbtMap) { + } + + boolean hasInline = false; + boolean hasReference = false; + for (Object element : list) { + if (element instanceof NbtMap) { + hasInline = true; + } else if (element instanceof String) { + hasReference = true; + } + } + + if (hasInline && !hasReference) { if (reader != null) { return new GeyserHolderSet<>(registry, list.stream().map(o -> (NbtMap) o).map(reader).toList()); } - } else { + } else if (hasReference && !hasInline) { // Assume the list is a list of strings (resource locations) - return new GeyserHolderSet<>(registry, IntList.of(list.stream().map(o -> (String) o).map(Key::key).mapToInt(idMapper).toArray())); + return new GeyserHolderSet<>(registry, IntList.of(list.stream().map(o -> (String) o).map(MinecraftKey::key).mapToInt(idMapper).toArray())); + } else if (hasInline && hasReference) { + if (reader != null) { + List> entries = new ArrayList<>(list.size()); + for (Object element : list) { + if (element instanceof NbtMap map) { + entries.add(new HolderEntry.Direct<>(reader.apply(map))); + } else if (element instanceof String str) { + entries.add(new HolderEntry.Id<>(idMapper.applyAsInt(MinecraftKey.key(str)))); + } else { + GeyserImpl.getInstance().getLogger().warning("Unexpected element type in mixed HolderSet for registry " + registry + ": " + element); + } + } + return ofEntries(registry, entries); + } } } String expected = reader == null ? "either a tag, a string ID, or a list of string IDs" - : "either a tag, a string ID, an inline registry element, a list of string IDs, or a list of inline registry elements"; - GeyserImpl.getInstance().getLogger().warning("Failed parsing HolderSet for registry + " + registry + "! Expected " + expected + ", found " + holderSet); + : "either a tag, a string ID, an inline registry element, a list of string IDs, a list of inline registry elements, or a heterogeneous list of both"; + GeyserImpl.getInstance().getLogger().warning("Failed parsing HolderSet for registry " + registry + "! Expected " + expected + ", found " + holderSet); return new GeyserHolderSet<>(registry); } } diff --git a/core/src/test/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSetTest.java b/core/src/test/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSetTest.java new file mode 100644 index 00000000000..a03eae01001 --- /dev/null +++ b/core/src/test/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSetTest.java @@ -0,0 +1,191 @@ +package org.geysermc.geyser.session.cache.tags; + +import it.unimi.dsi.fastutil.ints.IntList; +import net.kyori.adventure.key.Key; +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.scoreboard.network.util.GeyserMockContext; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.RegistryCache; +import org.geysermc.geyser.session.cache.TagCache; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.registry.JavaRegistryProvider; +import org.geysermc.geyser.session.cache.registry.RegistryEntryData; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +class GeyserHolderSetTest { + + private void runWithContext(TestRunner runner) { + GeyserMockContext.mockContext(context -> { + GeyserSession session = context.mock(GeyserSession.class); + RegistryCache registryCache = context.mock(RegistryCache.class); + TagCache tagCache = context.mock(TagCache.class); + when(session.getRegistryCache()).thenReturn(registryCache); + when(session.getTagCache()).thenReturn(tagCache); + + JavaRegistryKey.RegistryLookup lookup = new JavaRegistryKey.RegistryLookup<>() { + @Override + public Optional> entry(JavaRegistryProvider registries, JavaRegistryKey registry, int networkId) { + if (networkId == 100) { + return Optional.of(new RegistryEntryData<>(100, Key.key("minecraft:dialog_ref"), "ResolvedDialog")); + } + return Optional.empty(); + } + + @Override + public Optional> entry(JavaRegistryProvider registries, JavaRegistryKey registry, Key key) { + if (key.equals(Key.key("minecraft:dialog_ref"))) { + return Optional.of(new RegistryEntryData<>(100, Key.key("minecraft:dialog_ref"), "ResolvedDialog")); + } + return Optional.empty(); + } + + @Override + public Optional> entry(JavaRegistryProvider registries, JavaRegistryKey registry, String object) { + if ("ResolvedDialog".equals(object)) { + return Optional.of(new RegistryEntryData<>(100, Key.key("minecraft:dialog_ref"), "ResolvedDialog")); + } + return Optional.empty(); + } + }; + + JavaRegistryKey registryKey = new JavaRegistryKey<>(Key.key("minecraft:dialog"), lookup); + runner.run(session, registryKey, tagCache); + }); + } + + @FunctionalInterface + interface TestRunner { + void run(GeyserSession session, JavaRegistryKey registryKey, TagCache tagCache); + } + + @Test + void testEmptyHolderSet() { + runWithContext((session, registryKey, tagCache) -> { + GeyserHolderSet set = GeyserHolderSet.readHolderSet(registryKey, null, key -> -1, null); + assertNotNull(set); + assertNull(set.getTag()); + assertEquals(0, set.getHolders().size()); + assertNull(set.getInline()); + assertNull(set.getEntries()); + assertTrue(set.resolve(session).isEmpty()); + }); + } + + @Test + void testTagHolderSet() { + runWithContext((session, registryKey, tagCache) -> { + GeyserHolderSet set = GeyserHolderSet.readHolderSet(registryKey, "#minecraft:all_dialogs", key -> -1, null); + assertNotNull(set.getTag()); + assertEquals("minecraft", set.getTag().tag().namespace()); + assertEquals("all_dialogs", set.getTag().tag().value()); + assertNull(set.getHolders()); + assertNull(set.getInline()); + assertNull(set.getEntries()); + }); + } + + @Test + void testSingleStringHolderSet() { + runWithContext((session, registryKey, tagCache) -> { + ToIntFunction idMapper = key -> key.asString().equals("minecraft:dialog_ref") ? 100 : -1; + GeyserHolderSet set = GeyserHolderSet.readHolderSet(registryKey, "minecraft:dialog_ref", idMapper, null); + assertNull(set.getTag()); + assertNotNull(set.getHolders()); + assertEquals(IntList.of(100), set.getHolders()); + assertNull(set.getInline()); + assertNull(set.getEntries()); + assertEquals(List.of("ResolvedDialog"), set.resolve(session)); + }); + } + + @Test + void testSingleInlineHolderSet() { + runWithContext((session, registryKey, tagCache) -> { + NbtMap inlineNbt = NbtMap.builder().putString("title", "Inline 1").build(); + Function reader = map -> map.getString("title"); + + GeyserHolderSet set = GeyserHolderSet.readHolderSet(registryKey, inlineNbt, key -> -1, reader); + assertNull(set.getTag()); + assertNull(set.getHolders()); + assertNotNull(set.getInline()); + assertEquals(List.of("Inline 1"), set.getInline()); + assertNull(set.getEntries()); + assertEquals(List.of("Inline 1"), set.resolve(session)); + assertThrows(IllegalStateException.class, () -> set.resolveRaw(tagCache)); + }); + } + + @Test + void testHomogeneousStringListHolderSet() { + runWithContext((session, registryKey, tagCache) -> { + ToIntFunction idMapper = key -> 100; + GeyserHolderSet set = GeyserHolderSet.readHolderSet(registryKey, List.of("minecraft:dialog_ref"), idMapper, null); + assertNotNull(set.getHolders()); + assertEquals(IntList.of(100), set.getHolders()); + assertNull(set.getInline()); + assertNull(set.getEntries()); + assertEquals(List.of("ResolvedDialog"), set.resolve(session)); + }); + } + + @Test + void testHomogeneousInlineListHolderSet() { + runWithContext((session, registryKey, tagCache) -> { + NbtMap nbt1 = NbtMap.builder().putString("title", "Inline 1").build(); + NbtMap nbt2 = NbtMap.builder().putString("title", "Inline 2").build(); + Function reader = map -> map.getString("title"); + + GeyserHolderSet set = GeyserHolderSet.readHolderSet(registryKey, List.of(nbt1, nbt2), key -> -1, reader); + assertNull(set.getHolders()); + assertNotNull(set.getInline()); + assertEquals(List.of("Inline 1", "Inline 2"), set.getInline()); + assertNull(set.getEntries()); + assertEquals(List.of("Inline 1", "Inline 2"), set.resolve(session)); + }); + } + + @Test + void testHeterogeneousListHolderSet() { + runWithContext((session, registryKey, tagCache) -> { + NbtMap inline1 = NbtMap.builder().putString("title", "Inline First").build(); + String ref = "minecraft:dialog_ref"; + NbtMap inline2 = NbtMap.builder().putString("title", "Inline Last").build(); + + ToIntFunction idMapper = key -> key.asString().equals("minecraft:dialog_ref") ? 100 : -1; + Function reader = map -> map.getString("title"); + + List mixedList = List.of(inline1, ref, inline2); + GeyserHolderSet set = GeyserHolderSet.readHolderSet(registryKey, mixedList, idMapper, reader); + + assertNull(set.getHolders()); + assertNull(set.getInline()); + assertNotNull(set.getEntries()); + assertEquals(3, set.getEntries().size()); + + assertInstanceOf(GeyserHolderSet.HolderEntry.Direct.class, set.getEntries().get(0)); + assertInstanceOf(GeyserHolderSet.HolderEntry.Id.class, set.getEntries().get(1)); + assertInstanceOf(GeyserHolderSet.HolderEntry.Direct.class, set.getEntries().get(2)); + + // Test resolution preserves exact sequence + List resolved = set.resolve(session); + assertEquals(List.of("Inline First", "ResolvedDialog", "Inline Last"), resolved); + + // Test resolveRaw throws + assertThrows(IllegalStateException.class, () -> set.resolveRaw(tagCache)); + + // Test contains + assertTrue(set.contains(session, "Inline First")); + assertTrue(set.contains(session, "ResolvedDialog")); + assertTrue(set.contains(session, "Inline Last")); + assertFalse(set.contains(session, "NonExistentDialog")); + }); + } +}