3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2024-12-27 16:40:14 +01:00

Fix some item interactions (#3083)

* Remove Bedrock only banner patterns from the creative inventory

* Add sound for tadpole bucket

* Fix lily pad and frogspawn placing on mobile/single stacks

* Workaround? Fix? for bucket usage on mobile

* Simplify math and update position+rotation whenever ServerboundUseItemPacket is sent

* Rotate the player back after using an item and fix glass bottles

* ITEM_USE actionType 1 does not need the rotation fix

Increase delay for look back

* Add some checks

* Prevent buckets and spawn eggs from being unintentionally placed when interacting with special blocks

As of 1.19 Bedrock no longer sends a PlayerActionPacket with action=BLOCK_INTERACT. Bedrock now sends action=ITEM_USE_ON_START before and action=ITEM_USE_ON_STOP after using an item on a block. However, this is not useful as it is sent for all block interactions.

* Fix inventory transactions being rejected after restoreCorrectBlock

The held item's netId is always 0 in the InventoryTransactionPacket.

* Touch ups

* Fix lookAt for different poses and sneaking + cauldron + bucket interactions

Fix boat items being desynced when placing them very close to collision
Fix bottles being desynced when tapping above water

Resend the held item if we do encounter a desync

* Avoid getting blockstate twice and fix comment

* Use generated interaction data

* Fix glass bottles being double filled and phantom water bottles/water buckets

* Don't update the entire inventory on useItem

* Use Geyser's inventory copy for check

* Use ItemTranslator#getBedrockItemMapping to avoid NBT translation

* mappings

Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com>
Dieser Commit ist enthalten in:
David Choo 2022-06-24 16:48:28 -04:00 committet von GitHub
Ursprung 60327339d6
Commit 9ea22042eb
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
13 geänderte Dateien mit 239 neuen und 59 gelöschten Zeilen

Datei anzeigen

@ -42,16 +42,20 @@ public class StoredItemMappings {
private final ItemMapping banner; private final ItemMapping banner;
private final ItemMapping barrier; private final ItemMapping barrier;
private final int bowl; private final int bowl;
private final int bucket;
private final int chest; private final int chest;
private final ItemMapping compass; private final ItemMapping compass;
private final ItemMapping crossbow; private final ItemMapping crossbow;
private final ItemMapping enchantedBook; private final ItemMapping enchantedBook;
private final ItemMapping fishingRod; private final ItemMapping fishingRod;
private final int flintAndSteel; private final int flintAndSteel;
private final int frogspawn;
private final int glassBottle;
private final int goldenApple; private final int goldenApple;
private final int goldIngot; private final int goldIngot;
private final int ironIngot; private final int ironIngot;
private final int lead; private final int lead;
private final int lilyPad;
private final ItemMapping milkBucket; private final ItemMapping milkBucket;
private final int nameTag; private final int nameTag;
private final ItemMapping powderSnowBucket; 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.banner = load(itemMappings, "white_banner"); // As of 1.17.10, all banners have the same Bedrock ID
this.barrier = load(itemMappings, "barrier"); this.barrier = load(itemMappings, "barrier");
this.bowl = load(itemMappings, "bowl").getJavaId(); this.bowl = load(itemMappings, "bowl").getJavaId();
this.bucket = load(itemMappings, "bucket").getBedrockId();
this.chest = load(itemMappings, "chest").getJavaId(); this.chest = load(itemMappings, "chest").getJavaId();
this.compass = load(itemMappings, "compass"); this.compass = load(itemMappings, "compass");
this.crossbow = load(itemMappings, "crossbow"); this.crossbow = load(itemMappings, "crossbow");
this.enchantedBook = load(itemMappings, "enchanted_book"); this.enchantedBook = load(itemMappings, "enchanted_book");
this.fishingRod = load(itemMappings, "fishing_rod"); this.fishingRod = load(itemMappings, "fishing_rod");
this.flintAndSteel = load(itemMappings, "flint_and_steel").getJavaId(); 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.goldenApple = load(itemMappings, "golden_apple").getJavaId();
this.goldIngot = load(itemMappings, "gold_ingot").getJavaId(); this.goldIngot = load(itemMappings, "gold_ingot").getJavaId();
this.ironIngot = load(itemMappings, "iron_ingot").getJavaId(); this.ironIngot = load(itemMappings, "iron_ingot").getJavaId();
this.lead = load(itemMappings, "lead").getJavaId(); this.lead = load(itemMappings, "lead").getJavaId();
this.lilyPad = load(itemMappings, "lily_pad").getBedrockId();
this.milkBucket = load(itemMappings, "milk_bucket"); this.milkBucket = load(itemMappings, "milk_bucket");
this.nameTag = load(itemMappings, "name_tag").getJavaId(); this.nameTag = load(itemMappings, "name_tag").getJavaId();
this.powderSnowBucket = load(itemMappings, "powder_snow_bucket"); this.powderSnowBucket = load(itemMappings, "powder_snow_bucket");

Datei anzeigen

@ -44,6 +44,7 @@ import java.util.Locale;
* Used for block entities if the Java block state contains Bedrock block information. * Used for block entities if the Java block state contains Bedrock block information.
*/ */
public final class BlockStateValues { public final class BlockStateValues {
private static final IntSet ALL_CAULDRONS = new IntOpenHashSet();
private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap(); private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap();
private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap(); private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap();
private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap(); private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap();
@ -193,6 +194,9 @@ public final class BlockStateValues {
return; return;
} }
if (javaId.contains("cauldron")) {
ALL_CAULDRONS.add(javaBlockState);
}
if (javaId.contains("_cauldron") && !javaId.contains("water_")) { if (javaId.contains("_cauldron") && !javaId.contains("water_")) {
NON_WATER_CAULDRONS.add(javaBlockState); 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 * @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); 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 * The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags
* in Bedrock need the conditional information. * in Bedrock need the conditional information.

Datei anzeigen

@ -72,6 +72,16 @@ public class BlockRegistries {
*/ */
public static final SimpleRegistry<IntSet> WATERLOGGED = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); public static final SimpleRegistry<IntSet> WATERLOGGED = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new));
/**
* A registry containing all blockstates which are always interactive.
*/
public static final SimpleRegistry<IntSet> 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<IntSet> INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new));
static { static {
BlockRegistryPopulator.populate(); BlockRegistryPopulator.populate();
} }

