Skip to content

Commit f88fa69

Browse files
committed
feat(nbt): Initial implementation of heterogeneous list handling
1 parent 95cea20 commit f88fa69

File tree

7 files changed

+194
-34
lines changed

7 files changed

+194
-34
lines changed

nbt/src/main/java/net/kyori/adventure/nbt/BinaryTagTypes.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ public final class BinaryTagTypes {
116116
}
117117
return ListBinaryTag.listBinaryTag(type, tags);
118118
}
119-
}, (tag, output) -> {
119+
}, (rawTag, output) -> {
120+
final ListBinaryTag tag = rawTag.wrapHeterogeneity();
120121
output.writeByte(tag.elementType().id());
121122
final int size = tag.size();
122123
output.writeInt(size);
@@ -203,6 +204,18 @@ public final class BinaryTagTypes {
203204
output.writeLong(value[i]);
204205
}
205206
});
207+
/**
208+
* Synthetic tag type used as a list's element type to indicate it contains elements of multiple types.
209+
*
210+
* <p>This tag type cannot be read or written. List tag serialization will auto-box lists with this element type.</p>
211+
*
212+
* @since 4.21.0
213+
*/
214+
public static final BinaryTagType<BinaryTag> LIST_WILDCARD = new BinaryTagType.Impl<>(BinaryTag.class, Byte.MAX_VALUE, input -> {
215+
throw new IllegalArgumentException("Unable to read values of placeholder type. This tag type exists only to indicate heterogeneous lists");
216+
}, (tag, output) -> {
217+
throw new IllegalArgumentException("Unable to write values of placeholder type. This tag type exists only to indicate heterogeneous lists");
218+
});
206219

207220
private BinaryTagTypes() {
208221
}

nbt/src/main/java/net/kyori/adventure/nbt/ListBinaryTag.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,17 @@ public interface ListBinaryTag extends ListTagSetter<ListBinaryTag, BinaryTag>,
6969
* @since 4.0.0
7070
*/
7171
static @NotNull Builder<BinaryTag> builder() {
72-
return new ListTagBuilder<>();
72+
return new ListTagBuilder<>(false);
73+
}
74+
75+
/**
76+
* Creates a builder that can accept elements of multiple types.
77+
*
78+
* @return a new builder
79+
* @since 4.21.0
80+
*/
81+
static @NotNull Builder<BinaryTag> heterogeneousListBinaryTag() {
82+
return new ListTagBuilder<>(true);
7383
}
7484

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

8999
/**
@@ -94,13 +104,14 @@ public interface ListBinaryTag extends ListTagSetter<ListBinaryTag, BinaryTag>,
94104
* @param type the element type
95105
* @param tags the elements
96106
* @return a tag
97-
* @throws IllegalArgumentException if {@code type} is {@link BinaryTagTypes#END}
107+
* @throws IllegalArgumentException if {@code type} is {@link BinaryTagTypes#END}, or if elements are of different types
98108
* @since 4.14.0
99109
*/
100110
static @NotNull ListBinaryTag listBinaryTag(final @NotNull BinaryTagType<? extends BinaryTag> type, final @NotNull List<BinaryTag> tags) {
101111
if (tags.isEmpty()) return empty();
102112
if (type == BinaryTagTypes.END) throw new IllegalArgumentException("Cannot create a list of " + BinaryTagTypes.END);
103-
return new ListBinaryTagImpl(type, new ArrayList<>(tags)); // explicitly copy
113+
ListBinaryTagImpl.validateTagType(tags, type == BinaryTagTypes.LIST_WILDCARD);
114+
return new ListBinaryTagImpl(type, type == BinaryTagTypes.LIST_WILDCARD, new ArrayList<>(tags)); // explicitly copy
104115
}
105116

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

