|
39 | 39 | import org.enginehub.linbus.tree.LinStringTag; |
40 | 40 | import org.enginehub.linbus.tree.LinTag; |
41 | 41 | import org.enginehub.linbus.tree.LinTagType; |
| 42 | +import org.jspecify.annotations.Nullable; |
42 | 43 |
|
43 | 44 | import java.nio.ByteBuffer; |
44 | 45 | import java.nio.IntBuffer; |
45 | 46 | import java.nio.LongBuffer; |
46 | 47 | import java.util.ArrayList; |
47 | 48 | import java.util.Iterator; |
48 | 49 | import java.util.List; |
| 50 | +import java.util.Map; |
49 | 51 | import java.util.Map.Entry; |
50 | 52 | import java.util.function.BiConsumer; |
51 | 53 | import java.util.function.Consumer; |
|
57 | 59 | * A {@link DynamicOps} implementation backed by lin-bus tags, analogous to Minecraft's {@code NbtOps}. |
58 | 60 | * |
59 | 61 | * <p> |
60 | | - * The lin-bus tags are stricter than generic NBT: lists are homogeneous and compounds cannot contain an END value. |
| 62 | + * The lin-bus tags are stricter than generic NBT as compounds cannot contain an END value. |
61 | 63 | * Operations that would produce such a structure either return a failed {@link DataResult} or throw. |
62 | 64 | * </p> |
63 | 65 | */ |
@@ -189,39 +191,21 @@ public DataResult<LinTag<?>> mergeToList(LinTag<?> list, LinTag<?> value) { |
189 | 191 | @Override |
190 | 192 | public DataResult<LinTag<?>> mergeToList(LinTag<?> list, List<LinTag<?>> values) { |
191 | 193 | return switch (list) { |
192 | | - case LinListTag<?> existing -> mergeList(existing.value(), values); |
| 194 | + case LinListTag<?> existing -> mergeRaw(unwrapStoredList(existing), values); |
193 | 195 | case LinByteArrayTag array when array.view().hasRemaining() -> mergeBytes(array, values); |
194 | 196 | case LinIntArrayTag array when array.view().hasRemaining() -> mergeInts(array, values); |
195 | 197 | case LinLongArrayTag array when array.view().hasRemaining() -> mergeLongs(array, values); |
196 | | - case LinByteArrayTag _, LinIntArrayTag _, LinLongArrayTag _, LinEndTag _ -> mergeList(List.of(), values); |
| 198 | + case LinByteArrayTag _, LinIntArrayTag _, LinLongArrayTag _, LinEndTag _ -> mergeRaw(List.of(), values); |
197 | 199 | default -> DataResult.error(() -> "mergeToList called with non-list: " + list, list); |
198 | 200 | }; |
199 | 201 | } |
200 | 202 |
|
201 | | - private static DataResult<LinTag<?>> mergeList(List<? extends LinTag<?>> prefix, List<LinTag<?>> values) { |
| 203 | + private static DataResult<LinTag<?>> mergeRaw(List<LinTag<?>> prefix, List<LinTag<?>> values) { |
202 | 204 | if (prefix.isEmpty() && values.isEmpty()) { |
203 | 205 | return DataResult.success(LinListTag.empty(LinTagType.endTag())); |
204 | 206 | } |
205 | | - LinTagType<? extends LinTag<?>> elementType = |
206 | | - prefix.isEmpty() ? values.getFirst().type() : prefix.getFirst().type(); |
207 | | - return mergeListTyped(elementType, prefix, values); |
208 | | - } |
209 | | - |
210 | | - @SuppressWarnings("unchecked") |
211 | | - private static <T extends LinTag<?>> DataResult<LinTag<?>> mergeListTyped( |
212 | | - LinTagType<T> elementType, List<? extends LinTag<?>> prefix, List<LinTag<?>> values |
213 | | - ) { |
214 | | - LinListTag.Builder<T> builder = LinListTag.builderWithExpectedSize( |
215 | | - elementType, prefix.size() + values.size() |
216 | | - ); |
217 | 207 | try { |
218 | | - /* |
219 | | - * addAll checks every element against elementType and throws on a mismatch, so the |
220 | | - * casts to the element type are sound. |
221 | | - */ |
222 | | - builder.addAll((List<? extends T>) prefix); |
223 | | - builder.addAll((List<? extends T>) values); |
224 | | - return DataResult.success(builder.build()); |
| 208 | + return DataResult.success(createHomogenizedList(prefix, values)); |
225 | 209 | } catch (IllegalArgumentException e) { |
226 | 210 | return DataResult.error(e::getMessage); |
227 | 211 | } |
@@ -373,7 +357,7 @@ public LinTag<?> createMap(Stream<Pair<LinTag<?>, LinTag<?>>> map) { |
373 | 357 | @Override |
374 | 358 | public DataResult<Stream<LinTag<?>>> getStream(LinTag<?> input) { |
375 | 359 | return switch (input) { |
376 | | - case LinListTag<?> tag -> DataResult.success(tag.value().stream().map(element -> (LinTag<?>) element)); |
| 360 | + case LinListTag<?> tag -> DataResult.success(unwrapStoredList(tag).stream()); |
377 | 361 | case LinByteArrayTag tag -> { |
378 | 362 | ByteBuffer values = tag.view(); |
379 | 363 | yield DataResult.success( |
@@ -442,22 +426,102 @@ public LinTag<?> createLongList(LongStream input) { |
442 | 426 |
|
443 | 427 | @Override |
444 | 428 | public LinTag<?> createList(Stream<LinTag<?>> input) { |
445 | | - List<LinTag<?>> elements = input.toList(); |
446 | | - if (elements.isEmpty()) { |
| 429 | + return createHomogenizedList(input.toList(), List.of()); |
| 430 | + } |
| 431 | + |
| 432 | + private static LinTag<?> createHomogenizedList(List<LinTag<?>> prefix, List<LinTag<?>> values) { |
| 433 | + LinTagType<? extends LinTag<?>> rawType = identifyRawElementType(prefix, values); |
| 434 | + if (rawType == LinTagType.endTag()) { |
| 435 | + assert prefix.isEmpty() && values.isEmpty() |
| 436 | + : "rawType is endTag but elements are not empty: " + prefix + ", " + values; |
447 | 437 | return LinListTag.empty(LinTagType.endTag()); |
448 | 438 | } |
449 | | - return createListTagTyped(elements.getFirst().type(), elements); |
| 439 | + boolean compound = rawType == LinTagType.compoundTag(); |
| 440 | + LinListTag.Builder<LinTag<?>> builder = homogenizedBuilder(rawType, prefix.size() + values.size()); |
| 441 | + addHomogenized(builder, prefix, compound); |
| 442 | + addHomogenized(builder, values, compound); |
| 443 | + return builder.build(); |
450 | 444 | } |
451 | 445 |
|
452 | 446 | @SuppressWarnings("unchecked") |
453 | | - private static <T extends LinTag<?>> LinListTag<T> createListTagTyped( |
454 | | - LinTagType<T> elementType, List<LinTag<?>> elements |
| 447 | + private static LinListTag.Builder<LinTag<?>> homogenizedBuilder( |
| 448 | + LinTagType<? extends LinTag<?>> elementType, int expectedSize |
455 | 449 | ) { |
456 | 450 | /* |
457 | | - * LinListTag.of checks every element against elementType, which is the runtime type |
458 | | - * of the first element, so casting the homogeneous list to List<T> is sound. |
| 451 | + * The builder checks every element against elementType, which is the shared runtime type of the |
| 452 | + * elements, so widening the builder's element type to LinTag<?> is sound. |
459 | 453 | */ |
460 | | - return LinListTag.of(elementType, (List<T>) elements); |
| 454 | + return (LinListTag.Builder<LinTag<?>>) LinListTag.builderWithExpectedSize(elementType, expectedSize); |
| 455 | + } |
| 456 | + |
| 457 | + private static void addHomogenized( |
| 458 | + LinListTag.Builder<LinTag<?>> builder, List<LinTag<?>> elements, boolean compound |
| 459 | + ) { |
| 460 | + if (!compound) { |
| 461 | + builder.addAll(elements); |
| 462 | + return; |
| 463 | + } |
| 464 | + for (LinTag<?> element : elements) { |
| 465 | + builder.add(element instanceof LinCompoundTag existing ? existing : wrapElement(element)); |
| 466 | + } |
| 467 | + } |
| 468 | + |
| 469 | + /** |
| 470 | + * {@return the shared type of {@code elements}, or the compound type if they are heterogeneous} |
| 471 | + * |
| 472 | + * <p> |
| 473 | + * This mirrors Minecraft's {@code ListTag} raw-element-type identification: a heterogeneous list is |
| 474 | + * reported as compound so its elements are stored in wrapper compounds. |
| 475 | + * </p> |
| 476 | + */ |
| 477 | + private static LinTagType<? extends LinTag<?>> identifyRawElementType( |
| 478 | + List<LinTag<?>> prefix, List<LinTag<?>> values |
| 479 | + ) { |
| 480 | + LinTagType<? extends LinTag<?>> type = scanRawElementType(null, prefix); |
| 481 | + if (type != LinTagType.compoundTag()) { |
| 482 | + type = scanRawElementType(type, values); |
| 483 | + } |
| 484 | + return type == null ? LinTagType.endTag() : type; |
| 485 | + } |
| 486 | + |
| 487 | + private static @Nullable LinTagType<? extends LinTag<?>> scanRawElementType( |
| 488 | + @Nullable LinTagType<? extends LinTag<?>> currentType, List<LinTag<?>> elements |
| 489 | + ) { |
| 490 | + for (LinTag<?> element : elements) { |
| 491 | + LinTagType<? extends LinTag<?>> elementType = element.type(); |
| 492 | + // If it's a compound list or heterogeneous, we treat it as a compound list. |
| 493 | + if (elementType == LinTagType.compoundTag() || (currentType != null && currentType != elementType)) { |
| 494 | + return LinTagType.compoundTag(); |
| 495 | + } |
| 496 | + currentType = elementType; |
| 497 | + } |
| 498 | + return currentType; |
| 499 | + } |
| 500 | + |
| 501 | + /** |
| 502 | + * {@return the raw (logical) elements of a stored list, unwrapping any {@code {"": value}} wrappers} |
| 503 | + */ |
| 504 | + private static List<LinTag<?>> unwrapStoredList(LinListTag<?> list) { |
| 505 | + boolean wrapped = list.elementType() == LinTagType.compoundTag(); |
| 506 | + List<LinTag<?>> result = new ArrayList<>(list.value().size()); |
| 507 | + for (LinTag<?> element : list.value()) { |
| 508 | + result.add(wrapped ? tryUnwrap(element) : element); |
| 509 | + } |
| 510 | + return result; |
| 511 | + } |
| 512 | + |
| 513 | + private static LinCompoundTag wrapElement(LinTag<?> element) { |
| 514 | + return LinCompoundTag.of(Map.of("", element)); |
| 515 | + } |
| 516 | + |
| 517 | + private static LinTag<?> tryUnwrap(LinTag<?> element) { |
| 518 | + if (element instanceof LinCompoundTag compound && compound.value().size() == 1) { |
| 519 | + LinTag<?> unwrapped = compound.value().get(""); |
| 520 | + if (unwrapped != null) { |
| 521 | + return unwrapped; |
| 522 | + } |
| 523 | + } |
| 524 | + return element; |
461 | 525 | } |
462 | 526 |
|
463 | 527 | @Override |
|
0 commit comments