Skip to content

feat(nbt): Initial implementation of heterogeneous list handling #1218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion nbt/src/main/java/net/kyori/adventure/nbt/BinaryTagTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ public final class BinaryTagTypes {
}
return ListBinaryTag.listBinaryTag(type, tags);
}
}, (tag, output) -> {
}, (rawTag, output) -> {
final ListBinaryTag tag = rawTag.wrapHeterogeneity();
output.writeByte(tag.elementType().id());
final int size = tag.size();
output.writeInt(size);
Expand Down Expand Up @@ -203,6 +204,18 @@ public final class BinaryTagTypes {
output.writeLong(value[i]);
}
});
/**
* Synthetic tag type used as a list's element type to indicate it contains elements of multiple types.
*
* <p>This tag type cannot be read or written. List tag serialization will auto-box lists with this element type.</p>
*
* @since 4.21.0
*/
public static final BinaryTagType<BinaryTag> LIST_WILDCARD = new BinaryTagType.Impl<>(BinaryTag.class, Byte.MAX_VALUE, input -> {
throw new IllegalArgumentException("Unable to read values of placeholder type. This tag type exists only to indicate heterogeneous lists");
}, (tag, output) -> {
throw new IllegalArgumentException("Unable to write values of placeholder type. This tag type exists only to indicate heterogeneous lists");
});

private BinaryTagTypes() {
}
Expand Down
35 changes: 31 additions & 4 deletions nbt/src/main/java/net/kyori/adventure/nbt/ListBinaryTag.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,17 @@ public interface ListBinaryTag extends ListTagSetter<ListBinaryTag, BinaryTag>,
* @since 4.0.0
*/
static @NotNull Builder<BinaryTag> builder() {
return new ListTagBuilder<>();
return new ListTagBuilder<>(false);
}

/**
* Creates a builder that can accept elements of multiple types.
*
* @return a new builder
* @since 4.21.0
*/
static @NotNull Builder<BinaryTag> heterogeneousListBinaryTag() {
return new ListTagBuilder<>(true);
}

/**
Expand All @@ -83,7 +93,7 @@ public interface ListBinaryTag extends ListTagSetter<ListBinaryTag, BinaryTag>,
*/
static <T extends BinaryTag> @NotNull Builder<T> builder(final @NotNull BinaryTagType<T> type) {
if (type == BinaryTagTypes.END) throw new IllegalArgumentException("Cannot create a list of " + BinaryTagTypes.END);
return new ListTagBuilder<>(type);
return new ListTagBuilder<>(false, type);
}

/**
Expand All @@ -94,13 +104,14 @@ public interface ListBinaryTag extends ListTagSetter<ListBinaryTag, BinaryTag>,
* @param type the element type
* @param tags the elements
* @return a tag
* @throws IllegalArgumentException if {@code type} is {@link BinaryTagTypes#END}
* @throws IllegalArgumentException if {@code type} is {@link BinaryTagTypes#END}, or if elements are of different types
* @since 4.14.0
*/
static @NotNull ListBinaryTag listBinaryTag(final @NotNull BinaryTagType<? extends BinaryTag> type, final @NotNull List<BinaryTag> tags) {
if (tags.isEmpty()) return empty();
if (type == BinaryTagTypes.END) throw new IllegalArgumentException("Cannot create a list of " + BinaryTagTypes.END);
return new ListBinaryTagImpl(type, new ArrayList<>(tags)); // explicitly copy
ListBinaryTagImpl.validateTagType(tags, type == BinaryTagTypes.LIST_WILDCARD);
return new ListBinaryTagImpl(type, type == BinaryTagTypes.LIST_WILDCARD, new ArrayList<>(tags)); // explicitly copy
}

/**
Expand Down Expand Up @@ -567,6 +578,22 @@ default double getDouble(final @Range(from = 0, to = Integer.MAX_VALUE) int inde
*/
@NotNull Stream<BinaryTag> stream();

/**
* Unwrap any compound-boxed heterogeneous values in this tag.
*
* @return a list tag that permits heterogeneity
* @since 4.21.0
*/
@NotNull ListBinaryTag unwrapHeterogeneity();

/**
* Wrap any heterogeneous values in this tag into compound-tag boxes.
*
* @return a list tag that does not permit heterogeneity, with any heterogeneous values boxed if necessary
* @since 4.21.0
*/
@NotNull ListBinaryTag wrapHeterogeneity();