581+
/**
582+
* Unwrap any compound-boxed heterogeneous values in this tag.
583+
*
584+
* @return a list tag that permits heterogeneity
585+
* @since 4.21.0
586+
*/
587+
@NotNull ListBinaryTag unwrapHeterogeneity();
588+
589+
/**
590+
* Wrap any heterogeneous values in this tag into compound-tag boxes.
591+
*
592+
* @return a list tag that does not permit heterogeneity, with any heterogeneous values boxed if necessary
593+
* @since 4.21.0
594+
*/
595+
@NotNull ListBinaryTag wrapHeterogeneity();
596+
570597
/**
571598
* A list tag builder.
572599
*

nbt/src/main/java/net/kyori/adventure/nbt/ListBinaryTagImpl.java

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.Collections;
2929
import java.util.Iterator;
3030
import java.util.List;
31+
import java.util.ListIterator;
3132
import java.util.Spliterator;
3233
import java.util.Spliterators;
3334
import java.util.function.Consumer;
@@ -40,13 +41,15 @@
4041

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

48-
ListBinaryTagImpl(final BinaryTagType<? extends BinaryTag> elementType, final List<BinaryTag> tags) {
50+
ListBinaryTagImpl(final BinaryTagType<? extends BinaryTag> elementType, final boolean permitsHeterogeneity, final List<BinaryTag> tags) {
4951
this.tags = Collections.unmodifiableList(tags);
52+
this.permitsHeterogeneity = permitsHeterogeneity;
5053
this.elementType = elementType;
5154
this.hashCode = tags.hashCode();
5255
}
@@ -73,12 +76,13 @@ public boolean isEmpty() {
7376

7477
@Override
7578
public @NotNull ListBinaryTag set(final int index, final @NotNull BinaryTag newTag, final @Nullable Consumer<? super BinaryTag> removed) {
79+
final BinaryTagType<?> targetType = ListBinaryTagImpl.validateTagType(newTag, this.elementType, this.permitsHeterogeneity);
7680
return this.edit(tags -> {
7781
final BinaryTag oldTag = tags.set(index, newTag);
7882
if (removed != null) {
7983
removed.accept(oldTag);
8084
}
81-
}, newTag.type());
85+
}, targetType);
8286
}
8387

8488
@Override
@@ -93,19 +97,16 @@ public boolean isEmpty() {
9397

9498
@Override
9599
public @NotNull ListBinaryTag add(final BinaryTag tag) {
96-
noAddEnd(tag);
97-
if (this.elementType != BinaryTagTypes.END) {
98-
mustBeSameType(tag, this.elementType);
99-
}
100-
return this.edit(tags -> tags.add(tag), tag.type());
100+
final BinaryTagType<?> targetType = validateTagType(tag, this.elementType, this.permitsHeterogeneity);
101+
return this.edit(tags -> tags.add(tag), targetType);
101102
}
102103

103104
@Override
104105
public @NotNull ListBinaryTag add(final Iterable<? extends BinaryTag> tagsToAdd) {
105106
if (tagsToAdd instanceof Collection<?> && ((Collection<?>) tagsToAdd).isEmpty()) {
106107
return this;
107108
}
108-
final BinaryTagType<?> type = ListBinaryTagImpl.mustBeSameType(tagsToAdd);
109+
final BinaryTagType<?> type = ListBinaryTagImpl.validateTagType(tagsToAdd, this.permitsHeterogeneity);
109110
return this.edit(tags -> {
110111
for (final BinaryTag tag : tagsToAdd) {
111112
tags.add(tag);
@@ -120,35 +121,46 @@ static void noAddEnd(final BinaryTag tag) {
120121
}
121122
}
122123

123-
// Cannot have different element types in a list tag
124-
static BinaryTagType<?> mustBeSameType(final Iterable<? extends BinaryTag> tags) {
124+
// Cannot have different element types in a list tag unless we're in heterogeneous mode
125+
static BinaryTagType<?> validateTagType(final Iterable<? extends BinaryTag> tags, final boolean permitHeterogeneity) {
125126
BinaryTagType<?> type = null;
126127
for (final BinaryTag tag : tags) {
127128
if (type == null) {
129+
noAddEnd(tag);
128130
type = tag.type();
129131
} else {
130-
mustBeSameType(tag, type);
132+
validateTagType(tag, type, permitHeterogeneity);
133+
if (type != tag.type()) {
134+
type = BinaryTagTypes.LIST_WILDCARD;
135+
}
131136
}
132137
}
133138
return type;
134139
}
135140

136-
// Cannot have different element types in a list tag
137-
static void mustBeSameType(final BinaryTag tag, final BinaryTagType<? extends BinaryTag> type) {
138-
if (tag.type() != type) {
141+
// An end tag cannot be an element in a list tag
142+
// AND Cannot have a different element type in a list tag unless we're in heterogeneous mode
143+
static BinaryTagType<?> validateTagType(final BinaryTag tag, final BinaryTagType<? extends BinaryTag> type, final boolean permitHeterogenity) {
144+
noAddEnd(tag);
145+
if (type == BinaryTagTypes.END) {
146+
return tag.type();
147+
}
148+
149+
if (tag.type() != type && !permitHeterogenity) {
139150
throw new IllegalArgumentException(String.format("Trying to add tag of type %s to list of %s", tag.type(), type));
140151
}
152+
return tag.type() != type ? BinaryTagTypes.LIST_WILDCARD : type;
141153
}
142154

143155
private ListBinaryTag edit(final Consumer<List<BinaryTag>> consumer, final @Nullable BinaryTagType<? extends BinaryTag> maybeElementType) {
144156
final List<BinaryTag> tags = new ArrayList<>(this.tags);
145157
consumer.accept(tags);
146158
BinaryTagType<? extends BinaryTag> elementType = this.elementType;
147159
// set the type if it has not yet been set
148-
if (maybeElementType != null && elementType == BinaryTagTypes.END) {
160+
if (maybeElementType != null) {
149161
elementType = maybeElementType;
150162
}
151-
return new ListBinaryTagImpl(elementType, new ArrayList<>(tags)); // explicitly copy
163+
return new ListBinaryTagImpl(elementType, this.permitsHeterogeneity, new ArrayList<>(tags)); // explicitly copy
152164
}
153165

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

159171
@Override
160-
public Iterator<BinaryTag> iterator() {
172+
public @NotNull ListBinaryTag unwrapHeterogeneity() {
173+
// Unlock where it makes sense
174+
if (!this.permitsHeterogeneity) {
175+
if (this.elementType != BinaryTagTypes.COMPOUND) {
176+
return new ListBinaryTagImpl(this.elementType, true, this.tags);
177+
} else {
178+
List<BinaryTag> newTags = null;
179+
BinaryTag current;
180+
for (final ListIterator<BinaryTag> it = this.tags.listIterator(); it.hasNext();) {
181+
current = it.next();
182+
final BinaryTag unboxed = ListBinaryTag0.unbox((CompoundBinaryTag) current);
183+
// only initialize newTags if we need to unbox something
184+
if (unboxed != current && newTags == null) {
185+
newTags = new ArrayList<>(this.tags.size());
186+
for (int idx = it.nextIndex() - 1, ptr = 0; ptr < idx; ptr++) {
187+
newTags.add(this.tags.get(ptr));
188+
}
189+
}
190+
// if we're already initialized, unconditionally add
191+
if (newTags != null) {
192+
newTags.add(unboxed);
193+
}
194+
}
195+
return new ListBinaryTagImpl(newTags == null ? BinaryTagTypes.COMPOUND : BinaryTagTypes.LIST_WILDCARD, true, newTags == null ? this.tags : newTags);
196+
}
197+
}
198+
199+
// heterogeneity-permitted nothing to unbox
200+
return this;
201+
}
202+
203+
@Override
204+
public @NotNull ListBinaryTag wrapHeterogeneity() {
205+
if (this.elementType != BinaryTagTypes.LIST_WILDCARD) {
206+
return this;
207+
}
208+
209+
final List<BinaryTag> newTags = new ArrayList<>(this.tags.size());
210+
for (final BinaryTag tag : this.tags) {
211+
newTags.add(ListBinaryTag0.box(tag));
212+
}
213+
214+
return new ListBinaryTagImpl(BinaryTagTypes.COMPOUND, false, newTags);
215+
}
216+
217+
@Override
218+
public @NotNull Iterator<BinaryTag> iterator() {
161219
final Iterator<BinaryTag> iterator = this.tags.iterator();
162220
return new Iterator<BinaryTag>() {
163221
@Override
@@ -205,3 +263,27 @@ public int hashCode() {
205263
);
206264
}
207265
}
266+
267+
final class ListBinaryTag0 {
268+
private static final String WRAPPER_KEY = "";
269+
270+
private ListBinaryTag0() {
271+
}
272+
273+
static BinaryTag unbox(final CompoundBinaryTag compound) {
274+
if (compound.size() == 1) {
275+
final BinaryTag potentialValue = compound.get(WRAPPER_KEY);
276+
if (potentialValue != null) return potentialValue;
277+
}
278+
279+
return compound;
280+
}
281+
282+
static CompoundBinaryTag box(final BinaryTag tag) {
283+
if (tag instanceof CompoundBinaryTag) {
284+
return (CompoundBinaryTag) tag;
285+
} else {
286+
return new CompoundBinaryTagImpl(Collections.singletonMap(WRAPPER_KEY, tag));
287+
}
288+
}
289+
}

nbt/src/main/java/net/kyori/adventure/nbt/ListTagBuilder.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,22 @@
3030

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

35-
ListTagBuilder() {
36-
this(BinaryTagTypes.END);
36+
ListTagBuilder(final boolean permitsHeterogeneity) {
37+
this(permitsHeterogeneity, BinaryTagTypes.END);
3738
}
3839

39-
ListTagBuilder(final BinaryTagType<? extends BinaryTag> type) {
40+
ListTagBuilder(final boolean permitsHeterogeneity, final BinaryTagType<? extends BinaryTag> type) {
41+
this.permitsHeterogeneity = permitsHeterogeneity;
4042
this.elementType = type;
4143
}
4244

4345
@Override
4446
public ListBinaryTag.@NotNull Builder<T> add(final BinaryTag tag) {
45-
ListBinaryTagImpl.noAddEnd(tag);
46-
// set the type if it has not yet been set
47-
if (this.elementType == BinaryTagTypes.END) {
48-
this.elementType = tag.type();
49-
}
5047
// check after changing from an empty tag
51-
ListBinaryTagImpl.mustBeSameType(tag, this.elementType);
48+
this.elementType = ListBinaryTagImpl.validateTagType(tag, this.elementType, this.permitsHeterogeneity);
5249
if (this.tags == null) {
5350
this.tags = new ArrayList<>();
5451
}
@@ -67,6 +64,6 @@ final class ListTagBuilder<T extends BinaryTag> implements ListBinaryTag.Builder
6764
@Override
6865
public @NotNull ListBinaryTag build() {
6966
if (this.tags == null) return ListBinaryTag.empty();
70-
return new ListBinaryTagImpl(this.elementType, new ArrayList<>(this.tags)); // explicitly copy
67+
return new ListBinaryTagImpl(this.elementType, this.permitsHeterogeneity, new ArrayList<>(this.tags)); // explicitly copy
7168
}
7269
}

nbt/src/main/java/net/kyori/adventure/nbt/TagStringReader.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class TagStringReader {
3636

3737
private final CharBuffer buffer;
3838
private boolean acceptLegacy;
39+
private boolean acceptHeterogenousLists;
3940
private int depth;
4041

4142
TagStringReader(final CharBuffer buffer) {

0 commit comments

Comments
 (0)