From 2c9affa0e322c600df921260382d03b8fb17587a Mon Sep 17 00:00:00 2001 From: Nassim Jahnke Date: Sun, 10 Dec 2023 12:10:15 +0100 Subject: [PATCH] Move component conversion into its own class Remove serialized component type in 1.20.3->1.20.2 --- .../api/type/types/UUIDIntArrayType.java | 66 ---- .../packets/InventoryPackets.java | 10 +- .../packets/WorldPackets.java | 6 +- .../Protocol1_20_3To1_20_2.java | 277 +--------------- .../rewriter/EntityPacketRewriter1_20_3.java | 5 +- .../util/ComponentConverter.java | 304 ++++++++++++++++++ .../viaversion/viaversion/util/UUIDUtil.java | 47 +++ 7 files changed, 365 insertions(+), 350 deletions(-) delete mode 100644 api/src/main/java/com/viaversion/viaversion/api/type/types/UUIDIntArrayType.java create mode 100644 common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/util/ComponentConverter.java create mode 100644 common/src/main/java/com/viaversion/viaversion/util/UUIDUtil.java diff --git a/api/src/main/java/com/viaversion/viaversion/api/type/types/UUIDIntArrayType.java b/api/src/main/java/com/viaversion/viaversion/api/type/types/UUIDIntArrayType.java deleted file mode 100644 index b7076e30a..000000000 --- a/api/src/main/java/com/viaversion/viaversion/api/type/types/UUIDIntArrayType.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion - * Copyright (C) 2016-2023 ViaVersion and contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package com.viaversion.viaversion.api.type.types; - -import com.viaversion.viaversion.api.type.Type; -import io.netty.buffer.ByteBuf; -import java.util.UUID; - -public class UUIDIntArrayType extends Type { - - public UUIDIntArrayType() { - super(UUID.class); - } - - @Override - public UUID read(ByteBuf buffer) { - int[] ints = { - buffer.readInt(), - buffer.readInt(), - buffer.readInt(), - buffer.readInt() - }; - return uuidFromIntArray(ints); - } - - @Override - public void write(ByteBuf buffer, UUID object) { - int[] ints = uuidToIntArray(object); - buffer.writeInt(ints[0]); - buffer.writeInt(ints[1]); - buffer.writeInt(ints[2]); - buffer.writeInt(ints[3]); - } - - public static UUID uuidFromIntArray(int[] ints) { - return new UUID((long) ints[0] << 32 | ((long) ints[1] & 0xFFFFFFFFL), (long) ints[2] << 32 | ((long) ints[3] & 0xFFFFFFFFL)); - } - - public static int[] uuidToIntArray(UUID uuid) { - return bitsToIntArray(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); - } - - public static int[] bitsToIntArray(long long1, long long2) { - return new int[]{(int) (long1 >> 32), (int) long1, (int) (long2 >> 32), (int) long2}; - } -} diff --git a/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_16to1_15_2/packets/InventoryPackets.java b/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_16to1_15_2/packets/InventoryPackets.java index 2cf41c79b..3e2db6041 100644 --- a/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_16to1_15_2/packets/InventoryPackets.java +++ b/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_16to1_15_2/packets/InventoryPackets.java @@ -29,7 +29,6 @@ import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; import com.viaversion.viaversion.api.protocol.remapper.PacketHandler; import com.viaversion.viaversion.api.protocol.remapper.PacketHandlers; import com.viaversion.viaversion.api.type.Type; -import com.viaversion.viaversion.api.type.types.UUIDIntArrayType; import com.viaversion.viaversion.protocols.protocol1_15to1_14_4.ClientboundPackets1_15; import com.viaversion.viaversion.protocols.protocol1_16to1_15_2.ClientboundPackets1_16; import com.viaversion.viaversion.protocols.protocol1_16to1_15_2.Protocol1_16To1_15_2; @@ -38,6 +37,7 @@ import com.viaversion.viaversion.protocols.protocol1_16to1_15_2.storage.Inventor import com.viaversion.viaversion.rewriter.ItemRewriter; import com.viaversion.viaversion.rewriter.RecipeRewriter; import com.viaversion.viaversion.util.Key; +import com.viaversion.viaversion.util.UUIDUtil; import java.util.UUID; public class InventoryPackets extends ItemRewriter { @@ -153,7 +153,7 @@ public class InventoryPackets extends ItemRewriter Target UUID targetUuid = UUID.fromString((String) targetUuidTag.getValue()); - compoundTag.put("Target", new IntArrayTag(UUIDIntArrayType.uuidToIntArray(targetUuid))); + compoundTag.put("Target", new IntArrayTag(UUIDUtil.toIntArray(targetUuid))); } else if (id.equals("minecraft:skull") && compoundTag.get("Owner") instanceof CompoundTag) { CompoundTag ownerTag = compoundTag.remove("Owner"); StringTag ownerUuidTag = ownerTag.remove("Id"); if (ownerUuidTag != null) { UUID ownerUuid = UUID.fromString(ownerUuidTag.getValue()); - ownerTag.put("Id", new IntArrayTag(UUIDIntArrayType.uuidToIntArray(ownerUuid))); + ownerTag.put("Id", new IntArrayTag(UUIDUtil.toIntArray(ownerUuid))); } // Owner -> SkullOwner diff --git a/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/Protocol1_20_3To1_20_2.java b/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/Protocol1_20_3To1_20_2.java index c36ea44e4..d32abff43 100644 --- a/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/Protocol1_20_3To1_20_2.java +++ b/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/Protocol1_20_3To1_20_2.java @@ -17,26 +17,6 @@ */ package com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2; -import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag; -import com.github.steveice10.opennbt.tag.builtin.ByteTag; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.DoubleTag; -import com.github.steveice10.opennbt.tag.builtin.FloatTag; -import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; -import com.github.steveice10.opennbt.tag.builtin.IntTag; -import com.github.steveice10.opennbt.tag.builtin.ListTag; -import com.github.steveice10.opennbt.tag.builtin.LongArrayTag; -import com.github.steveice10.opennbt.tag.builtin.LongTag; -import com.github.steveice10.opennbt.tag.builtin.NumberTag; -import com.github.steveice10.opennbt.tag.builtin.ShortTag; -import com.github.steveice10.opennbt.tag.builtin.StringTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.internal.LazilyParsedNumber; -import com.viaversion.viaversion.api.Via; import com.viaversion.viaversion.api.connection.UserConnection; import com.viaversion.viaversion.api.data.MappingData; import com.viaversion.viaversion.api.data.MappingDataBase; @@ -49,7 +29,6 @@ import com.viaversion.viaversion.api.protocol.packet.State; import com.viaversion.viaversion.api.protocol.remapper.PacketHandler; import com.viaversion.viaversion.api.protocol.remapper.PacketHandlers; import com.viaversion.viaversion.api.type.Type; -import com.viaversion.viaversion.api.type.types.UUIDIntArrayType; import com.viaversion.viaversion.api.type.types.misc.ParticleType; import com.viaversion.viaversion.api.type.types.version.Types1_20_3; import com.viaversion.viaversion.data.entity.EntityTrackerBase; @@ -63,30 +42,17 @@ import com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.packet.Clientb import com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.packet.ServerboundPackets1_20_3; import com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.rewriter.BlockItemPacketRewriter1_20_3; import com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.rewriter.EntityPacketRewriter1_20_3; +import com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.util.ComponentConverter; import com.viaversion.viaversion.rewriter.SoundRewriter; import com.viaversion.viaversion.rewriter.StatisticsRewriter; import com.viaversion.viaversion.rewriter.TagRewriter; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.BitSet; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.logging.Level; -import org.checkerframework.checker.nullness.qual.Nullable; public final class Protocol1_20_3To1_20_2 extends AbstractProtocol { public static final MappingData MAPPINGS = new MappingDataBase("1.20.2", "1.20.3"); - private static final Set BOOLEAN_TYPES = new HashSet<>(Arrays.asList( - "interpret", - "bold", - "italic", - "underlined", - "strikethrough", - "obfuscated" - )); private final BlockItemPacketRewriter1_20_3 itemRewriter = new BlockItemPacketRewriter1_20_3(this); private final EntityPacketRewriter1_20_3 entityRewriter = new EntityPacketRewriter1_20_3(this); @@ -370,248 +336,11 @@ public final class Protocol1_20_3To1_20_2 extends AbstractProtocol"); - } - } - - public static @Nullable Tag jsonComponentToTag(@Nullable final JsonElement component) { - try { - return convertToTag(component); - } catch (final Exception e) { - Via.getPlatform().getLogger().log(Level.SEVERE, "Error converting component: " + component, e); - return new StringTag(""); - } - } - - private static @Nullable Tag convertToTag(final @Nullable JsonElement element) { - if (element == null || element.isJsonNull()) { - return null; - } else if (element.isJsonObject()) { - final CompoundTag tag = new CompoundTag(); - final JsonObject jsonObject = element.getAsJsonObject(); - for (final Map.Entry entry : jsonObject.entrySet()) { - convertObjectEntry(entry.getKey(), entry.getValue(), tag); - } - - addComponentType(jsonObject, tag); - return tag; - } else if (element.isJsonArray()) { - return convertJsonArray(element.getAsJsonArray()); - } else if (element.isJsonPrimitive()) { - final JsonPrimitive primitive = element.getAsJsonPrimitive(); - if (primitive.isString()) { - return new StringTag(primitive.getAsString()); - } else if (primitive.isBoolean()) { - return new ByteTag((byte) (primitive.getAsBoolean() ? 1 : 0)); - } - - final Number number = primitive.getAsNumber(); - if (number instanceof Integer) { - return new IntTag(number.intValue()); - } else if (number instanceof Byte) { - return new ByteTag(number.byteValue()); - } else if (number instanceof Short) { - return new ShortTag(number.shortValue()); - } else if (number instanceof Long) { - return new LongTag(number.longValue()); - } else if (number instanceof Double) { - return new DoubleTag(number.doubleValue()); - } else if (number instanceof Float) { - return new FloatTag(number.floatValue()); - } else if (number instanceof LazilyParsedNumber) { - // TODO: This might need better handling - return new IntTag(number.intValue()); - } - return new IntTag(number.intValue()); // ??? - } - throw new IllegalArgumentException("Unhandled json type " + element.getClass().getSimpleName() + " with value " + element.getAsString()); - } - - private static void addComponentType(final JsonObject object, final CompoundTag tag) { - if (object.has("type")) { - return; - } - - // Add the type to speed up deserialization and make DFU errors slightly more useful - // Order is important - if (object.has("text")) { - tag.put("type", new StringTag("text")); - } else if (object.has("translate")) { - tag.put("type", new StringTag("translatable")); - } else if (object.has("score")) { - tag.put("type", new StringTag("score")); - } else if (object.has("selector")) { - tag.put("type", new StringTag("selector")); - } else if (object.has("keybind")) { - tag.put("type", new StringTag("keybind")); - } else if (object.has("nbt")) { - tag.put("type", new StringTag("nbt")); - } - } - - private static ListTag convertJsonArray(final JsonArray array) { - // TODO Number arrays? - final ListTag listTag = new ListTag(); - boolean singleType = true; - for (final JsonElement entry : array) { - final Tag convertedEntryTag = convertToTag(entry); - if (listTag.getElementType() != null && listTag.getElementType() != convertedEntryTag.getClass()) { - singleType = false; - break; - } - - listTag.add(convertedEntryTag); - } - - if (singleType) { - return listTag; - } - - // Generally, vanilla-esque serializers should not produce this format, so it should be rare - // Lists are only used for lists of components ("extra" and "with") - final ListTag processedListTag = new ListTag(); - for (final JsonElement entry : array) { - final Tag convertedTag = convertToTag(entry); - if (convertedTag instanceof CompoundTag) { - processedListTag.add(convertedTag); - continue; - } - - // Wrap all entries in compound tags, as lists can only consist of one type of tag - final CompoundTag compoundTag = new CompoundTag(); - compoundTag.put("type", new StringTag("text")); - if (convertedTag instanceof ListTag) { - compoundTag.put("text", new StringTag()); - compoundTag.put("extra", convertedTag); - } else { - compoundTag.put("text", new StringTag(convertedTag.asRawString())); - } - processedListTag.add(compoundTag); - } - return processedListTag; - } - - /** - * Converts a json object entry to a tag entry. - * - * @param key key of the entry - * @param value value of the entry - * @param tag the resulting compound tag - */ - private static void convertObjectEntry(final String key, final JsonElement value, final CompoundTag tag) { - if ((key.equals("contents")) && value.isJsonObject()) { - // Store show_entity id as int array instead of uuid string - // Not really required, but we might as well make it more compact - final JsonObject hoverEvent = value.getAsJsonObject(); - final JsonElement id = hoverEvent.get("id"); - final UUID uuid; - if (id != null && id.isJsonPrimitive() && (uuid = parseUUID(id.getAsString())) != null) { - hoverEvent.remove("id"); - - final CompoundTag convertedTag = (CompoundTag) convertToTag(value); - convertedTag.put("id", new IntArrayTag(UUIDIntArrayType.uuidToIntArray(uuid))); - tag.put(key, convertedTag); - return; - } - } - - tag.put(key, convertToTag(value)); - } - - private static @Nullable UUID parseUUID(final String uuidString) { - try { - return UUID.fromString(uuidString); - } catch (final IllegalArgumentException e) { - return null; - } - } - - private static @Nullable JsonElement convertToJson(final @Nullable String key, final @Nullable Tag tag) { - if (tag == null) { - return null; - } else if (tag instanceof CompoundTag) { - final JsonObject object = new JsonObject(); - for (final Map.Entry entry : ((CompoundTag) tag).entrySet()) { - convertCompoundTagEntry(entry.getKey(), entry.getValue(), object); - } - return object; - } else if (tag instanceof ListTag) { - final ListTag list = (ListTag) tag; - final JsonArray array = new JsonArray(); - for (final Tag listEntry : list) { - array.add(convertToJson(null, listEntry)); - } - return array; - } else if (tag instanceof NumberTag) { - final NumberTag numberTag = (NumberTag) tag; - if (key != null && BOOLEAN_TYPES.contains(key)) { - // Booleans don't have a direct representation in nbt - return new JsonPrimitive(numberTag.asBoolean()); - } - return new JsonPrimitive(numberTag.getValue()); - } else if (tag instanceof StringTag) { - return new JsonPrimitive(((StringTag) tag).getValue()); - } else if (tag instanceof ByteArrayTag) { - final ByteArrayTag arrayTag = (ByteArrayTag) tag; - final JsonArray array = new JsonArray(); - for (final byte num : arrayTag.getValue()) { - array.add(num); - } - return array; - } else if (tag instanceof IntArrayTag) { - final IntArrayTag arrayTag = (IntArrayTag) tag; - final JsonArray array = new JsonArray(); - for (final int num : arrayTag.getValue()) { - array.add(num); - } - return array; - } else if (tag instanceof LongArrayTag) { - final LongArrayTag arrayTag = (LongArrayTag) tag; - final JsonArray array = new JsonArray(); - for (final long num : arrayTag.getValue()) { - array.add(num); - } - return array; - } - throw new IllegalArgumentException("Unhandled tag type " + tag.getClass().getSimpleName()); - } - - private static void convertCompoundTagEntry(final String key, final Tag tag, final JsonObject object) { - if ((key.equals("contents")) && tag instanceof CompoundTag) { - // Back to a UUID string - final CompoundTag showEntity = (CompoundTag) tag; - final Tag idTag = showEntity.get("id"); - if (idTag instanceof IntArrayTag) { - showEntity.remove("id"); - - final JsonObject convertedElement = (JsonObject) convertToJson(key, tag); - convertedElement.addProperty("id", uuidIntsToString(((IntArrayTag) idTag).getValue())); - object.add(key, convertedElement); - return; - } - } - - // "":1 is a valid tag, but not a valid json component - object.add(key.isEmpty() ? "text" : key, convertToJson(key, tag)); - } - - private static String uuidIntsToString(final int[] parts) { - if (parts.length != 4) { - return new UUID(0, 0).toString(); - } - return UUIDIntArrayType.uuidFromIntArray(parts).toString(); + wrapper.write(Type.OPTIONAL_TAG, ComponentConverter.jsonComponentToTag(wrapper.read(Type.OPTIONAL_COMPONENT))); } @Override diff --git a/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/rewriter/EntityPacketRewriter1_20_3.java b/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/rewriter/EntityPacketRewriter1_20_3.java index 4fcc6c23b..0bda1241b 100644 --- a/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/rewriter/EntityPacketRewriter1_20_3.java +++ b/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/rewriter/EntityPacketRewriter1_20_3.java @@ -32,6 +32,7 @@ import com.viaversion.viaversion.protocols.protocol1_20_2to1_20.packet.Clientbou import com.viaversion.viaversion.protocols.protocol1_20_2to1_20.packet.ClientboundPackets1_20_2; import com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.Protocol1_20_3To1_20_2; import com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.packet.ClientboundPackets1_20_3; +import com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.util.ComponentConverter; import com.viaversion.viaversion.rewriter.EntityRewriter; import com.viaversion.viaversion.util.Key; @@ -102,9 +103,9 @@ public final class EntityPacketRewriter1_20_3 extends EntityRewriter { final MetaType type = meta.metaType(); if (type == Types1_20_2.META_TYPES.componentType) { - meta.setTypeAndValue(Types1_20_3.META_TYPES.componentType, Protocol1_20_3To1_20_2.jsonComponentToTag(meta.value())); + meta.setTypeAndValue(Types1_20_3.META_TYPES.componentType, ComponentConverter.jsonComponentToTag(meta.value())); } else if (type == Types1_20_2.META_TYPES.optionalComponentType) { - meta.setTypeAndValue(Types1_20_3.META_TYPES.optionalComponentType, Protocol1_20_3To1_20_2.jsonComponentToTag(meta.value())); + meta.setTypeAndValue(Types1_20_3.META_TYPES.optionalComponentType, ComponentConverter.jsonComponentToTag(meta.value())); } else if (type == Types1_20_2.META_TYPES.particleType) { final Particle particle = (Particle) meta.getValue(); final ParticleMappings particleMappings = protocol.getMappingData().getParticleMappings(); diff --git a/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/util/ComponentConverter.java b/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/util/ComponentConverter.java new file mode 100644 index 000000000..f78489168 --- /dev/null +++ b/common/src/main/java/com/viaversion/viaversion/protocols/protocol1_20_3to1_20_2/util/ComponentConverter.java @@ -0,0 +1,304 @@ +/* + * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion + * Copyright (C) 2023 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viaversion.protocols.protocol1_20_3to1_20_2.util; + +import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag; +import com.github.steveice10.opennbt.tag.builtin.ByteTag; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.DoubleTag; +import com.github.steveice10.opennbt.tag.builtin.FloatTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.LongArrayTag; +import com.github.steveice10.opennbt.tag.builtin.LongTag; +import com.github.steveice10.opennbt.tag.builtin.NumberTag; +import com.github.steveice10.opennbt.tag.builtin.ShortTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.internal.LazilyParsedNumber; +import com.viaversion.viaversion.api.Via; +import com.viaversion.viaversion.util.Pair; +import com.viaversion.viaversion.util.UUIDUtil; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class ComponentConverter { + + private static final Set BOOLEAN_TYPES = new HashSet<>(Arrays.asList( + "interpret", + "bold", + "italic", + "underlined", + "strikethrough", + "obfuscated" + )); + // Order is important + private static final List> COMPONENT_TYPES = Arrays.asList( + new Pair<>("text", "text"), + new Pair<>("translatable", "translate"), + new Pair<>("score", "score"), + new Pair<>("selector", "selector"), + new Pair<>("keybind", "keybind"), + new Pair<>("nbt", "nbt") + ); + + public static @Nullable JsonElement tagComponentToJson(@Nullable final Tag tag) { + try { + return convertToJson(null, tag); + } catch (final Exception e) { + Via.getPlatform().getLogger().log(Level.SEVERE, "Error converting component: " + tag, e); + return new JsonPrimitive(""); + } + } + + public static @Nullable Tag jsonComponentToTag(@Nullable final JsonElement component) { + try { + return convertToTag(component); + } catch (final Exception e) { + Via.getPlatform().getLogger().log(Level.SEVERE, "Error converting component: " + component, e); + return new StringTag(""); + } + } + + private static @Nullable Tag convertToTag(final @Nullable JsonElement element) { + if (element == null || element.isJsonNull()) { + return null; + } else if (element.isJsonObject()) { + final CompoundTag tag = new CompoundTag(); + final JsonObject jsonObject = element.getAsJsonObject(); + for (final Map.Entry entry : jsonObject.entrySet()) { + convertObjectEntry(entry.getKey(), entry.getValue(), tag); + } + + addComponentType(jsonObject, tag); + return tag; + } else if (element.isJsonArray()) { + return convertJsonArray(element.getAsJsonArray()); + } else if (element.isJsonPrimitive()) { + final JsonPrimitive primitive = element.getAsJsonPrimitive(); + if (primitive.isString()) { + return new StringTag(primitive.getAsString()); + } else if (primitive.isBoolean()) { + return new ByteTag((byte) (primitive.getAsBoolean() ? 1 : 0)); + } + + final Number number = primitive.getAsNumber(); + if (number instanceof Integer) { + return new IntTag(number.intValue()); + } else if (number instanceof Byte) { + return new ByteTag(number.byteValue()); + } else if (number instanceof Short) { + return new ShortTag(number.shortValue()); + } else if (number instanceof Long) { + return new LongTag(number.longValue()); + } else if (number instanceof Double) { + return new DoubleTag(number.doubleValue()); + } else if (number instanceof Float) { + return new FloatTag(number.floatValue()); + } else if (number instanceof LazilyParsedNumber) { + // TODO: This might need better handling + return new IntTag(number.intValue()); + } + return new IntTag(number.intValue()); // ??? + } + throw new IllegalArgumentException("Unhandled json type " + element.getClass().getSimpleName() + " with value " + element.getAsString()); + } + + private static ListTag convertJsonArray(final JsonArray array) { + // TODO Number arrays? + final ListTag listTag = new ListTag(); + boolean singleType = true; + for (final JsonElement entry : array) { + final Tag convertedEntryTag = convertToTag(entry); + if (listTag.getElementType() != null && listTag.getElementType() != convertedEntryTag.getClass()) { + singleType = false; + break; + } + + listTag.add(convertedEntryTag); + } + + if (singleType) { + return listTag; + } + + // Generally, vanilla-esque serializers should not produce this format, so it should be rare + // Lists are only used for lists of components ("extra" and "with") + final ListTag processedListTag = new ListTag(); + for (final JsonElement entry : array) { + final Tag convertedTag = convertToTag(entry); + if (convertedTag instanceof CompoundTag) { + processedListTag.add(convertedTag); + continue; + } + + // Wrap all entries in compound tags, as lists can only consist of one type of tag + final CompoundTag compoundTag = new CompoundTag(); + compoundTag.put("type", new StringTag("text")); + if (convertedTag instanceof ListTag) { + compoundTag.put("text", new StringTag()); + compoundTag.put("extra", convertedTag); + } else { + compoundTag.put("text", new StringTag(convertedTag.asRawString())); + } + processedListTag.add(compoundTag); + } + return processedListTag; + } + + /** + * Converts a json object entry to a tag entry. + * + * @param key key of the entry + * @param value value of the entry + * @param tag the resulting compound tag + */ + private static void convertObjectEntry(final String key, final JsonElement value, final CompoundTag tag) { + if ((key.equals("contents")) && value.isJsonObject()) { + // Store show_entity id as int array instead of uuid string + // Not really required, but we might as well make it more compact + final JsonObject hoverEvent = value.getAsJsonObject(); + final JsonElement id = hoverEvent.get("id"); + final UUID uuid; + if (id != null && id.isJsonPrimitive() && (uuid = UUIDUtil.parseUUID(id.getAsString())) != null) { + hoverEvent.remove("id"); + + final CompoundTag convertedTag = (CompoundTag) convertToTag(value); + convertedTag.put("id", new IntArrayTag(UUIDUtil.toIntArray(uuid))); + tag.put(key, convertedTag); + return; + } + } + + tag.put(key, convertToTag(value)); + } + + private static void addComponentType(final JsonObject object, final CompoundTag tag) { + if (object.has("type")) { + return; + } + + // Add the type to speed up deserialization and make DFU errors slightly more useful + for (final Pair pair : COMPONENT_TYPES) { + if (object.has(pair.value())) { + tag.put("type", new StringTag(pair.key())); + return; + } + } + } + + private static @Nullable JsonElement convertToJson(final @Nullable String key, final @Nullable Tag tag) { + if (tag == null) { + return null; + } else if (tag instanceof CompoundTag) { + final JsonObject object = new JsonObject(); + if (!"value".equals(key)) { + removeComponentType(object); + } + + for (final Map.Entry entry : ((CompoundTag) tag).entrySet()) { + convertCompoundTagEntry(entry.getKey(), entry.getValue(), object); + } + return object; + } else if (tag instanceof ListTag) { + final ListTag list = (ListTag) tag; + final JsonArray array = new JsonArray(); + for (final Tag listEntry : list) { + array.add(convertToJson(null, listEntry)); + } + return array; + } else if (tag instanceof NumberTag) { + final NumberTag numberTag = (NumberTag) tag; + if (key != null && BOOLEAN_TYPES.contains(key)) { + // Booleans don't have a direct representation in nbt + return new JsonPrimitive(numberTag.asBoolean()); + } + return new JsonPrimitive(numberTag.getValue()); + } else if (tag instanceof StringTag) { + return new JsonPrimitive(((StringTag) tag).getValue()); + } else if (tag instanceof ByteArrayTag) { + final ByteArrayTag arrayTag = (ByteArrayTag) tag; + final JsonArray array = new JsonArray(); + for (final byte num : arrayTag.getValue()) { + array.add(num); + } + return array; + } else if (tag instanceof IntArrayTag) { + final IntArrayTag arrayTag = (IntArrayTag) tag; + final JsonArray array = new JsonArray(); + for (final int num : arrayTag.getValue()) { + array.add(num); + } + return array; + } else if (tag instanceof LongArrayTag) { + final LongArrayTag arrayTag = (LongArrayTag) tag; + final JsonArray array = new JsonArray(); + for (final long num : arrayTag.getValue()) { + array.add(num); + } + return array; + } + throw new IllegalArgumentException("Unhandled tag type " + tag.getClass().getSimpleName()); + } + + private static void convertCompoundTagEntry(final String key, final Tag tag, final JsonObject object) { + if ((key.equals("contents")) && tag instanceof CompoundTag) { + // Back to a UUID string + final CompoundTag showEntity = (CompoundTag) tag; + final Tag idTag = showEntity.get("id"); + if (idTag instanceof IntArrayTag) { + showEntity.remove("id"); + + final JsonObject convertedElement = (JsonObject) convertToJson(key, tag); + final UUID uuid = UUIDUtil.fromIntArray(((IntArrayTag) idTag).getValue()); + convertedElement.addProperty("id", uuid.toString()); + object.add(key, convertedElement); + return; + } + } + + // "":1 is a valid tag, but not a valid json component + object.add(key.isEmpty() ? "text" : key, convertToJson(key, tag)); + } + + private static void removeComponentType(final JsonObject object) { + final JsonElement type = object.remove("type"); + if (!type.isJsonPrimitive()) { + return; + } + + // Remove the other fields + final String typeString = type.getAsString(); + for (final Pair pair : COMPONENT_TYPES) { + if (!pair.key().equals(typeString)) { + object.remove(pair.value()); + } + } + } +} diff --git a/common/src/main/java/com/viaversion/viaversion/util/UUIDUtil.java b/common/src/main/java/com/viaversion/viaversion/util/UUIDUtil.java new file mode 100644 index 000000000..6c4ea54d2 --- /dev/null +++ b/common/src/main/java/com/viaversion/viaversion/util/UUIDUtil.java @@ -0,0 +1,47 @@ +/* + * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion + * Copyright (C) 2023 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viaversion.util; + +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class UUIDUtil { + + public static UUID fromIntArray(final int[] parts) { + if (parts.length != 4) { + return new UUID(0, 0); + } + return new UUID((long) parts[0] << 32 | (parts[1] & 0xFFFFFFFFL), (long) parts[2] << 32 | (parts[3] & 0xFFFFFFFFL)); + } + + public static int[] toIntArray(final UUID uuid) { + return toIntArray(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); + } + + public static int[] toIntArray(final long msb, final long lsb) { + return new int[]{(int) (msb >> 32), (int) msb, (int) (lsb >> 32), (int) lsb}; + } + + public static @Nullable UUID parseUUID(final String uuidString) { + try { + return UUID.fromString(uuidString); + } catch (final IllegalArgumentException e) { + return null; + } + } +}