Geyser/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransaction...

419 lines
27 KiB
Java

/*
* Copyright (c) 2019-2021 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.connector.network.translators.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
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.data.game.world.block.BlockFace;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPlaceBlockPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerUseItemPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.inventory.*;
import com.nukkitx.protocol.bedrock.packet.*;
import org.geysermc.connector.entity.CommandBlockMinecartEntity;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.sound.EntitySoundInteractionHandler;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import org.geysermc.connector.registry.BlockRegistries;
import org.geysermc.connector.registry.type.ItemMapping;
import org.geysermc.connector.registry.type.ItemMappings;
import org.geysermc.connector.utils.BlockUtils;
import java.util.concurrent.TimeUnit;
/**
* BedrockInventoryTransactionTranslator handles most interactions between the client and the world,
* or the client and their inventory.
*/
@Translator(packet = InventoryTransactionPacket.class)
public class BedrockInventoryTransactionTranslator extends PacketTranslator<InventoryTransactionPacket> {
private static final float MAXIMUM_BLOCK_PLACING_DISTANCE = 64f;
private static final int CREATIVE_EYE_HEIGHT_PLACE_DISTANCE = 49;
private static final int SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE = 36;
private static final float MAXIMUM_BLOCK_DESTROYING_DISTANCE = 36f;
@Override
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
*/
// Blocks cannot be placed or destroyed outside of the world border
if (!session.getWorldBorder().isInsideBorderBoundaries()) {
restoreCorrectBlock(session, blockPos, packet);
return;
}
// 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);
case SWIMMING,
FALL_FLYING, // Elytra
SPIN_ATTACK -> // Trident spin attack
playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.4f), 0);
case SLEEPING ->
playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.2f), 0);
} // 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
if (session.getBlockMappings().getJigsawStateIds().contains(packet.getBlockRuntimeId())) {
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);
}
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);
}
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.
if (!session.getWorldBorder().isInsideBorderBoundaries()) {
restoreCorrectBlock(session, packet.getBlockPosition(), packet);
return;
}
// 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.
Vector3f playerPosition = session.getPlayerEntity().getPosition();
Vector3f floatBlockPosition = packet.getBlockPosition().toFloat();
float diffX = playerPosition.getX() - (floatBlockPosition.getX() + 0.5f);
float diffY = (playerPosition.getY() - EntityType.PLAYER.getOffset()) - (floatBlockPosition.getY() + 0.5f) + 1.5f;
float 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;
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;
}
}
/**
* Restore the correct block state from the server without updating the chunk cache.
*
* @param session the session of the Bedrock client
* @param blockPos the block position to restore
*/
private void restoreCorrectBlock(GeyserSession session, Vector3i blockPos, InventoryTransactionPacket packet) {
int javaBlockState = session.getConnector().getWorldManager().getBlockAt(session, blockPos);
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(blockPos);
updateBlockPacket.setRuntimeId(session.getBlockMappings().getBedrockBlockId(javaBlockState));
updateBlockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
session.sendUpstreamPacket(updateBlockPacket);
UpdateBlockPacket updateWaterPacket = new UpdateBlockPacket();
updateWaterPacket.setDataLayer(1);
updateWaterPacket.setBlockPosition(blockPos);
updateWaterPacket.setRuntimeId(BlockRegistries.WATERLOGGED.get().contains(javaBlockState) ? session.getBlockMappings().getBedrockWaterId() : session.getBlockMappings().getBedrockAirId());
updateWaterPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
session.sendUpstreamPacket(updateWaterPacket);
// Reset the item in hand to prevent "missing" blocks
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.INVENTORY);
slotPacket.setSlot(packet.getHotbarSlot());
slotPacket.setItem(packet.getItemInHand());
session.sendUpstreamPacket(slotPacket);
}
}