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 new file mode 100644 index 000000000..008e68df8 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.parser; + +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; +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.type.Item; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.level.block.entity.SkullBlockEntityTranslator; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +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.ItemEnchantments; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +@SuppressWarnings("unchecked") +public class ItemStackParser { + private static final ItemStack AIR = new ItemStack(Items.AIR_ID); + private static final Map, DataComponentParser> PARSERS = new Reference2ObjectOpenHashMap<>(); + + private static void register(DataComponentType component, Class rawClass, DataComponentParser parser) { + if (PARSERS.containsKey(component)) { + throw new IllegalStateException("Duplicate data component parser registered for " + component); + } + PARSERS.put(component, parser); + } + + private static void registerSimple(DataComponentType component, Class rawClass, Function parser) { + register(component, rawClass, (session, raw) -> parser.apply(raw)); + } + + private static void registerSimple(DataComponentType component, Class parsedClass) { + registerSimple(component, parsedClass, Function.identity()); + } + + private static void registerInstance(DataComponentType component, Parsed instance) { + register(component, Object.class, (session, o) -> instance); + } + + 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] + 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.DYED_COLOR, Integer.class); + registerSimple(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, Boolean.class); + registerInstance(DataComponentTypes.ENCHANTMENTS, new ItemEnchantments(new Int2IntOpenHashMap())); + registerSimple(DataComponentTypes.ITEM_MODEL, String.class, MinecraftKey::key); + registerSimple(DataComponentTypes.MAP_COLOR, Integer.class); + registerSimple(DataComponentTypes.PROFILE, NbtMap.class, SkullBlockEntityTranslator::parseResolvableProfile); + + register(DataComponentTypes.POTION_CONTENTS, NbtMap.class, (session, map) -> { + // TODO + return Optional.ofNullable(tag.getString("potion")).map(Potion::getByJavaIdentifier); + }); + } + + private static void parseDataComponent(GeyserSession session, DataComponents patch, DataComponentType type, + DataComponentParser parser, Object raw) { + try { + patch.put((DataComponentType) type, parser.parse(session, (Raw) raw)); + } catch (ClassCastException exception) { + GeyserImpl.getInstance().getLogger().error("Received incorrect object type for component " + type + "!", exception); + } catch (Exception exception) { + GeyserImpl.getInstance().getLogger().error("Failed to parse component" + type + " from " + raw + "!", exception); + } + } + + public static @Nullable DataComponents parseDataComponentPatch(GeyserSession session, @Nullable NbtMap map) { + if (map == null || map.isEmpty()) { + return null; + } + + DataComponents patch = new DataComponents(new Reference2ObjectOpenHashMap<>()); + try { + for (Map.Entry patchEntry : map.entrySet()) { + String rawType = patchEntry.getKey(); + // When a component starts with a '!', indicates removal of the component from the default component set + boolean removal = rawType.startsWith("!"); + if (removal) { + rawType = rawType.substring(1); + } + + DataComponentType type = DataComponentTypes.fromKey(MinecraftKey.key(rawType)); + if (type == null) { + GeyserImpl.getInstance().getLogger().warning("Received unknown data component " + rawType + " in NBT data component patch: " + map); + } else if (removal) { + // Removals are easy, we don't have to parse anything + patch.put(type, null); + } else { + DataComponentParser parser = PARSERS.get(type); + if (parser != null) { + parseDataComponent(session, patch, type, parser, patchEntry.getValue()); + } else { + GeyserImpl.getInstance().getLogger().debug("Ignoring data component " + type + " whilst parsing NBT patch because there is no parser registered for it"); + } + } + } + } catch (Exception exception) { + // TODO + } + } + + 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; + } + + int id = item.javaId(); + int count = map.getInt("count"); + DataComponents patch = parseDataComponentPatch(session, map); + return new ItemStack(id, count, patch); + } catch (Exception exception) { + // TODO + } + } + + @FunctionalInterface + private interface DataComponentParser { + + Parsed parse(GeyserSession session, Raw raw) throws Exception; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/GameRule.java b/core/src/main/java/org/geysermc/geyser/level/GameRule.java index 8a1af095a..a77d5f09c 100644 --- a/core/src/main/java/org/geysermc/geyser/level/GameRule.java +++ b/core/src/main/java/org/geysermc/geyser/level/GameRule.java @@ -68,7 +68,12 @@ public enum GameRule { SPAWNRADIUS("spawnRadius", 10), SPECTATORSGENERATECHUNKS("spectatorsGenerateChunks", true), // JE only UNIVERSALANGER("universalAnger", false), - LOCATORBAR("locatorBar", true); + LOCATORBAR("locatorBar", true), + ALLOWENTERINGNETHERUSINGPORTALS("allowEnteringNetherUsingPortals", true), // JE only + COMMANDBLOCKSENABLED("commandBlocksEnabled", true), + PVP("pvp", true), + SPAWNMONSTERS("spawnMonsters", true), + SPAWNERBLOCKSENABLED("spawnerBlocksEnabled", true); public static final GameRule[] VALUES = values(); diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/CopperBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/CopperBlockEntityTranslator.java index bc6a91eff..15e699f6c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/CopperBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/CopperBlockEntityTranslator.java @@ -38,7 +38,7 @@ public class CopperBlockEntityTranslator extends BlockEntityTranslator implement @Override public void translateTag(GeyserSession session, NbtMapBuilder bedrockNbt, NbtMap javaNbt, BlockState blockState) { // Copper golem poses are set through block states on Java and through NBT on bedrock - bedrockNbt.putBoolean("isMovable", true) + bedrockNbt.putBoolean("isMovable", false) .putInt("Pose", translateCopperPose(blockState.getValue(Properties.COPPER_GOLEM_POSE))); } 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 new file mode 100644 index 000000000..e440a02df --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/ShelfBlockEntityTranslator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.translator.level.block.entity; + +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.session.GeyserSession; + +public class ShelfBlockEntityTranslator extends BlockEntityTranslator { + + @Override + public void translateTag(GeyserSession session, NbtMapBuilder bedrockNbt, NbtMap javaNbt, BlockState blockState) { + + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java index 7098de84c..0783746fb 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java @@ -85,7 +85,7 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements .toList(); } - private static ResolvableProfile parseResolvableProfile(NbtMap profile) { + public static ResolvableProfile parseResolvableProfile(NbtMap profile) { UUID uuid = parseUUID(profile.getIntArray("id", null)); String name = profile.getString("name", null); List properties = parseProperties(profile.getList("properties", NbtType.COMPOUND, null));