diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java b/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java index 316253e3e..bc880ef85 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java @@ -25,11 +25,22 @@ package org.geysermc.geyser.item.hashing.data; +import java.util.Locale; + // Ordered and named by Java ID public enum FireworkExplosionShape { SMALL_BALL, LARGE_BALL, STAR, CREEPER, - BURST + BURST; + + public static FireworkExplosionShape fromJavaIdentifier(String identifier) { + for (FireworkExplosionShape shape : values()) { + if (shape.name().toLowerCase(Locale.ROOT).equals(identifier)) { + return shape; + } + } + return null; + } } diff --git a/core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java b/core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java index 008e68df8..13d6b732c 100644 --- a/core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java +++ b/core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java @@ -25,35 +25,60 @@ package org.geysermc.geyser.item.parser; +import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.item.DyeColor; import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.hashing.data.FireworkExplosionShape; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.translator.item.BedrockItemBuilder; +import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.translator.level.block.entity.SkullBlockEntityTranslator; import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.CustomModelData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Fireworks; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.Function; +/** + * Utility class to parse an item stack, or a data component patch, from NBT data. + * + *

This class does NOT parse all possible data components in a data component patch, only those that + * can visually change the way an item looks. This class should/is usually used for parsing block entity NBT data, + * such as for vault or shelf block entities.

+ * + *

Be sure to update this class for Java updates!