Datei anzeigen

@ -26,6 +26,7 @@
package org.geysermc.geyser.registry.populator; package org.geysermc.geyser.registry.populator;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.nukkitx.nbt.*; import com.nukkitx.nbt.*;
import com.nukkitx.protocol.bedrock.v527.Bedrock_v527; 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])); BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0]));
BLOCKS_JSON = blocksJson; 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<String, NbtMapBuilder, String> statesMapper) { private static NbtMap buildBedrockState(JsonNode node, int blockStateVersion, BiFunction<String, NbtMapBuilder, String> statesMapper) {

Datei anzeigen

@ -164,6 +164,9 @@ public class ItemRegistryPopulator {
} else if (identifier.equals("minecraft:empty_map") && damage == 2) { } else if (identifier.equals("minecraft:empty_map") && damage == 2) {
// Bedrock-only as its own item // Bedrock-only as its own item
continue; 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); StartGamePacket.ItemEntry entry = entries.get(identifier);
int id = -1; int id = -1;

Datei anzeigen

@ -433,11 +433,10 @@ public class GeyserSession implements GeyserConnection, CommandSender {
private long lastInteractionTime; private long lastInteractionTime;
/** /**
* Stores a future interaction to place a bucket. Will be cancelled if the client instead intended to * Stores whether the player intended to place a bucket.
* interact with a block.
*/ */
@Setter @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. * 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; 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; private MinecraftProtocol protocol;
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) { public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {

Datei anzeigen

@ -62,7 +62,7 @@ public interface BedrockOnlyBlockEntity extends RequiresBlockState {
return FlowerPotBlockEntityTranslator.getTag(session, blockState, position); return FlowerPotBlockEntityTranslator.getTag(session, blockState, position);
} else if (PistonBlockEntityTranslator.isBlock(blockState)) { } else if (PistonBlockEntityTranslator.isBlock(blockState)) {
return PistonBlockEntityTranslator.getTag(blockState, position); 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) // 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() return NbtMap.builder()
.putString("id", "Cauldron") .putString("id", "Cauldron")

Datei anzeigen

@ -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.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.inventory.ServerboundContainerClickPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*; 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.Vector3f;
import com.nukkitx.math.vector.Vector3i; import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType; 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.EntityDefinitions;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity; 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.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory; 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.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.GeyserSession; 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.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.BlockUtils; import org.geysermc.geyser.util.BlockUtils;
@ -170,6 +174,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.setLastInteractionTime(System.currentTimeMillis()); session.setLastInteractionTime(System.currentTimeMillis());
} }
if (isIncorrectHeldItem(session, packet)) {
restoreCorrectBlock(session, blockPos, packet);
return;
}
// Bedrock sends block interact code for a Java entity so we send entity code back to Java // Bedrock sends block interact code for a Java entity so we send entity code back to Java
if (session.getBlockMappings().isItemFrame(packet.getBlockRuntimeId())) { if (session.getBlockMappings().isItemFrame(packet.getBlockRuntimeId())) {
Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition()); Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
@ -192,18 +201,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
// CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch // 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(); Vector3f playerPosition = session.getPlayerEntity().getPosition();
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - getEyeHeight(session));
// Adjust position for current eye height
switch (session.getPose()) {
case SNEAKING ->
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
boolean creative = session.getGameMode() == GameMode.CREATIVE; boolean creative = session.getGameMode() == GameMode.CREATIVE;
@ -255,9 +253,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()); int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition());
if (blockState == BlockStateValues.JAVA_WATER_ID) { if (blockState == BlockStateValues.JAVA_WATER_ID) {
// Otherwise causes multiple mobs to spawn - just send a use item packet // Otherwise causes multiple mobs to spawn - just send a use item packet
// TODO when we fix mobile bucket rotation, use it for this, too useItem(session, packet, blockState);
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
session.sendDownstreamPacket(itemPacket);
break; break;
} }
} }
@ -272,29 +268,29 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.sendDownstreamPacket(blockPacket); session.sendDownstreamPacket(blockPacket);
if (packet.getItemInHand() != null) { if (packet.getItemInHand() != null) {
// Otherwise boats will not be able to be placed in survival and buckets won't work on mobile int itemId = packet.getItemInHand().getId();
if (session.getItemMappings().getBoatIds().contains(packet.getItemInHand().getId())) { int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition());
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); // Otherwise boats will not be able to be placed in survival and buckets, lily pads, frogspawn, and glass bottles won't work on mobile
session.sendDownstreamPacket(itemPacket); if (session.getItemMappings().getBoatIds().contains(itemId) ||
} else if (session.getItemMappings().getBucketIds().contains(packet.getItemInHand().getId())) { itemId == session.getItemMappings().getStoredItems().lilyPad() ||
// Let the server decide if the bucket item should change, not the client, and revert the changes the client made itemId == session.getItemMappings().getStoredItems().frogspawn()) {
InventorySlotPacket slotPacket = new InventorySlotPacket(); useItem(session, packet, blockState);
slotPacket.setContainerId(ContainerId.INVENTORY); } else if (itemId == session.getItemMappings().getStoredItems().glassBottle()) {
slotPacket.setSlot(packet.getHotbarSlot()); if (!session.isSneaking() && BlockStateValues.isCauldron(blockState) && !BlockStateValues.isNonWaterCauldron(blockState)) {
slotPacket.setItem(packet.getItemInHand()); // ServerboundUseItemPacket is not sent for water cauldrons and glass bottles
session.sendUpstreamPacket(slotPacket); return;
}
useItem(session, packet, blockState);
} else if (session.getItemMappings().getBucketIds().contains(itemId)) {
// Don't send ServerboundUseItemPacket for powder snow buckets // Don't send ServerboundUseItemPacket for powder snow buckets
if (packet.getItemInHand().getId() != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) { if (itemId != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) {
// Special check for crafting tables since clients don't send BLOCK_INTERACT when interacting if (!session.isSneaking() && BlockStateValues.isCauldron(blockState)) {
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()); // ServerboundUseItemPacket is not sent for cauldrons and buckets
if (session.isSneaking() || blockState != BlockRegistries.JAVA_IDENTIFIERS.get("minecraft:crafting_table")) { return;
// Delay the interaction in case the client doesn't intend to actually use the bucket
// See BedrockActionTranslator.java
session.setBucketScheduledFuture(session.scheduleInEventLoop(() -> {
ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
session.sendDownstreamPacket(itemPacket);
}, 5, TimeUnit.MILLISECONDS));
} }
session.setPlacedBucket(useItem(session, packet, blockState));
} else {
session.setPlacedBucket(true);
} }
} }
} }
@ -320,6 +316,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.setInteracting(true); session.setInteracting(true);
} }
case 1 -> { case 1 -> {
if (isIncorrectHeldItem(session, packet)) {
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
break;
}
// Handled when sneaking // Handled when sneaking
if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) { if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) {
break; break;
@ -334,6 +335,9 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
} else if (session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) { } else if (session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) {
// Handled in case 0 // Handled in case 0
break; break;
} else if (packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().glassBottle()) {
// Handled in case 0
break;
} }
} }
@ -520,10 +524,128 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.sendUpstreamPacket(updateWaterPacket); session.sendUpstreamPacket(updateWaterPacket);
// Reset the item in hand to prevent "missing" blocks // Reset the item in hand to prevent "missing" blocks
InventorySlotPacket slotPacket = new InventorySlotPacket(); InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
slotPacket.setContainerId(ContainerId.INVENTORY); }
slotPacket.setSlot(packet.getHotbarSlot());
slotPacket.setItem(packet.getItemInHand()); private boolean isIncorrectHeldItem(GeyserSession session, InventoryTransactionPacket packet) {
session.sendUpstreamPacket(slotPacket); int javaSlot = session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot());
int expectedItemId = ItemTranslator.getBedrockItemMapping(session, session.getPlayerInventory().getItem(javaSlot)).getBedrockId();
int heldItemId = packet.getItemInHand() == null ? ItemData.AIR.getId() : packet.getItemInHand().getId();
if (expectedItemId != heldItemId) {
session.getGeyser().getLogger().debug(session.name() + "'s held item has desynced! Expected: " + expectedItemId + " Received: " + heldItemId);
session.getGeyser().getLogger().debug("Packet: " + packet);
return true;
}
return false;
}
private boolean useItem(GeyserSession session, InventoryTransactionPacket packet, int blockState) {
// Update the player's inventory to remove any items added by the client itself
Inventory playerInventory = session.getPlayerInventory();
int heldItemSlot = playerInventory.getOffsetForHotbar(packet.getHotbarSlot());
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, playerInventory, heldItemSlot);
if (playerInventory.getItem(heldItemSlot).getAmount() > 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();
};
} }
} }