/**
* A list tag builder.
*
Expand Down
118 changes: 100 additions & 18 deletions nbt/src/main/java/net/kyori/adventure/nbt/ListBinaryTagImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
Expand All @@ -40,13 +41,15 @@

@Debug.Renderer(text = "\"ListBinaryTag[type=\" + this.type.toString() + \"]\"", childrenArray = "this.tags.toArray()", hasChildren = "!this.tags.isEmpty()")
final class ListBinaryTagImpl extends AbstractBinaryTag implements ListBinaryTag {
static final ListBinaryTag EMPTY = new ListBinaryTagImpl(BinaryTagTypes.END, Collections.emptyList());
static final ListBinaryTag EMPTY = new ListBinaryTagImpl(BinaryTagTypes.END, false, Collections.emptyList());
private final List<BinaryTag> tags;
private final boolean permitsHeterogeneity;
private final BinaryTagType<? extends BinaryTag> elementType;
private final int hashCode;

ListBinaryTagImpl(final BinaryTagType<? extends BinaryTag> elementType, final List<BinaryTag> tags) {
ListBinaryTagImpl(final BinaryTagType<? extends BinaryTag> elementType, final boolean permitsHeterogeneity, final List<BinaryTag> tags) {
this.tags = Collections.unmodifiableList(tags);
this.permitsHeterogeneity = permitsHeterogeneity;
this.elementType = elementType;
this.hashCode = tags.hashCode();
}
Expand All @@ -73,12 +76,13 @@ public boolean isEmpty() {

@Override
public @NotNull ListBinaryTag set(final int index, final @NotNull BinaryTag newTag, final @Nullable Consumer<? super BinaryTag> removed) {
final BinaryTagType<?> targetType = ListBinaryTagImpl.validateTagType(newTag, this.elementType, this.permitsHeterogeneity);
return this.edit(tags -> {
final BinaryTag oldTag = tags.set(index, newTag);
if (removed != null) {
removed.accept(oldTag);
}
}, newTag.type());
}, targetType);
}

@Override
Expand All @@ -93,19 +97,16 @@ public boolean isEmpty() {

@Override
public @NotNull ListBinaryTag add(final BinaryTag tag) {
noAddEnd(tag);
if (this.elementType != BinaryTagTypes.END) {
mustBeSameType(tag, this.elementType);
}
return this.edit(tags -> tags.add(tag), tag.type());
final BinaryTagType<?> targetType = validateTagType(tag, this.elementType, this.permitsHeterogeneity);
return this.edit(tags -> tags.add(tag), targetType);
}

@Override
public @NotNull ListBinaryTag add(final Iterable<? extends BinaryTag> tagsToAdd) {
if (tagsToAdd instanceof Collection<?> && ((Collection<?>) tagsToAdd).isEmpty()) {
return this;
}
final BinaryTagType<?> type = ListBinaryTagImpl.mustBeSameType(tagsToAdd);
final BinaryTagType<?> type = ListBinaryTagImpl.validateTagType(tagsToAdd, this.permitsHeterogeneity);
return this.edit(tags -> {
for (final BinaryTag tag : tagsToAdd) {
tags.add(tag);
Expand All @@ -120,35 +121,46 @@ static void noAddEnd(final BinaryTag tag) {
}
}

// Cannot have different element types in a list tag
static BinaryTagType<?> mustBeSameType(final Iterable<? extends BinaryTag> tags) {
// Cannot have different element types in a list tag unless we're in heterogeneous mode
static BinaryTagType<?> validateTagType(final Iterable<? extends BinaryTag> tags, final boolean permitHeterogeneity) {
BinaryTagType<?> type = null;
for (final BinaryTag tag : tags) {
if (type == null) {
noAddEnd(tag);
type = tag.type();
} else {
mustBeSameType(tag, type);
validateTagType(tag, type, permitHeterogeneity);
if (type != tag.type()) {
type = BinaryTagTypes.LIST_WILDCARD;
}
}
}
return type;
}

// Cannot have different element types in a list tag
static void mustBeSameType(final BinaryTag tag, final BinaryTagType<? extends BinaryTag> type) {
if (tag.type() != type) {
// An end tag cannot be an element in a list tag
// AND Cannot have a different element type in a list tag unless we're in heterogeneous mode
static BinaryTagType<?> validateTagType(final BinaryTag tag, final BinaryTagType<? extends BinaryTag> type, final boolean permitHeterogenity) {
noAddEnd(tag);
if (type == BinaryTagTypes.END) {
return tag.type();
}

if (tag.type() != type && !permitHeterogenity) {
throw new IllegalArgumentException(String.format("Trying to add tag of type %s to list of %s", tag.type(), type));
}
return tag.type() != type ? BinaryTagTypes.LIST_WILDCARD : type;
}

private ListBinaryTag edit(final Consumer<List<BinaryTag>> consumer, final @Nullable BinaryTagType<? extends BinaryTag> maybeElementType) {
final List<BinaryTag> tags = new ArrayList<>(this.tags);
consumer.accept(tags);
BinaryTagType<? extends BinaryTag> elementType = this.elementType;
// set the type if it has not yet been set
if (maybeElementType != null && elementType == BinaryTagTypes.END) {
if (maybeElementType != null) {
elementType = maybeElementType;
}
return new ListBinaryTagImpl(elementType, new ArrayList<>(tags)); // explicitly copy
return new ListBinaryTagImpl(elementType, this.permitsHeterogeneity, new ArrayList<>(tags)); // explicitly copy
}

@Override
Expand All @@ -157,7 +169,53 @@ private ListBinaryTag edit(final Consumer<List<BinaryTag>> consumer, final @Null
}

@Override
public Iterator<BinaryTag> iterator() {
public @NotNull ListBinaryTag unwrapHeterogeneity() {
// Unlock where it makes sense
if (!this.permitsHeterogeneity) {
if (this.elementType != BinaryTagTypes.COMPOUND) {
return new ListBinaryTagImpl(this.elementType, true, this.tags);
} else {
List<BinaryTag> newTags = null;
BinaryTag current;
for (final ListIterator<BinaryTag> it = this.tags.listIterator(); it.hasNext();) {
current = it.next();
final BinaryTag unboxed = ListBinaryTag0.unbox((CompoundBinaryTag) current);
// only initialize newTags if we need to unbox something
if (unboxed != current && newTags == null) {
newTags = new ArrayList<>(this.tags.size());
for (int idx = it.nextIndex() - 1, ptr = 0; ptr < idx; ptr++) {
newTags.add(this.tags.get(ptr));
}
}
// if we're already initialized, unconditionally add
if (newTags != null) {
newTags.add(unboxed);
}
}
return new ListBinaryTagImpl(newTags == null ? BinaryTagTypes.COMPOUND : BinaryTagTypes.LIST_WILDCARD, true, newTags == null ? this.tags : newTags);
}
}

// heterogeneity-permitted nothing to unbox
return this;
}

@Override
public @NotNull ListBinaryTag wrapHeterogeneity() {
if (this.elementType != BinaryTagTypes.LIST_WILDCARD) {
return this;
}

final List<BinaryTag> newTags = new ArrayList<>(this.tags.size());
for (final BinaryTag tag : this.tags) {
newTags.add(ListBinaryTag0.box(tag));
}

return new ListBinaryTagImpl(BinaryTagTypes.COMPOUND, false, newTags);
}

@Override
public @NotNull Iterator<BinaryTag> iterator() {
final Iterator<BinaryTag> iterator = this.tags.iterator();
return new Iterator<BinaryTag>() {
@Override
Expand Down Expand Up @@ -205,3 +263,27 @@ public int hashCode() {
);
}
}

final class ListBinaryTag0 {
private static final String WRAPPER_KEY = "";

private ListBinaryTag0() {
}

static BinaryTag unbox(final CompoundBinaryTag compound) {
if (compound.size() == 1) {
final BinaryTag potentialValue = compound.get(WRAPPER_KEY);
if (potentialValue != null) return potentialValue;
}

return compound;
}

static CompoundBinaryTag box(final BinaryTag tag) {
if (tag instanceof CompoundBinaryTag) {
return (CompoundBinaryTag) tag;
} else {
return new CompoundBinaryTagImpl(Collections.singletonMap(WRAPPER_KEY, tag));
}
}
}
17 changes: 7 additions & 10 deletions nbt/src/main/java/net/kyori/adventure/nbt/ListTagBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,22 @@

final class ListTagBuilder<T extends BinaryTag> implements ListBinaryTag.Builder<T> {
private @Nullable List<BinaryTag> tags;
private final boolean permitsHeterogeneity;
private BinaryTagType<? extends BinaryTag> elementType;

ListTagBuilder() {
this(BinaryTagTypes.END);
ListTagBuilder(final boolean permitsHeterogeneity) {
this(permitsHeterogeneity, BinaryTagTypes.END);
}

ListTagBuilder(final BinaryTagType<? extends BinaryTag> type) {
ListTagBuilder(final boolean permitsHeterogeneity, final BinaryTagType<? extends BinaryTag> type) {
this.permitsHeterogeneity = permitsHeterogeneity;
this.elementType = type;
}

@Override
public ListBinaryTag.@NotNull Builder<T> add(final BinaryTag tag) {
ListBinaryTagImpl.noAddEnd(tag);
// set the type if it has not yet been set
if (this.elementType == BinaryTagTypes.END) {
this.elementType = tag.type();
}
// check after changing from an empty tag
ListBinaryTagImpl.mustBeSameType(tag, this.elementType);
this.elementType = ListBinaryTagImpl.validateTagType(tag, this.elementType, this.permitsHeterogeneity);
if (this.tags == null) {
this.tags = new ArrayList<>();
}
Expand All @@ -67,6 +64,6 @@ final class ListTagBuilder<T extends BinaryTag> implements ListBinaryTag.Builder
@Override
public @NotNull ListBinaryTag build() {
if (this.tags == null) return ListBinaryTag.empty();
return new ListBinaryTagImpl(this.elementType, new ArrayList<>(this.tags)); // explicitly copy
return new ListBinaryTagImpl(this.elementType, this.permitsHeterogeneity, new ArrayList<>(this.tags)); // explicitly copy
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ final class TagStringReader {

private final CharBuffer buffer;
private boolean acceptLegacy;
private boolean acceptHeterogenousLists;
private int depth;

TagStringReader(final CharBuffer buffer) {
Expand Down
Loading