+ */ +// Lots of unchecked casting happens here. It should all be handled properly. +// TODO only log somethings once (like was done in vault translator) @SuppressWarnings("unchecked") public class ItemStackParser { - private static final ItemStack AIR = new ItemStack(Items.AIR_ID); private static final Map, DataComponentParser> PARSERS = new Reference2ObjectOpenHashMap<>(); + // We need the rawClass parameter here because the Raw type can't be inferred from the parser alone private static void register(DataComponentType component, Class rawClass, DataComponentParser parser) { if (PARSERS.containsKey(component)) { throw new IllegalStateException("Duplicate data component parser registered for " + component); @@ -69,41 +94,96 @@ public class ItemStackParser { registerSimple(component, parsedClass, Function.identity()); } - private static void registerInstance(DataComponentType component, Parsed instance) { - register(component, Object.class, (session, o) -> instance); + private static int javaItemIdentifierToNetworkId(String identifier) { + if (identifier == null || identifier.isEmpty()) { + return Items.AIR_ID; + } + + Item item = Registries.JAVA_ITEM_IDENTIFIERS.get(identifier); + if (item == null) { + GeyserImpl.getInstance().getLogger().warning("Received unknown item ID " + identifier + " whilst parsing NBT item stack!"); + return Items.AIR_ID; + } + return item.javaId(); + } + + private static ItemEnchantments parseEnchantments(GeyserSession session, NbtMap map) { + Int2IntMap enchantments = new Int2IntOpenHashMap(map.size()); + for (Map.Entry entry : map.entrySet()) { + enchantments.put(JavaRegistries.ENCHANTMENT.networkId(session, MinecraftKey.key(entry.getKey())), (int) entry.getValue()); + } + return new ItemEnchantments(enchantments); } static { - // TODO check this again - // banner patterns [] - // base color [X] - // charged projectiles [X] - // custom model data [] - // dyed color [X] - // enchantment glint override [X] - // enchantments [X] - // firework explosion [] - // item model [X] - // map color [X] - // pot decorations [] - // profile [X] + // The various ignored null-warnings are for things that should never be null as they shouldn't be missing from the data component + // If they are null an exception will be thrown, but this will be caught with an error message logged + + register(DataComponentTypes.BANNER_PATTERNS, List.class, (session, raw) -> { + List casted = (List) raw; + List layers = new ArrayList<>(); + for (NbtMap layer : casted) { + DyeColor colour = DyeColor.getByJavaIdentifier(layer.getString("color")); + + // Patterns can be an ID or inline + Object pattern = layer.get("pattern"); + Holder patternHolder; + if (pattern instanceof String id) { + patternHolder = Holder.ofId(JavaRegistries.BANNER_PATTERN.networkId(session, MinecraftKey.key(id))); + } else { + NbtMap inline = (NbtMap) pattern; + Key assetId = MinecraftKey.key(inline.getString("asset_id")); + String translationKey = inline.getString("translation_key"); + patternHolder = Holder.ofCustom(new BannerPatternLayer.BannerPattern(assetId, translationKey)); + } + layers.add(new BannerPatternLayer(patternHolder, colour.ordinal())); + } + return layers; + }); registerSimple(DataComponentTypes.BASE_COLOR, String.class, raw -> DyeColor.getByJavaIdentifier(raw).ordinal()); register(DataComponentTypes.CHARGED_PROJECTILES, List.class, (session, projectiles) -> projectiles.stream() .map(object -> parseItemStack(session, (NbtMap) object)) .toList()); - + registerSimple(DataComponentTypes.CUSTOM_MODEL_DATA, NbtMap.class, raw -> { + List floats = raw.getList("floats", NbtType.FLOAT); + List flags = raw.getList("flags", NbtType.BYTE).stream().map(b -> b != 0).toList(); + List strings = raw.getList("strings", NbtType.STRING); + List colours = raw.getList("colors", NbtType.INT); + return new CustomModelData(floats, flags, strings, colours); + }); registerSimple(DataComponentTypes.DYED_COLOR, Integer.class); registerSimple(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, Boolean.class); - registerInstance(DataComponentTypes.ENCHANTMENTS, new ItemEnchantments(new Int2IntOpenHashMap())); + register(DataComponentTypes.ENCHANTMENTS, NbtMap.class, ItemStackParser::parseEnchantments); + registerSimple(DataComponentTypes.FIREWORK_EXPLOSION, NbtMap.class, raw -> { + FireworkExplosionShape shape = FireworkExplosionShape.fromJavaIdentifier(raw.getString("shape")); + List colours = raw.getList("colors", NbtType.INT); + List fadeColours = raw.getList("fade_colors", NbtType.INT); + boolean hasTrail = raw.getBoolean("has_trail"); + boolean hasTwinkle = raw.getBoolean("has_twinkle"); + return new Fireworks.FireworkExplosion(shape.ordinal(), + colours.stream() + .mapToInt(i -> i) // We need to do this because MCPL wants an int[] array + .toArray(), + fadeColours.stream() + .mapToInt(i -> i) + .toArray(), + hasTrail, hasTwinkle); + }); registerSimple(DataComponentTypes.ITEM_MODEL, String.class, MinecraftKey::key); registerSimple(DataComponentTypes.MAP_COLOR, Integer.class); - registerSimple(DataComponentTypes.PROFILE, NbtMap.class, SkullBlockEntityTranslator::parseResolvableProfile); - + registerSimple(DataComponentTypes.POT_DECORATIONS, List.class, list -> list.stream() + .map(item -> javaItemIdentifierToNetworkId((String) item)) + .toList()); register(DataComponentTypes.POTION_CONTENTS, NbtMap.class, (session, map) -> { - // TODO - return Optional.ofNullable(tag.getString("potion")).map(Potion::getByJavaIdentifier); + Potion potion = Potion.getByJavaIdentifier(map.getString("potion")); + int customColour = map.getInt("custom_color", -1); + // Not reading custom effects + String customName = map.getString("custom_name", null); + return new PotionContents(potion == null ? -1 : potion.ordinal(), customColour, List.of(), customName); }); + registerSimple(DataComponentTypes.PROFILE, NbtMap.class, SkullBlockEntityTranslator::parseResolvableProfile); + register(DataComponentTypes.STORED_ENCHANTMENTS, NbtMap.class, ItemStackParser::parseEnchantments); } private static void parseDataComponent(GeyserSession session, DataComponents patch, DataComponentType type, @@ -148,25 +228,39 @@ public class ItemStackParser { } } } catch (Exception exception) { - // TODO + GeyserImpl.getInstance().getLogger().error("Failed to parse data component patch from NBT data!", exception); } + + return patch; } - public static ItemStack parseItemStack(GeyserSession session, NbtMap map) { - try { - Item item = Registries.JAVA_ITEM_IDENTIFIERS.get(map.getString("id")); - if (item == null) { - GeyserImpl.getInstance().getLogger().warning("Unknown item " + map.getString("id") + " whilst trying to parse NBT item stack!"); - return AIR; - } + public static ItemStack parseItemStack(GeyserSession session, @Nullable NbtMap map) { + if (map == null) { + return new ItemStack(Items.AIR_ID); + } - int id = item.javaId(); + try { + int id = javaItemIdentifierToNetworkId(map.getString("id")); int count = map.getInt("count"); - DataComponents patch = parseDataComponentPatch(session, map); + DataComponents patch = parseDataComponentPatch(session, map.getCompound("components")); return new ItemStack(id, count, patch); } catch (Exception exception) { - // TODO + GeyserImpl.getInstance().getLogger().error("Failed to parse item stack from NBT data!", exception); } + return new ItemStack(Items.AIR_ID); + } + + /** + * Shorthand method for calling the following methods: + * + *
    + *
  • {@link ItemStackParser#parseItemStack(GeyserSession, NbtMap)}
  • + *
  • {@link ItemTranslator#translateToBedrock(GeyserSession, ItemStack)}
  • + *
  • {@link BedrockItemBuilder#itemDataToNbt(ItemData)}
  • + *
+ */ + public static NbtMapBuilder javaItemStackToBedrock(GeyserSession session, @Nullable NbtMap map) { + return BedrockItemBuilder.itemDataToNbt(ItemTranslator.translateToBedrock(session, parseItemStack(session, map))); } @FunctionalInterface diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java b/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java index 2f51c0007..645f1a70d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java @@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.registry.type.ItemMapping; import java.util.ArrayList; @@ -142,6 +143,17 @@ public final class BedrockItemBuilder { return builder.build(); } + /** + * Creates item NBT to nest within NBT with name, count, damage, and tag set. + */ + public static NbtMapBuilder itemDataToNbt(ItemData data) { + NbtMapBuilder builder = BedrockItemBuilder.createItemNbt(data.getDefinition().getIdentifier(), data.getCount(), data.getDamage()); + if (data.getTag() != null) { + builder.putCompound("tag", data.getTag()); + } + return builder; + } + /** * Creates item NBT to nest within NBT with name, count, and damage set. */ diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/ShelfBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/ShelfBlockEntityTranslator.java index e440a02df..ff01ecbd5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/ShelfBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/ShelfBlockEntityTranslator.java @@ -27,13 +27,23 @@ package org.geysermc.geyser.translator.level.block.entity; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; +import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.item.parser.ItemStackParser; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType; +import java.util.Comparator; + +@BlockEntity(type = BlockEntityType.SHELF) public class ShelfBlockEntityTranslator extends BlockEntityTranslator { @Override public void translateTag(GeyserSession session, NbtMapBuilder bedrockNbt, NbtMap javaNbt, BlockState blockState) { - + // We can't translate align_items_to_bottom, I think :( + bedrockNbt.putList("Items", NbtType.COMPOUND, javaNbt.getList("Items", NbtType.COMPOUND).stream() + .sorted(Comparator.comparingInt(stack -> stack.getByte("Slot"))) + .map(stack -> ItemStackParser.javaItemStackToBedrock(session, stack).build()) + .toList()); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/VaultBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/VaultBlockEntityTranslator.java index bebea2f22..46531b9b8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/VaultBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/VaultBlockEntityTranslator.java @@ -25,39 +25,23 @@ package org.geysermc.geyser.translator.level.block.entity; -import it.unimi.dsi.fastutil.ints.Int2IntMap; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongList; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; -import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; -import org.cloudburstmc.protocol.common.util.TriConsumer; import org.geysermc.geyser.entity.type.player.PlayerEntity; -import org.geysermc.geyser.inventory.item.Potion; +import org.geysermc.geyser.item.parser.ItemStackParser; import org.geysermc.geyser.level.block.type.BlockState; -import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.session.cache.registry.JavaRegistries; -import org.geysermc.geyser.translator.item.BedrockItemBuilder; -import org.geysermc.geyser.translator.item.ItemTranslator; -import org.geysermc.geyser.util.MinecraftKey; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Optional; import java.util.UUID; @BlockEntity(type = BlockEntityType.VAULT) public class VaultBlockEntityTranslator extends BlockEntityTranslator { - private static boolean loggedComponentTranslationFailure = false; // Bedrock 1.21 does not send the position nor ID in the tag. @Override @@ -72,44 +56,7 @@ public class VaultBlockEntityTranslator extends BlockEntityTranslator { @Override public void translateTag(GeyserSession session, NbtMapBuilder bedrockNbt, NbtMap javaNbt, BlockState blockState) { NbtMap sharedData = javaNbt.getCompound("shared_data"); - - NbtMap item = sharedData.getCompound("display_item"); - ItemMapping mapping = session.getItemMappings().getMapping(item.getString("id")); - if (mapping == null) { - bedrockNbt.putCompound("display_item", NbtMap.builder() - .putByte("Count", (byte) 0) - .putShort("Damage", (short) 0) - .putString("Name", "") - .putByte("WasPickedUp", (byte) 0).build()); - } else { - int count = item.getInt("count"); - NbtMap componentsTag = item.getCompound("components"); - NbtMapBuilder itemAsNbt; - if (!componentsTag.isEmpty()) { - DataComponents components = new DataComponents(new HashMap<>()); - for (Map.Entry entry : componentsTag.entrySet()) { - var consumer = DATA_COMPONENT_DECODERS.get(entry.getKey()); - if (consumer != null) { - try { - consumer.accept(session, (NbtMap) entry.getValue(), components); - } catch (RuntimeException exception) { - if (!loggedComponentTranslationFailure) { - session.getGeyser().getLogger().warning("Failed to translate vault item component data for " + entry.getKey() + "! Did the component structure change?"); - loggedComponentTranslationFailure = true; - } - } - } - } - ItemData bedrockItem = ItemTranslator.translateToBedrock(session, mapping.getJavaItem(), mapping, count, components).build(); - itemAsNbt = BedrockItemBuilder.createItemNbt(mapping, bedrockItem.getCount(), bedrockItem.getDamage()); - if (bedrockItem.getTag() != null) { - itemAsNbt.putCompound("tag", bedrockItem.getTag()); - } - } else { - itemAsNbt = BedrockItemBuilder.createItemNbt(mapping, count, mapping.getBedrockData()); - } - bedrockNbt.putCompound("display_item", itemAsNbt.build()); - } + bedrockNbt.putCompound("display_item", ItemStackParser.javaItemStackToBedrock(session, sharedData.getCompound("display_item")).build()); List connectedPlayers = sharedData.getList("connected_players", NbtType.INT_ARRAY); LongList bedrockPlayers = new LongArrayList(connectedPlayers.size()); @@ -132,24 +79,8 @@ public class VaultBlockEntityTranslator extends BlockEntityTranslator { } // From ViaVersion! thank u!! + // TODO code dup private static UUID uuidFromIntArray(int[] parts) { return new UUID((long) parts[0] << 32 | (parts[1] & 0xFFFFFFFFL), (long) parts[2] << 32 | (parts[3] & 0xFFFFFFFFL)); } - - // This might be easier to maintain in the long run so items don't have two translate methods. - // Also, it's not out of the question that block entities get the data component treatment, likely rendering this useless. - // The goal is to just translate the basics so clients know what potion is roughly present, and that any enchantment even exists. - private static final Map> DATA_COMPONENT_DECODERS = Map.of( - "minecraft:potion_contents", (session, tag, components) -> { - // Can only translate built-in potions, potions with custom colours don't work - Optional.ofNullable(tag.getString("potion")).map(Potion::getByJavaIdentifier) - .ifPresent(potion -> components.put(DataComponentTypes.POTION_CONTENTS, potion.toComponent())); - }, - "minecraft:enchantments", (session, tag, components) -> { // Enchanted books already have glint. Translating them doesn't matter. - Int2IntMap enchantments = new Int2IntOpenHashMap(tag.size()); - for (Map.Entry entry : tag.entrySet()) { - enchantments.put(JavaRegistries.ENCHANTMENT.networkId(session, MinecraftKey.key(entry.getKey())), (int) entry.getValue()); - } - components.put(DataComponentTypes.ENCHANTMENTS, new ItemEnchantments(enchantments)); - }); }