Datei anzeigen

@ -136,14 +136,6 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED); ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED);
session.sendDownstreamPacket(stopSleepingPacket); session.sendDownstreamPacket(stopSleepingPacket);
break; break;
case BLOCK_INTERACT:
// Client means to interact with a block; cancel bucket interaction, if any
if (session.getBucketScheduledFuture() != null) {
session.getBucketScheduledFuture().cancel(true);
session.setBucketScheduledFuture(null);
}
// Otherwise handled in BedrockInventoryTransactionTranslator
break;
case START_BREAK: case START_BREAK:
// Start the block breaking animation // Start the block breaking animation
if (session.getGameMode() != GameMode.CREATIVE) { if (session.getGameMode() != GameMode.CREATIVE) {

Datei anzeigen

@ -77,6 +77,13 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
boolean positionChanged = !entity.getPosition().equals(packet.getPosition()); boolean positionChanged = !entity.getPosition().equals(packet.getPosition());
boolean rotationChanged = entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw; boolean rotationChanged = entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw;
if (session.getLookBackScheduledFuture() != null) {
// Resend the rotation if it was changed by Geyser
rotationChanged |= !session.getLookBackScheduledFuture().isDone();
session.getLookBackScheduledFuture().cancel(false);
session.setLookBackScheduledFuture(null);
}
// If only the pitch and yaw changed // If only the pitch and yaw changed
// This isn't needed, but it makes the packets closer to vanilla // This isn't needed, but it makes the packets closer to vanilla
// It also means you can't "lag back" while only looking, in theory // It also means you can't "lag back" while only looking, in theory

Datei anzeigen

@ -142,7 +142,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
} }
// Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) { if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session, bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)), Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
javaId javaId
@ -183,7 +183,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
} }
// Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock // Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) { if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
bedrockOnlyBlockEntityIds.set(i); bedrockOnlyBlockEntityIds.set(i);
} }
} }

