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));
- });
}