diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java index 56b6ee7ac..c4375efba 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java @@ -42,16 +42,20 @@ public class StoredItemMappings { private final ItemMapping banner; private final ItemMapping barrier; private final int bowl; + private final int bucket; private final int chest; private final ItemMapping compass; private final ItemMapping crossbow; private final ItemMapping enchantedBook; private final ItemMapping fishingRod; private final int flintAndSteel; + private final int frogspawn; + private final int glassBottle; private final int goldenApple; private final int goldIngot; private final int ironIngot; private final int lead; + private final int lilyPad; private final ItemMapping milkBucket; private final int nameTag; private final ItemMapping powderSnowBucket; @@ -70,16 +74,20 @@ public class StoredItemMappings { this.banner = load(itemMappings, "white_banner"); // As of 1.17.10, all banners have the same Bedrock ID this.barrier = load(itemMappings, "barrier"); this.bowl = load(itemMappings, "bowl").getJavaId(); + this.bucket = load(itemMappings, "bucket").getBedrockId(); this.chest = load(itemMappings, "chest").getJavaId(); this.compass = load(itemMappings, "compass"); this.crossbow = load(itemMappings, "crossbow"); this.enchantedBook = load(itemMappings, "enchanted_book"); this.fishingRod = load(itemMappings, "fishing_rod"); this.flintAndSteel = load(itemMappings, "flint_and_steel").getJavaId(); + this.frogspawn = load(itemMappings, "frogspawn").getBedrockId(); + this.glassBottle = load(itemMappings, "glass_bottle").getBedrockId(); this.goldenApple = load(itemMappings, "golden_apple").getJavaId(); this.goldIngot = load(itemMappings, "gold_ingot").getJavaId(); this.ironIngot = load(itemMappings, "iron_ingot").getJavaId(); this.lead = load(itemMappings, "lead").getJavaId(); + this.lilyPad = load(itemMappings, "lily_pad").getBedrockId(); this.milkBucket = load(itemMappings, "milk_bucket"); this.nameTag = load(itemMappings, "name_tag").getJavaId(); this.powderSnowBucket = load(itemMappings, "powder_snow_bucket"); diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java index a9b3ffedc..adb81761f 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java @@ -44,6 +44,7 @@ import java.util.Locale; * Used for block entities if the Java block state contains Bedrock block information. */ public final class BlockStateValues { + private static final IntSet ALL_CAULDRONS = new IntOpenHashSet(); private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap(); private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap(); private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap(); @@ -193,6 +194,9 @@ public final class BlockStateValues { return; } + if (javaId.contains("cauldron")) { + ALL_CAULDRONS.add(javaBlockState); + } if (javaId.contains("_cauldron") && !javaId.contains("water_")) { NON_WATER_CAULDRONS.add(javaBlockState); } @@ -225,10 +229,19 @@ public final class BlockStateValues { * * @return if this Java block state is a non-empty non-water cauldron */ - public static boolean isCauldron(int state) { + public static boolean isNonWaterCauldron(int state) { return NON_WATER_CAULDRONS.contains(state); } + /** + * When using a bucket on a cauldron sending a ServerboundUseItemPacket can result in the liquid being placed. + * + * @return if this Java block state is a cauldron + */ + public static boolean isCauldron(int state) { + return ALL_CAULDRONS.contains(state); + } + /** * The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags * in Bedrock need the conditional information. diff --git a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java index 609647b2d..586e7d08b 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java @@ -72,6 +72,16 @@ public class BlockRegistries { */ public static final SimpleRegistry WATERLOGGED = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); + /** + * A registry containing all blockstates which are always interactive. + */ + public static final SimpleRegistry INTERACTIVE = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); + + /** + * A registry containing all blockstates which are interactive if the player has the may build permission. + */ + public static final SimpleRegistry INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); + static { BlockRegistryPopulator.populate(); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java index 25528a919..53c3e2310 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.registry.populator; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.common.collect.ImmutableMap; import com.nukkitx.nbt.*; import com.nukkitx.protocol.bedrock.v527.Bedrock_v527; @@ -355,6 +356,24 @@ public class BlockRegistryPopulator { BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0])); BLOCKS_JSON = blocksJson; + + JsonNode blockInteractionsJson; + try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource("mappings/interactions.json")) { + blockInteractionsJson = GeyserImpl.JSON_MAPPER.readTree(stream); + } catch (Exception e) { + throw new AssertionError("Unable to load Java block interaction mappings", e); + } + + BlockRegistries.INTERACTIVE.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("always_consumes"))); + BlockRegistries.INTERACTIVE_MAY_BUILD.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("requires_may_build"))); + } + + private static IntSet toBlockStateSet(ArrayNode node) { + IntSet blockStateSet = new IntOpenHashSet(node.size()); + for (JsonNode javaIdentifier : node) { + blockStateSet.add(BlockRegistries.JAVA_IDENTIFIERS.get().getInt(javaIdentifier.textValue())); + } + return blockStateSet; } private static NbtMap buildBedrockState(JsonNode node, int blockStateVersion, BiFunction statesMapper) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index f3d936b2e..eaeefbe56 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -164,6 +164,9 @@ public class ItemRegistryPopulator { } else if (identifier.equals("minecraft:empty_map") && damage == 2) { // Bedrock-only as its own item continue; + } else if (identifier.equals("minecraft:bordure_indented_banner_pattern") || identifier.equals("minecraft:field_masoned_banner_pattern")) { + // Bedrock-only banner patterns + continue; } StartGamePacket.ItemEntry entry = entries.get(identifier); int id = -1; diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 46f5eb1ca..705ee6416 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -433,11 +433,10 @@ public class GeyserSession implements GeyserConnection, CommandSender { private long lastInteractionTime; /** - * Stores a future interaction to place a bucket. Will be cancelled if the client instead intended to - * interact with a block. + * Stores whether the player intended to place a bucket. */ @Setter - private ScheduledFuture bucketScheduledFuture; + private boolean placedBucket; /** * Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. @@ -524,6 +523,12 @@ public class GeyserSession implements GeyserConnection, CommandSender { */ private ScheduledFuture tickThread = null; + /** + * Used to return the player to their original rotation after using an item in BedrockInventoryTransactionTranslator + */ + @Setter + private ScheduledFuture lookBackScheduledFuture = null; + private MinecraftProtocol protocol; public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java index 94760b66c..9ae3300cd 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java @@ -62,7 +62,7 @@ public interface BedrockOnlyBlockEntity extends RequiresBlockState { return FlowerPotBlockEntityTranslator.getTag(session, blockState, position); } else if (PistonBlockEntityTranslator.isBlock(blockState)) { return PistonBlockEntityTranslator.getTag(blockState, position); - } else if (BlockStateValues.isCauldron(blockState)) { + } else if (BlockStateValues.isNonWaterCauldron(blockState)) { // As of 1.18.30: this is required to make rendering not look weird on chunk load (lava and snow cauldrons look dim) return NbtMap.builder() .putString("id", "Cauldron") diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 243b1cede..d9603ae8e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -33,6 +33,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*; +import com.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; @@ -44,6 +45,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; @@ -53,6 +55,8 @@ import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; +import org.geysermc.geyser.translator.inventory.item.ItemTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.BlockUtils; @@ -170,6 +174,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator - playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 1.27f), 0); - case SWIMMING, - FALL_FLYING, // Elytra - SPIN_ATTACK -> // Trident spin attack - playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.4f), 0); - case SLEEPING -> - playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.2f), 0); - } // else, we don't have to modify the position + playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - getEyeHeight(session)); boolean creative = session.getGameMode() == GameMode.CREATIVE; @@ -255,9 +253,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { - ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); - session.sendDownstreamPacket(itemPacket); - }, 5, TimeUnit.MILLISECONDS)); + if (itemId != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) { + if (!session.isSneaking() && BlockStateValues.isCauldron(blockState)) { + // ServerboundUseItemPacket is not sent for cauldrons and buckets + return; } + session.setPlacedBucket(useItem(session, packet, blockState)); + } else { + session.setPlacedBucket(true); } } } @@ -320,6 +316,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { + if (isIncorrectHeldItem(session, packet)) { + InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot())); + break; + } + // Handled when sneaking if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) { break; @@ -334,6 +335,9 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator 1) { + if (packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().bucket() || + packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().glassBottle()) { + // Using a stack of buckets or glass bottles will result in an item being added to the first empty slot. + // We need to revert the item in case the interaction fails. The order goes from left to right in the + // hotbar. Then left to right and top to bottom in the inventory. + for (int i = 0; i < 36; i++) { + int slot = i; + if (i < 9) { + slot = playerInventory.getOffsetForHotbar(slot); + } + if (playerInventory.getItem(slot).isEmpty()) { + InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, playerInventory, slot); + break; + } + } + } + } + // Check if the player is interacting with a block + if (!session.isSneaking()) { + if (BlockRegistries.INTERACTIVE.get().contains(blockState)) { + return false; + } + + boolean mayBuild = session.getGameMode() == GameMode.SURVIVAL || session.getGameMode() == GameMode.CREATIVE; + if (mayBuild && BlockRegistries.INTERACTIVE_MAY_BUILD.get().contains(blockState)) { + return false; + } + } + + Vector3f target = packet.getBlockPosition().toFloat().add(packet.getClickPosition()); + lookAt(session, target); + + ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); + session.sendDownstreamPacket(itemPacket); + return true; + } + + /** + * Determine the rotation necessary to activate this transaction. + * + * The position between the intended click position and the player can be determined with two triangles. + * First, we compute the difference of the X and Z coordinates: + * + * Player position (0, 0) + * | + * | + * | + * |_____________ Intended target (-3, 2) + * + * We then use the Pythagorean Theorem to find the direct line (hypotenuse) on the XZ plane. Finding the angle of the + * triangle from there, closest to the player, gives us our yaw rotation value + * Then doing the same using the new XZ distance and Y difference, we can find the direct line of sight from the + * player to the intended target, and the pitch rotation value. We can then send the necessary packets to update + * the player's rotation. + * + * @param session the Geyser Session + * @param target the position to look at + */ + private void lookAt(GeyserSession session, Vector3f target) { + // Use the bounding box's position since we need the player's position seen by the Java server + Vector3d playerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter(); + float xDiff = (float) (target.getX() - playerPosition.getX()); + float yDiff = (float) (target.getY() - (playerPosition.getY() + getEyeHeight(session))); + float zDiff = (float) (target.getZ() - playerPosition.getZ()); + + // First triangle on the XZ plane + float yaw = (float) -Math.toDegrees(Math.atan2(xDiff, zDiff)); + // Second triangle on the Y axis using the hypotenuse of the first triangle as a side + double xzHypot = Math.sqrt(xDiff * xDiff + zDiff * zDiff); + float pitch = (float) -Math.toDegrees(Math.atan2(yDiff, xzHypot)); + + SessionPlayerEntity entity = session.getPlayerEntity(); + ServerboundMovePlayerPosRotPacket returnPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), entity.getYaw(), entity.getPitch()); + // This matches Java edition behavior + ServerboundMovePlayerPosRotPacket movementPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), yaw, pitch); + session.sendDownstreamPacket(movementPacket); + + if (session.getLookBackScheduledFuture() != null) { + session.getLookBackScheduledFuture().cancel(false); + } + if (Math.abs(entity.getYaw() - yaw) > 1f || Math.abs(entity.getPitch() - pitch) > 1f) { + session.setLookBackScheduledFuture(session.scheduleInEventLoop(() -> { + Vector3d newPlayerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter(); + if (!newPlayerPosition.equals(playerPosition) || entity.getYaw() != returnPacket.getYaw() || entity.getPitch() != returnPacket.getPitch()) { + // The player moved/rotated so there is no need to change their rotation back + return; + } + session.sendDownstreamPacket(returnPacket); + }, 150, TimeUnit.MILLISECONDS)); + } + } + + private float getEyeHeight(GeyserSession session) { + return switch (session.getPose()) { + case SNEAKING -> 1.27f; + case SWIMMING, + FALL_FLYING, // Elytra + SPIN_ATTACK -> 0.4f; // Trident spin attack + case SLEEPING -> 0.2f; + default -> EntityDefinitions.PLAYER.offset(); + }; } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java index fe519c329..dd7a1788c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java @@ -136,14 +136,6 @@ public class BedrockActionTranslator extends PacketTranslator> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)), javaId @@ -183,7 +183,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator