//  [java]
    public void translate(GeyserSession session, InventoryTransactionPacket packet) {
        // Send book updates before opening inventories
        session.getBookEditCache().checkForSend();

        ItemMappings mappings = session.getItemMappings();

        switch (packet.getTransactionType()) {
            case NORMAL:
                if (packet.getActions().size() == 2) {
                    InventoryActionData worldAction = packet.getActions().get(0);
                    InventoryActionData containerAction = packet.getActions().get(1);
                    if (worldAction.getSource().getType() == InventorySource.Type.WORLD_INTERACTION
                            && worldAction.getSource().getFlag() == InventorySource.Flag.DROP_ITEM) {
                        if (session.getPlayerInventory().getHeldItemSlot() != containerAction.getSlot() ||
                                session.getPlayerInventory().getItemInHand().isEmpty()) {
                            return;
                        }

                        boolean dropAll = worldAction.getToItem().getCount() > 1;
                        ClientPlayerActionPacket dropAllPacket = new ClientPlayerActionPacket(
                                dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
                                BlockUtils.POSITION_ZERO,
                                BlockFace.DOWN
                        );
                        session.sendDownstreamPacket(dropAllPacket);

                        if (dropAll) {
                            session.getPlayerInventory().setItemInHand(GeyserItemStack.EMPTY);
                        } else {
                            session.getPlayerInventory().getItemInHand().sub(1);
                        }
                    }
                }
                break;
            case INVENTORY_MISMATCH:
                break;
            case ITEM_USE:
                switch (packet.getActionType()) {
                    case 0:
                        // Check to make sure the client isn't spamming interaction
                        // Based on Nukkit 1.0, with changes to ensure holding down still works
                        boolean hasAlreadyClicked = System.currentTimeMillis() - session.getLastInteractionTime() < 110.0 &&
                                packet.getBlockPosition().distanceSquared(session.getLastInteractionBlockPosition()) < 0.00001;
                        session.setLastInteractionBlockPosition(packet.getBlockPosition());
                        session.setLastInteractionPlayerPosition(session.getPlayerEntity().getPosition());
                        if (hasAlreadyClicked) {
                            break;
                        } else {
                            // Only update the interaction time if it's valid - that way holding down still works.
                            session.setLastInteractionTime(System.currentTimeMillis());
                        }

                        // Bedrock sends block interact code for a Java entity so we send entity code back to Java
                        if (session.getBlockMappings().isItemFrame(packet.getBlockRuntimeId())) {
                            Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
                            if (itemFrameEntity != null) {
                                int entityId = (int) itemFrameEntity.getEntityId();
                                Vector3f vector = packet.getClickPosition();
                                ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket(entityId,
                                        InteractAction.INTERACT, Hand.MAIN_HAND, session.isSneaking());
                                ClientPlayerInteractEntityPacket interactAtPacket = new ClientPlayerInteractEntityPacket(entityId,
                                        InteractAction.INTERACT_AT, vector.getX(), vector.getY(), vector.getZ(), Hand.MAIN_HAND, session.isSneaking());
                                session.sendDownstreamPacket(interactPacket);
                                session.sendDownstreamPacket(interactAtPacket);
                                break;
                            }
                        }

                        Vector3i blockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getBlockFace());
                        /*
                        Checks to ensure that the range will be accepted by the server.
                        "Not in range" doesn't refer to how far a vanilla client goes (that's a whole other mess),
                        but how much a server will accept from the client maximum
                         */
                        // CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch
                        Vector3f playerPosition = session.getPlayerEntity().getPosition();

                        // Adjust position for current eye height
                        switch (session.getPose()) {
                            case SNEAKING:
                                playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 1.27f), 0);
                                break;
                            case SWIMMING:
                            case FALL_FLYING: // Elytra
                            case SPIN_ATTACK: // Trident spin attack
                                playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.4f), 0);
                                break;
                            case SLEEPING:
                                playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.2f), 0);
                                break;
                        } // else, we don't have to modify the position

                        float diffX = playerPosition.getX() - packet.getBlockPosition().getX();
                        float diffY = playerPosition.getY() - packet.getBlockPosition().getY();
                        float diffZ = playerPosition.getZ() - packet.getBlockPosition().getZ();
                        if (((diffX * diffX) + (diffY * diffY) + (diffZ * diffZ)) >
                                (session.getGameMode().equals(GameMode.CREATIVE) ? CREATIVE_EYE_HEIGHT_PLACE_DISTANCE : SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE)) {
                            restoreCorrectBlock(session, blockPos, packet);
                            return;
                        }

                        // Vanilla check
                        if (!(session.getPlayerEntity().getPosition().sub(0, EntityType.PLAYER.getOffset(), 0)
                                .distanceSquared(packet.getBlockPosition().toFloat().add(0.5f, 0.5f, 0.5f)) < MAXIMUM_BLOCK_PLACING_DISTANCE)) {
                            // The client thinks that its blocks have been successfully placed. Restore the server's blocks instead.
                            restoreCorrectBlock(session, blockPos, packet);
                            return;
                        }
                        /*
                        Block place checks end - client is good to go
                         */

                        if (packet.getItemInHand() != null && session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) {
                            int blockState = session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition());
                            if (blockState == BlockStateValues.JAVA_WATER_ID) {
                                // Otherwise causes multiple mobs to spawn - just send a use item packet
                                // TODO when we fix mobile bucket rotation, use it for this, too
                                ClientPlayerUseItemPacket itemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
                                session.sendDownstreamPacket(itemPacket);
                                break;
                            }
                        }

                        ClientPlayerPlaceBlockPacket blockPacket = new ClientPlayerPlaceBlockPacket(
                                new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()),
                                BlockFace.values()[packet.getBlockFace()],
                                Hand.MAIN_HAND,
                                packet.getClickPosition().getX(), packet.getClickPosition().getY(), packet.getClickPosition().getZ(),
                                false);
                        session.sendDownstreamPacket(blockPacket);

                        if (packet.getItemInHand() != null) {
                            // Otherwise boats will not be able to be placed in survival and buckets won't work on mobile
                            if (session.getItemMappings().getBoatIds().contains(packet.getItemInHand().getId())) {
                                ClientPlayerUseItemPacket itemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
                                session.sendDownstreamPacket(itemPacket);
                            } else if (session.getItemMappings().getBucketIds().contains(packet.getItemInHand().getId())) {
                                // Let the server decide if the bucket item should change, not the client, and revert the changes the client made
                                InventorySlotPacket slotPacket = new InventorySlotPacket();
                                slotPacket.setContainerId(ContainerId.INVENTORY);
                                slotPacket.setSlot(packet.getHotbarSlot());
                                slotPacket.setItem(packet.getItemInHand());
                                session.sendUpstreamPacket(slotPacket);
                                // Don't send ClientPlayerUseItemPacket for powder snow buckets
                                if (packet.getItemInHand().getId() != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) {
                                    // Special check for crafting tables since clients don't send BLOCK_INTERACT when interacting
                                    int blockState = session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition());
                                    if (session.isSneaking() || blockState != BlockRegistries.JAVA_IDENTIFIERS.get("minecraft:crafting_table")) {
                                        // Delay the interaction in case the client doesn't intend to actually use the bucket
                                        // See BedrockActionTranslator.java
                                        session.setBucketScheduledFuture(session.scheduleInEventLoop(() -> {
                                            ClientPlayerUseItemPacket itemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
                                            session.sendDownstreamPacket(itemPacket);
                                        }, 5, TimeUnit.MILLISECONDS));
                                    }
                                }
                            }
                        }

                        if (packet.getActions().isEmpty()) {
                            if (session.getOpPermissionLevel() >= 2 && session.getGameMode() == GameMode.CREATIVE) {
                                // Otherwise insufficient permissions
                                int blockState = session.getBlockMappings().getJavaBlockState(packet.getBlockRuntimeId());
                                String blockName = BlockRegistries.JAVA_IDENTIFIERS.get().getOrDefault(blockState, "");
                                // In the future this can be used for structure blocks too, however not all elements
                                // are available in each GUI
                                if (blockName.contains("jigsaw")) {
                                    ContainerOpenPacket openPacket = new ContainerOpenPacket();
                                    openPacket.setBlockPosition(packet.getBlockPosition());
                                    openPacket.setId((byte) 1);
                                    openPacket.setType(ContainerType.JIGSAW_EDITOR);
                                    openPacket.setUniqueEntityId(-1);
                                    session.sendUpstreamPacket(openPacket);
                                }
                            }
                        }

                        ItemMapping handItem = mappings.getMapping(packet.getItemInHand());
                        if (handItem.isBlock()) {
                            session.setLastBlockPlacePosition(blockPos);
                            session.setLastBlockPlacedId(handItem.getJavaIdentifier());
                        }
                        session.setInteracting(true);
                        break;
                    case 1:
                        if (packet.getActions().size() == 1 && packet.getLegacySlots().size() > 0) {
                            InventoryActionData actionData = packet.getActions().get(0);
                            LegacySetItemSlotData slotData = packet.getLegacySlots().get(0);
                            if (slotData.getContainerId() == 6 && actionData.getToItem().getId() != 0) {
                                // The player is trying to swap out an armor piece that already has an item in it
                                // Java Edition does not allow this; let's revert it
                                session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory());
                            }
                        }

                        // Handled when sneaking
                        if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) {
                            break;
                        }

                        // Handled in ITEM_USE if the item is not milk
                        if (packet.getItemInHand() != null) {
                            if (session.getItemMappings().getBucketIds().contains(packet.getItemInHand().getId()) &&
                                    packet.getItemInHand().getId() != session.getItemMappings().getStoredItems().milkBucket().getBedrockId()) {
                                // Handled in case 0 if the item is not milk
                                break;
                            } else if (session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) {
                                // Handled in case 0
                                break;
                            }
                        }

                        ClientPlayerUseItemPacket useItemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
                        session.sendDownstreamPacket(useItemPacket);
                        break;
                    case 2:
                        int blockState = session.getGameMode() == GameMode.CREATIVE ?
                                session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition()) : session.getBreakingBlock();

                        session.setLastBlockPlacedId(null);
                        session.setLastBlockPlacePosition(null);

                        // Same deal with vanilla block placing as above.
                        // This is working out the distance using 3d Pythagoras and the extra value added to the Y is the sneaking height of a java player.
                        playerPosition = session.getPlayerEntity().getPosition();
                        Vector3f floatBlockPosition = packet.getBlockPosition().toFloat();
                        diffX = playerPosition.getX() - (floatBlockPosition.getX() + 0.5f);
                        diffY = (playerPosition.getY() - EntityType.PLAYER.getOffset()) - (floatBlockPosition.getY() + 0.5f) + 1.5f;
                        diffZ = playerPosition.getZ() - (floatBlockPosition.getZ() + 0.5f);
                        float distanceSquared = diffX * diffX + diffY * diffY + diffZ * diffZ;
                        if (distanceSquared > MAXIMUM_BLOCK_DESTROYING_DISTANCE) {
                            restoreCorrectBlock(session, packet.getBlockPosition(), packet);
                            return;
                        }

                        LevelEventPacket blockBreakPacket = new LevelEventPacket();
                        blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK);
                        blockBreakPacket.setPosition(packet.getBlockPosition().toFloat());
                        blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState));
                        session.sendUpstreamPacket(blockBreakPacket);
                        session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID);

                        Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
                        if (itemFrameEntity != null) {
                            ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) itemFrameEntity.getEntityId(),
                                    InteractAction.ATTACK, session.isSneaking());
                            session.sendDownstreamPacket(attackPacket);
                            break;
                        }

                        PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING;
                        Position pos = new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ());
                        ClientPlayerActionPacket breakPacket = new ClientPlayerActionPacket(action, pos, BlockFace.values()[packet.getBlockFace()]);
                        session.sendDownstreamPacket(breakPacket);
                        break;
                }
                break;
            case ITEM_RELEASE:
                if (packet.getActionType() == 0) {
                    // Followed to the Minecraft Protocol specification outlined at wiki.vg
                    ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO,
                            BlockFace.DOWN);
                    session.sendDownstreamPacket(releaseItemPacket);
                }
                break;
            case ITEM_USE_ON_ENTITY:
                Entity entity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId());
                if (entity == null)
                    return;

                //https://wiki.vg/Protocol#Interact_Entity
                switch (packet.getActionType()) {
                    case 0: //Interact
                        if (entity instanceof CommandBlockMinecartEntity) {
                            // The UI is handled client-side on Java Edition
                            // Ensure OP permission level and gamemode is appropriate
                            if (session.getOpPermissionLevel() < 2 || session.getGameMode() != GameMode.CREATIVE) return;
                            ContainerOpenPacket openPacket = new ContainerOpenPacket();
                            openPacket.setBlockPosition(Vector3i.ZERO);
                            openPacket.setId((byte) 1);
                            openPacket.setType(ContainerType.COMMAND_BLOCK);
                            openPacket.setUniqueEntityId(entity.getGeyserId());
                            session.sendUpstreamPacket(openPacket);
                            break;
                        }
                        Vector3f vector = packet.getClickPosition().sub(entity.getPosition());
                        ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId(),
                                InteractAction.INTERACT, Hand.MAIN_HAND, session.isSneaking());
                        ClientPlayerInteractEntityPacket interactAtPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId(),
                                InteractAction.INTERACT_AT, vector.getX(), vector.getY(), vector.getZ(), Hand.MAIN_HAND, session.isSneaking());
                        session.sendDownstreamPacket(interactPacket);
                        session.sendDownstreamPacket(interactAtPacket);

                        EntitySoundInteractionHandler.handleEntityInteraction(session, packet.getClickPosition(), entity);
                        break;
                    case 1: //Attack
                        if (entity.getEntityType() == EntityType.ENDER_DRAGON) {
                            // Redirects the attack to its body entity, this only happens when
                            // attacking the underbelly of the ender dragon
                            ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId() + 3,
                                    InteractAction.ATTACK, session.isSneaking());
                            session.sendDownstreamPacket(attackPacket);
                        } else {
                            ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId(),
                                    InteractAction.ATTACK, session.isSneaking());
                            session.sendDownstreamPacket(attackPacket);
                        }
                        break;
                }
                break;
        }
    }