Datei anzeigen

@ -38,7 +38,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
@Override @Override
public void translate(GeyserSession session, Vector3f position, String identifier) { public void translate(GeyserSession session, Vector3f position, String identifier) {
if (session.getBucketScheduledFuture() == null) { if (!session.isPlacedBucket()) {
return; // No bucket was really interacted with return; // No bucket was really interacted with
} }
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(); GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand();
@ -71,6 +71,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
case "minecraft:salmon_bucket": case "minecraft:salmon_bucket":
case "minecraft:pufferfish_bucket": case "minecraft:pufferfish_bucket":
case "minecraft:tropical_fish_bucket": case "minecraft:tropical_fish_bucket":
case "minecraft:tadpole_bucket":
soundEvent = SoundEvent.BUCKET_EMPTY_FISH; soundEvent = SoundEvent.BUCKET_EMPTY_FISH;
break; break;
case "minecraft:water_bucket": case "minecraft:water_bucket":
@ -83,7 +84,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
if (soundEvent != null) { if (soundEvent != null) {
soundEventPacket.setSound(soundEvent); soundEventPacket.setSound(soundEvent);
session.sendUpstreamPacket(soundEventPacket); session.sendUpstreamPacket(soundEventPacket);
session.setBucketScheduledFuture(null); session.setPlacedBucket(false);
} }
} }
} }

@ -1 +1 @@
Subproject commit 01350da5d184ac535cce343d94ceaff6cff20137 Subproject commit 244efb7ea277a431c4e9c5b586eb401b691ed888