From 35be9072c7350b56afdb3c9365ed22de0adb3796 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 11 Sep 2025 14:51:09 +0200 Subject: [PATCH] Improve block breaking code (#5809) * Feature: Guessing how long block breaking will take * more changes * Remove BedrockBlockActions.java * More progress: Remove code that causes us to send erroneous early block breaking actions * Further work on insta-break handling * Add GAME_MASTER_BLOCK check * Cleanup, this actually works better than expected * Code cleanup * Inverted valid check will do invalid things! * Fix block breaking attribute reading * Implement adventure mode can_break predicates * Address reviews, minor changes to wonderful code * Remove JavaBlockBreakHandler.java in favor of extension * yeet debug * Avoid dividing by zero, fix item frame interactions for good * Also avoid dividing by zero here --------- Co-authored-by: Eclipse --- .../type/player/SessionPlayerEntity.java | 42 +- .../block/property/BasicEnumProperty.java | 6 + .../level/block/property/BooleanProperty.java | 12 + .../level/block/property/EnumProperty.java | 30 +- .../level/block/property/IntegerProperty.java | 12 + .../geyser/level/block/property/Property.java | 4 + .../geyser/level/block/type/Block.java | 2 +- .../geyser/session/GeyserSession.java | 25 +- .../session/cache/BlockBreakHandler.java | 524 ++++++++++++++++++ .../inventory/InventoryTranslator.java | 3 +- .../translator/item/ItemTranslator.java | 2 +- ...BedrockInventoryTransactionTranslator.java | 58 +- .../player/input/BedrockBlockActions.java | 237 -------- .../BedrockPlayerAuthInputTranslator.java | 59 +- .../level/JavaBlockDestructionTranslator.java | 51 +- .../org/geysermc/geyser/util/BlockUtils.java | 152 ++++- .../geysermc/geyser/util/DimensionUtils.java | 1 + 17 files changed, 805 insertions(+), 415 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java delete mode 100644 core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockBlockActions.java diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index c898bb645..356d55b23 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -42,6 +42,7 @@ import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.type.BoatEntity; import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.level.block.Blocks; @@ -79,11 +80,19 @@ public class SessionPlayerEntity extends PlayerEntity { */ @Getter protected final Map attributes = new Object2ObjectOpenHashMap<>(); + /** - * Java-only attribute + * Java-only attributes */ @Getter private double blockInteractionRange = GeyserAttributeType.BLOCK_INTERACTION_RANGE.getDefaultValue(); + @Getter + private double miningEfficiency = GeyserAttributeType.MINING_EFFICIENCY.getDefaultValue(); + @Getter + private double blockBreakSpeed = GeyserAttributeType.BLOCK_BREAK_SPEED.getDefaultValue(); + @Getter + private double submergedMiningSpeed = GeyserAttributeType.SUBMERGED_MINING_SPEED.getDefaultValue(); + /** * Used in PlayerInputTranslator for movement checks. */ @@ -321,12 +330,27 @@ public class SessionPlayerEntity extends PlayerEntity { @Override protected void updateAttribute(Attribute javaAttribute, List newAttributes) { - if (javaAttribute.getType() == AttributeType.Builtin.ATTACK_SPEED) { - session.setAttackSpeed(AttributeUtils.calculateValue(javaAttribute)); - } else if (javaAttribute.getType() == AttributeType.Builtin.BLOCK_INTERACTION_RANGE) { - this.blockInteractionRange = AttributeUtils.calculateValue(javaAttribute); - } else { - super.updateAttribute(javaAttribute, newAttributes); + if (javaAttribute.getType() instanceof AttributeType.Builtin type) { + switch (type) { + case ATTACK_SPEED -> { + session.setAttackSpeed(AttributeUtils.calculateValue(javaAttribute)); + } + case BLOCK_INTERACTION_RANGE -> { + this.blockInteractionRange = AttributeUtils.calculateValue(javaAttribute); + } + case MINING_EFFICIENCY -> { + this.miningEfficiency = AttributeUtils.calculateValue(javaAttribute); + } + case BLOCK_BREAK_SPEED -> { + this.blockBreakSpeed = AttributeUtils.calculateValue(javaAttribute); + } + case SUBMERGED_MINING_SPEED -> { + this.submergedMiningSpeed = AttributeUtils.calculateValue(javaAttribute); + } + default -> { + super.updateAttribute(javaAttribute, newAttributes); + } + } } } @@ -337,6 +361,10 @@ public class SessionPlayerEntity extends PlayerEntity { return attributeData; } + /** + * This will ONLY include attributes that have a Bedrock equivalent!!! + * see {@link LivingEntity#updateAttribute(Attribute, List)} + */ public float attributeOrDefault(GeyserAttributeType type) { var attribute = this.attributes.get(type); if (attribute == null) { diff --git a/core/src/main/java/org/geysermc/geyser/level/block/property/BasicEnumProperty.java b/core/src/main/java/org/geysermc/geyser/level/block/property/BasicEnumProperty.java index c34392504..91119f2b3 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/property/BasicEnumProperty.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/property/BasicEnumProperty.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.level.block.property; import java.util.List; +import java.util.Optional; /** * Represents enums we don't need classes for in Geyser. @@ -57,6 +58,11 @@ public final class BasicEnumProperty extends Property { return (T) this.values; } + @Override + public Optional valueOf(String string) { + return values.contains(string) ? Optional.of(string) : Optional.empty(); + } + public static BasicEnumProperty create(String name, String... values) { return new BasicEnumProperty(name, List.of(values)); } diff --git a/core/src/main/java/org/geysermc/geyser/level/block/property/BooleanProperty.java b/core/src/main/java/org/geysermc/geyser/level/block/property/BooleanProperty.java index 56877f537..2d17fac66 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/property/BooleanProperty.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/property/BooleanProperty.java @@ -25,6 +25,8 @@ package org.geysermc.geyser.level.block.property; +import java.util.Optional; + public final class BooleanProperty extends Property { private BooleanProperty(String name) { super(name); @@ -40,6 +42,16 @@ public final class BooleanProperty extends Property { return value ? 0 : 1; } + @Override + public Optional valueOf(String string) { + // Not using Boolean.parseBoolean because that will return false for any string not "true" + return switch (string) { + case "true" -> Optional.of(true); + case "false" -> Optional.of(false); + default -> Optional.empty(); + }; + } + public static BooleanProperty create(String name) { return new BooleanProperty(name); } diff --git a/core/src/main/java/org/geysermc/geyser/level/block/property/EnumProperty.java b/core/src/main/java/org/geysermc/geyser/level/block/property/EnumProperty.java index e31f665f9..3aecb1465 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/property/EnumProperty.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/property/EnumProperty.java @@ -25,31 +25,43 @@ package org.geysermc.geyser.level.block.property; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; +import java.util.Locale; +import java.util.Optional; public final class EnumProperty> extends Property { - private final IntList ordinalValues; + private final T[] values; /** * @param values all possible values of this enum. */ private EnumProperty(String name, T[] values) { super(name); - this.ordinalValues = new IntArrayList(values.length); - for (T anEnum : values) { - this.ordinalValues.add(anEnum.ordinal()); - } + this.values = values; } @Override public int valuesCount() { - return this.ordinalValues.size(); + return values.length; } @Override public int indexOf(T value) { - return this.ordinalValues.indexOf(value.ordinal()); + for (int i = 0; i < values.length; i++) { + if (value == values[i]) { + return i; + } + } + throw new IllegalArgumentException("Property " + this + " does not have value " + value); + } + + @Override + public Optional valueOf(String string) { + for (T value : values) { + if (value.name().toLowerCase(Locale.ROOT).equals(string)) { + return Optional.of(value); + } + } + return Optional.empty(); } @SafeVarargs diff --git a/core/src/main/java/org/geysermc/geyser/level/block/property/IntegerProperty.java b/core/src/main/java/org/geysermc/geyser/level/block/property/IntegerProperty.java index a772f414d..cce11e15d 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/property/IntegerProperty.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/property/IntegerProperty.java @@ -25,6 +25,8 @@ package org.geysermc.geyser.level.block.property; +import java.util.Optional; + public final class IntegerProperty extends Property { private final int offset; private final int valuesCount; @@ -53,6 +55,16 @@ public final class IntegerProperty extends Property { return this.offset + this.valuesCount; } + @Override + public Optional valueOf(String string) { + try { + int value = Integer.parseInt(string); + return value >= low() && value <= high() ? Optional.of(value) : Optional.empty(); + } catch (NumberFormatException exception) { + return Optional.empty(); + } + } + public static IntegerProperty create(String name, int low, int high) { return new IntegerProperty(name, low, high); } diff --git a/core/src/main/java/org/geysermc/geyser/level/block/property/Property.java b/core/src/main/java/org/geysermc/geyser/level/block/property/Property.java index 0c4713124..e38da7588 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/property/Property.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/property/Property.java @@ -25,6 +25,8 @@ package org.geysermc.geyser.level.block.property; +import java.util.Optional; + public abstract class Property> { private final String name; @@ -40,6 +42,8 @@ public abstract class Property> { public abstract int indexOf(T value); + public abstract Optional valueOf(String string); + @Override public String toString() { return getClass().getSimpleName() + "[" + name + "]"; diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java b/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java index 3ac4e8f3a..f15f73cac 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java @@ -223,7 +223,7 @@ public class Block { '}'; } - Property[] propertyKeys() { + public Property[] propertyKeys() { return propertyKeys; } 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 9661f68ea..b05a62894 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -53,7 +53,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.value.qual.IntRange; import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector2i; -import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; @@ -164,6 +163,7 @@ import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.auth.AuthData; import org.geysermc.geyser.session.auth.BedrockClientData; import org.geysermc.geyser.session.cache.AdvancementsCache; +import org.geysermc.geyser.session.cache.BlockBreakHandler; import org.geysermc.geyser.session.cache.BookEditCache; import org.geysermc.geyser.session.cache.BundleCache; import org.geysermc.geyser.session.cache.ChunkCache; @@ -222,7 +222,6 @@ import org.geysermc.mcprotocollib.protocol.data.handshake.HandshakeIntent; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundClientInformationPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatCommandSignedPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundClientTickEndPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemPacket; @@ -301,6 +300,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private final WaypointCache waypointCache; private final WorldCache worldCache; + /** + * Handles block breaking and break animation progress caching. + */ + @Setter + private BlockBreakHandler blockBreakHandler; + @Setter private TeleportCache unconfirmedTeleport; @@ -468,9 +473,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private BedrockDimension bedrockDimension = this.bedrockOverworldDimension; - @Setter - private int breakingBlock; - @Setter private Vector3i lastBlockPlacePosition; @@ -581,12 +583,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private long lastInteractionTime; - /** - * Stores when the player started to break a block. Used to allow correct break time for custom blocks. - */ - @Setter - private long blockBreakStartTime; - /** * Stores whether the player intended to place a bucket. */ @@ -776,8 +772,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { this.entityData = new GeyserEntityData(this); this.worldBorder = new WorldBorder(this); - this.collisionManager = new CollisionManager(this); + this.blockBreakHandler = new BlockBreakHandler(this); this.playerEntity = new SessionPlayerEntity(this); collisionManager.updatePlayerBoundingBox(this.playerEntity.getPosition()); @@ -1822,7 +1818,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.SERVER); startGamePacket.setRewindHistorySize(0); - startGamePacket.setServerAuthoritativeBlockBreaking(false); + // Server authorative block breaking results in the client always sending + // positions for block breaking actions, which is easier to validate + // It does *not* mean we can dictate the break speed server-sided :( + startGamePacket.setServerAuthoritativeBlockBreaking(true); startGamePacket.setServerId(""); startGamePacket.setWorldId(""); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java b/core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java new file mode 100644 index 000000000..87eb3a582 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.session.cache; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import it.unimi.dsi.fastutil.Pair; +import lombok.Getter; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.protocol.bedrock.data.LevelEvent; +import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData; +import org.cloudburstmc.protocol.bedrock.data.PlayerBlockActionData; +import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.ItemFrameEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.item.CustomItemTranslator; +import org.geysermc.geyser.translator.protocol.bedrock.BedrockInventoryTransactionTranslator; +import org.geysermc.geyser.translator.protocol.java.level.JavaBlockDestructionTranslator; +import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.BlockBreakStage; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.InteractAction; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.AdventureModePredicate; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Class responsible for block breaking handling. This is designed to be extensible + * by extensions (not officially supported!). + */ +public class BlockBreakHandler { + + private final static Set GAME_MASTER_BLOCKS = Set.of( + Blocks.COMMAND_BLOCK, + Blocks.CHAIN_COMMAND_BLOCK, + Blocks.REPEATING_COMMAND_BLOCK, + Blocks.JIGSAW, + Blocks.STRUCTURE_BLOCK, + Blocks.TEST_BLOCK, + Blocks.TEST_INSTANCE_BLOCK + ); + + protected final GeyserSession session; + + /** + * The position of the current block being broken. + * Null indicates no block breaking in progress. + */ + protected @Nullable Vector3i currentBlockPos = null; + + /** + * The current block state that is being broken. + * Null indicates no block breaking in progress. + */ + protected @Nullable BlockState currentBlockState = null; + + /** + * The Bedrock client tick in which block breaking of the current block began. + * Only set when keeping track of custom blocks / custom items breaking blocks. + */ + protected long blockStartBreakTime = 0; + + /** + * The last block position that was instantly broken. + * Used to ignore subsequent block actions from the Bedrock client. + */ + protected Vector3i lastInstaMinedPosition = null; + + /** + * Caches all blocks we had to restore e.g. due to out-of-range or being unable to mine + * in order to avoid duplicate corrections. + */ + protected Set restoredBlocks = new HashSet<>(2); + + /** + * Used to ignore subsequent block interactions after an item frame interaction + */ + protected @Nullable Vector3i itemFramePos = null; + + /** + * See {@link JavaBlockDestructionTranslator} for usage and explanation + */ + @Getter + private final Cache> destructionStageCache = CacheBuilder.newBuilder() + .maximumSize(200) + .expireAfterWrite(3, TimeUnit.MINUTES) + .build(); + + /** + * Used to cache adventure mode can break predicate lookups + */ + private final BlockPredicateCache blockPredicateCache = new BlockPredicateCache(); + + public BlockBreakHandler(final GeyserSession session) { + this.session = session; + } + + /** + * Main entrypoint that handles block breaking actions, if present + * @param packet the player auth input packet + */ + public void handlePlayerAuthInputPacket(PlayerAuthInputPacket packet) { + if (packet.getInputData().contains(PlayerAuthInputData.PERFORM_BLOCK_ACTIONS)) { + handleBlockBreakActions(packet); + restoredBlocks.clear(); + this.itemFramePos = null; + } + } + + protected void handleBlockBreakActions(PlayerAuthInputPacket packet) { + for (int i = 0; i < packet.getPlayerActions().size(); i++) { + PlayerBlockActionData actionData = packet.getPlayerActions().get(i); + Vector3i position = actionData.getBlockPosition(); + int blockFace = actionData.getFace(); + + switch (actionData.getAction()) { + case DROP_ITEM -> { + ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, + position, Direction.VALUES[blockFace], 0); + session.sendDownstreamGamePacket(dropItemPacket); + } + // Must do this ugly as it can also be called from the block_continue_destroy case :( + case START_BREAK -> preStartBreakHandle(position, blockFace, packet.getTick()); + case BLOCK_CONTINUE_DESTROY -> { + if (testForItemFrameEntity(position) || testForLastInstaBreakPosOrReset(position) || abortDueToBlockRestoring(position)) { + continue; + } + + // Position mismatch == we break a new block! Bedrock won't send START_BREAK when continuously mining + // That applies in creative mode too! (last test in 1.21.100) + if (!Objects.equals(position, currentBlockPos) || currentBlockState == null) { + if (currentBlockPos != null) { + handleAbortBreaking(currentBlockPos); + } + preStartBreakHandle(position, blockFace, packet.getTick()); + continue; + } + + // The client loves to send this block action alongside BLOCK_PREDICT_DESTROY in the same packet; + // we can skip handling this action if the same position is updated again in the same tick + if (i < packet.getPlayerActions().size() - 1) { + PlayerBlockActionData nextAction = packet.getPlayerActions().get(i + 1); + if (Objects.equals(nextAction.getBlockPosition(), position)) { + continue; + } + } + + BlockState state = session.getGeyser().getWorldManager().blockAt(session, position); + if (!canBreak(position, state)) { + BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); + restoredBlocks.add(position); + continue; + } + + handleContinueDestroy(position, state, blockFace, packet.getTick()); + } + case BLOCK_PREDICT_DESTROY -> { + if (testForItemFrameEntity(position) || testForLastInstaBreakPosOrReset(position)) { + continue; + } + + // Not using abortDueToBlockRestoring method here as we're fully restoring the block, + // to counteract Bedrock's own client-side prediction + if (!restoredBlocks.isEmpty()) { + BlockUtils.restoreCorrectBlock(session, position); + continue; + } + + BlockState state = session.getGeyser().getWorldManager().blockAt(session, position); + boolean valid = currentBlockState != null && Objects.equals(position, currentBlockPos); + if (!canBreak(position, state) || !valid) { + if (!valid) { + GeyserImpl.getInstance().getLogger().warning("Player %s tried to break block at %s (%s), without starting to destroy it!" + .formatted(session.bedrockUsername(), position, currentBlockState)); + } + BlockUtils.stopBreakAndRestoreBlock(session, position, state); + restoredBlocks.add(position); + continue; + } + + handlePredictDestroy(position, state, blockFace, packet.getTick()); + } + case ABORT_BREAK -> { + // Abort break can also be sent after the block on that pos was broken..... + // At that point it's safe to assume that we won't get subsequent block actions on this position + // so reset it and return since there isn't anything to abort + if (Objects.equals(lastInstaMinedPosition, position)) { + lastInstaMinedPosition = null; + continue; + } + + // Also handles item frame interactions in adventure mode + if (testForItemFrameEntity(position)) { + continue; + } + + handleAbortBreaking(position); + } + default -> { + throw new IllegalStateException("Unknown block break action: " + actionData.getAction()); + } + } + } + } + + /** + * Called from either a START_BREAK or BLOCK_CONTINUE_DESTROY case, the latter + * if the client switches to a new block. This method then runs pre-break checks. + */ + private void preStartBreakHandle(Vector3i position, int blockFace, long tick) { + // New block being broken -> ignore previous insta-mine pos since that's no longer relevant + lastInstaMinedPosition = null; + + if (testForItemFrameEntity(position) || abortDueToBlockRestoring(position)) { + return; + } + + BlockState state = session.getGeyser().getWorldManager().blockAt(session, position); + if (!canBreak(position, state)) { + BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); + restoredBlocks.add(position); + return; + } + + handleStartBreak(position, state, blockFace, tick); + } + + protected void handleStartBreak(@NonNull Vector3i position, @NonNull BlockState state, int blockFace, long tick) { + GeyserItemStack item = session.getPlayerInventory().getItemInHand(); + Direction direction = Direction.VALUES[blockFace]; + + // Account for fire - the client likes to hit the block behind. + Vector3i fireBlockPos = BlockUtils.getBlockPosition(position, blockFace); + Block possibleFireBlock = session.getGeyser().getWorldManager().blockAt(session, fireBlockPos).block(); + if (possibleFireBlock == Blocks.FIRE || possibleFireBlock == Blocks.SOUL_FIRE) { + ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos, + direction, session.getWorldCache().nextPredictionSequence()); + session.sendDownstreamGamePacket(startBreakingPacket); + } + + // % block breaking progress in this tick + float breakProgress = calculateBreakProgress(state, position, item); + + // insta-breaking should be treated differently; don't send STOP_BREAK for these + if (session.isInstabuild() || breakProgress >= 1.0F) { + // Avoid sending STOP_BREAK for instantly broken blocks + lastInstaMinedPosition = position; + destroyBlock(state, position, direction, true); + } else { + // If the block is custom or the breaking item is custom, we must keep track of break time ourselves + ItemMapping mapping = item.getMapping(session); + ItemDefinition customItem = mapping.isTool() ? CustomItemTranslator.getCustomItem(item.getComponents(), mapping) : null; + CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(state.javaId()); + SkullCache.Skull skull = session.getSkullCache().getSkulls().get(position); + + this.blockStartBreakTime = 0; + if (BlockRegistries.NON_VANILLA_BLOCK_IDS.get().get(state.javaId()) || blockStateOverride != null || + customItem != null || (skull != null && skull.getBlockDefinition() != null)) { + this.blockStartBreakTime = tick; + } + + LevelEventPacket startBreak = new LevelEventPacket(); + startBreak.setType(LevelEvent.BLOCK_START_BREAK); + startBreak.setPosition(position.toFloat()); + startBreak.setData((int) (65535 / BlockUtils.reciprocal(breakProgress))); + session.sendUpstreamPacket(startBreak); + + BlockUtils.spawnBlockBreakParticles(session, direction, position, state); + + this.currentBlockPos = position; + this.currentBlockState = state; + + session.sendDownstreamGamePacket(new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, position, direction, session.getWorldCache().nextPredictionSequence())); + } + } + + protected void handleContinueDestroy(Vector3i position, BlockState state, int blockFace, long tick) { + Direction direction = Direction.VALUES[blockFace]; + BlockUtils.spawnBlockBreakParticles(session, direction, position, state); + double totalBreakTime = BlockUtils.reciprocal(calculateBreakProgress(state, position, session.getPlayerInventory().getItemInHand())); + + if (blockStartBreakTime != 0) { + long ticksSinceStart = tick - blockStartBreakTime; + // We need to add a slight delay to the break time, otherwise the client breaks blocks too fast + if (ticksSinceStart >= (totalBreakTime += 2)) { + destroyBlock(state, position, direction, false); + return; + } + } + + // Update the break time in the event that player conditions changed (jumping, effects applied) + LevelEventPacket updateBreak = new LevelEventPacket(); + updateBreak.setType(LevelEvent.BLOCK_UPDATE_BREAK); + updateBreak.setPosition(position.toFloat()); + updateBreak.setData((int) (65535 / totalBreakTime)); + session.sendUpstreamPacket(updateBreak); + } + + protected void handlePredictDestroy(Vector3i position, BlockState state, int blockFace, long tick) { + destroyBlock(state, position, Direction.VALUES[blockFace], false); + } + + protected void handleAbortBreaking(Vector3i position) { + // Bedrock edition "confirms" it stopped breaking blocks by sending an abort packet + // We don't forward those as a Java client wouldn't send those either + if (currentBlockPos != null) { + ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, currentBlockPos, Direction.DOWN, 0); + session.sendDownstreamGamePacket(abortBreakingPacket); + } + + BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); + } + + /** + * Tests for a previous item frame block interaction, or the presence + * of an item frame at the position. + * @return whether block breaking must stop due to an item frame interaction + */ + protected boolean testForItemFrameEntity(Vector3i position) { + if (itemFramePos != null && itemFramePos.equals(position)) { + return true; + } + + Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, position); + if (itemFrameEntity != null) { + ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(itemFrameEntity.getEntityId(), + InteractAction.ATTACK, session.isSneaking()); + session.sendDownstreamGamePacket(attackPacket); + itemFramePos = position; + return true; + } + return false; + } + + /** + * Tests whether the block action should be processed by testing whether + * this action (or any other block action in this tick) was already rejected before + */ + private boolean abortDueToBlockRestoring(Vector3i position) { + // If it already contains our position, we can assume that a stop / restore was already sent + if (restoredBlocks.contains(position)) { + return true; + } + + // We don't want to continue handling even new blocks as those could be e.g. behind a block which is not broken + if (!restoredBlocks.isEmpty()) { + BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); + restoredBlocks.add(position); + return true; + } + return false; + } + + /** + * Checks whether a block interaction may proceed, or whether it must be interrupted. + * This includes world border, "hands busy" (boat steering), and GameMode checks. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + protected boolean canBreak(Vector3i vector, BlockState state) { + if (session.isHandsBusy() || !session.getWorldBorder().isInsideBorderBoundaries()) { + return false; + } + + switch (session.getGameMode()) { + case SPECTATOR -> { + return false; + } + case ADVENTURE -> { + if (!blockPredicateCache.calculatePredicate(session, state, session.getPlayerInventory().getItemInHand())) { + return false; + } + } + } + + Vector3f playerPosition = session.getPlayerEntity().getPosition(); + playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight()); + return BedrockInventoryTransactionTranslator.canInteractWithBlock(session, playerPosition, vector); + } + + protected boolean canDestroyBlock(BlockState state) { + boolean instabuild = session.isInstabuild(); + if (instabuild) { + ToolData data = session.getPlayerInventory().getItemInHand().getComponent(DataComponentTypes.TOOL); + if (data != null && !data.isCanDestroyBlocksInCreative()) { + return false; + } + } + + if (GAME_MASTER_BLOCKS.contains(state.block())) { + if (!instabuild || session.getOpPermissionLevel() < 2) { + return false; + } + } + + return !state.is(Blocks.AIR); + } + + protected void destroyBlock(BlockState state, Vector3i vector, Direction direction, boolean instamine) { + // Send java packet + session.sendDownstreamGamePacket(new ServerboundPlayerActionPacket(instamine ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING, + vector, direction, session.getWorldCache().nextPredictionSequence())); + session.getWorldCache().markPositionInSequence(vector); + + if (canDestroyBlock(state)) { + BlockUtils.spawnBlockBreakParticles(session, direction, vector, state); + BlockUtils.sendBedrockBlockDestroy(session, vector.toFloat(), state.javaId()); + } else { + BlockUtils.restoreCorrectBlock(session, vector, state); + } + clearCurrentVariables(); + } + + protected float calculateBreakProgress(BlockState state, Vector3i vector, GeyserItemStack stack) { + return BlockUtils.getBlockMiningProgressPerTick(session, state.block(), stack); + } + + /** + * Helper method to ignore all insta-break actions that were already sent to the Java server. + * This ensures that Geyser does not send a FINISH_DIGGING player action for instantly mined blocks, + * or those mined while in creative mode. + */ + protected boolean testForLastInstaBreakPosOrReset(Vector3i position) { + if (Objects.equals(lastInstaMinedPosition, position)) { + return true; + } + lastInstaMinedPosition = null; + return false; + } + + /** + * Resets variables after a block was broken. + */ + protected void clearCurrentVariables() { + this.currentBlockPos = null; + this.currentBlockState = null; + this.blockStartBreakTime = 0L; + } + + /** + * Resets the handler, including variables that persist across single packets + */ + public void reset() { + clearCurrentVariables(); + this.lastInstaMinedPosition = null; + this.destructionStageCache.invalidateAll(); + } + + private static class BlockPredicateCache { + private BlockState lastBlockState; + private GeyserItemStack lastItemStack; + private Boolean lastResult; + + private boolean calculatePredicate(GeyserSession session, BlockState state, GeyserItemStack stack) { + // An empty stack will never pass + if (stack.isEmpty()) { + return false; + } + + AdventureModePredicate canBreak = stack.getComponent(DataComponentTypes.CAN_BREAK); + if (canBreak == null) { // Neither will a stack without can_break + return false; + } else if (state.equals(lastBlockState) && stack.equals(lastItemStack) && lastResult != null) { // Check lastResult just in case. + return lastResult; + } + + this.lastBlockState = state; + this.lastItemStack = stack; + + // Any of the predicates have to match for the stack to match + for (AdventureModePredicate.BlockPredicate predicate : canBreak.getPredicates()) { + if (BlockUtils.blockMatchesPredicate(session, state, predicate)) { + return lastResult = true; + } + } + return lastResult = false; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index 64debc830..baf53487d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -604,7 +604,8 @@ public abstract class InventoryTranslator { case CRAFT_RESULTS_DEPRECATED: // Tends to be called for UI inventories case CRAFT_RECIPE_OPTIONAL: // Anvils and cartography tables will handle this case CRAFT_LOOM: // Looms 1.17.40+ - case CRAFT_REPAIR_AND_DISENCHANT: { // Grindstones 1.17.40+ + case CRAFT_REPAIR_AND_DISENCHANT: // Grindstones 1.17.40+ + case MINE_BLOCK: { // server-auth block breaking, confirming durability break; } default: diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index a38bbb272..412e99829 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -330,7 +330,7 @@ public final class ItemTranslator { amount += session.getPlayerEntity().attributeOrDefault(GeyserAttributeType.ATTACK_DAMAGE); baseModifier = true; } else if (modifier.getId().equals(BASE_ATTACK_SPEED_ID)) { - amount += session.getPlayerEntity().attributeOrDefault(GeyserAttributeType.ATTACK_SPEED); + amount += session.getAttackSpeed(); baseModifier = true; } 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 30c96f096..7e74e800d 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 @@ -27,7 +27,7 @@ package org.geysermc.geyser.translator.protocol.bedrock; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; -import org.cloudburstmc.math.vector.Vector3d; +import java.util.List; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; @@ -44,11 +44,9 @@ import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket; import org.cloudburstmc.protocol.bedrock.packet.InventoryTransactionPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; -import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; 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; @@ -66,10 +64,8 @@ import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.block.type.ButtonBlock; import org.geysermc.geyser.level.block.type.CauldronBlock; -import org.geysermc.geyser.level.block.type.SkullBlock; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.FakeHeadProvider; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; @@ -90,14 +86,10 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponen import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket; -import java.util.List; -import java.util.concurrent.TimeUnit; - /** * BedrockInventoryTransactionTranslator handles most interactions between the client and the world, * or the client and their inventory. @@ -189,7 +181,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator false; }; if (isGodBridging) { - restoreCorrectBlock(session, blockPos); + BlockUtils.restoreCorrectBlock(session, blockPos); return; } } @@ -209,7 +201,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator playerActions) { - // Send book update before any player action - session.getBookEditCache().checkForSend(); - - for (PlayerBlockActionData blockActionData : playerActions) { - handle(session, blockActionData); - } - } - - private static void handle(GeyserSession session, PlayerBlockActionData blockActionData) { - PlayerActionType action = blockActionData.getAction(); - Vector3i vector = blockActionData.getBlockPosition(); - int blockFace = blockActionData.getFace(); - switch (action) { - case DROP_ITEM -> { - ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, - vector, Direction.VALUES[blockFace], 0); - session.sendDownstreamGamePacket(dropItemPacket); - } - case START_BREAK -> { - // Ignore START_BREAK when the player is CREATIVE to avoid Spigot receiving 2 packets it interpets as block breaking. https://github.com/GeyserMC/Geyser/issues/4021 - if (session.getGameMode() == GameMode.CREATIVE) { - break; - } - - if (!canMine(session, vector)) { - return; - } - - // Start the block breaking animation - int blockState = session.getGeyser().getWorldManager().getBlockAt(session, vector); - LevelEventPacket startBreak = new LevelEventPacket(); - startBreak.setType(LevelEvent.BLOCK_START_BREAK); - startBreak.setPosition(vector.toFloat()); - double breakTime = BlockUtils.getSessionBreakTimeTicks(session, BlockState.of(blockState).block()); - - // If the block is custom or the breaking item is custom, we must keep track of break time ourselves - GeyserItemStack item = session.getPlayerInventory().getItemInHand(); - ItemMapping mapping = item.getMapping(session); - ItemDefinition customItem = mapping.isTool() ? CustomItemTranslator.getCustomItem(item.getComponents(), mapping) : null; - CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(blockState); - SkullCache.Skull skull = session.getSkullCache().getSkulls().get(vector); - - session.setBlockBreakStartTime(0); - if (BlockRegistries.NON_VANILLA_BLOCK_IDS.get().get(blockState) || blockStateOverride != null || customItem != null || (skull != null && skull.getBlockDefinition() != null)) { - session.setBlockBreakStartTime(System.currentTimeMillis()); - } - startBreak.setData((int) (65535 / breakTime)); - session.setBreakingBlock(blockState); - session.sendUpstreamPacket(startBreak); - - // Account for fire - the client likes to hit the block behind. - Vector3i fireBlockPos = BlockUtils.getBlockPosition(vector, blockFace); - Block block = session.getGeyser().getWorldManager().blockAt(session, fireBlockPos).block(); - Direction direction = Direction.VALUES[blockFace]; - if (block == Blocks.FIRE || block == Blocks.SOUL_FIRE) { - ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos, - direction, session.getWorldCache().nextPredictionSequence()); - session.sendDownstreamGamePacket(startBreakingPacket); - } - - ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, - vector, direction, session.getWorldCache().nextPredictionSequence()); - session.sendDownstreamGamePacket(startBreakingPacket); - - spawnBlockBreakParticles(session, direction, vector, BlockState.of(blockState)); - } - case CONTINUE_BREAK -> { - if (session.getGameMode() == GameMode.CREATIVE) { - break; - } - - if (!canMine(session, vector)) { - return; - } - - int breakingBlock = session.getBreakingBlock(); - if (breakingBlock == -1) { - breakingBlock = Block.JAVA_AIR_ID; - } - - Vector3f vectorFloat = vector.toFloat(); - - BlockState breakingBlockState = BlockState.of(breakingBlock); - Direction direction = Direction.VALUES[blockFace]; - spawnBlockBreakParticles(session, direction, vector, breakingBlockState); - - double breakTime = BlockUtils.getSessionBreakTimeTicks(session, breakingBlockState.block()); - // If the block is custom, we must keep track of when it should break ourselves - long blockBreakStartTime = session.getBlockBreakStartTime(); - if (blockBreakStartTime != 0) { - long timeSinceStart = System.currentTimeMillis() - blockBreakStartTime; - // We need to add a slight delay to the break time, otherwise the client breaks blocks too fast - if (timeSinceStart >= (breakTime += 2) * 50) { - // Play break sound and particle - LevelEventPacket effectPacket = new LevelEventPacket(); - effectPacket.setPosition(vectorFloat); - effectPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK); - effectPacket.setData(session.getBlockMappings().getBedrockBlockId(breakingBlock)); - session.sendUpstreamPacket(effectPacket); - - // Break the block - ServerboundPlayerActionPacket finishBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.FINISH_DIGGING, - vector, direction, session.getWorldCache().nextPredictionSequence()); - session.sendDownstreamGamePacket(finishBreakingPacket); - session.setBlockBreakStartTime(0); - break; - } - } - // Update the break time in the event that player conditions changed (jumping, effects applied) - LevelEventPacket updateBreak = new LevelEventPacket(); - updateBreak.setType(LevelEvent.BLOCK_UPDATE_BREAK); - updateBreak.setPosition(vectorFloat); - updateBreak.setData((int) (65535 / breakTime)); - session.sendUpstreamPacket(updateBreak); - } - case ABORT_BREAK -> { - if (session.getGameMode() != GameMode.CREATIVE) { - // As of 1.16.210: item frame items are taken out here. - // Survival also sends START_BREAK, but by attaching our process here adventure mode also works - Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, vector); - if (itemFrameEntity != null) { - ServerboundInteractPacket interactPacket = new ServerboundInteractPacket(itemFrameEntity.getEntityId(), - InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking()); - session.sendDownstreamGamePacket(interactPacket); - break; - } - } - - ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, 0); - session.sendDownstreamGamePacket(abortBreakingPacket); - - LevelEventPacket stopBreak = new LevelEventPacket(); - stopBreak.setType(LevelEvent.BLOCK_STOP_BREAK); - stopBreak.setPosition(vector.toFloat()); - stopBreak.setData(0); - session.setBreakingBlock(-1); - session.setBlockBreakStartTime(0); - session.sendUpstreamPacket(stopBreak); - } - // Handled in BedrockInventoryTransactionTranslator - case STOP_BREAK -> { - } - } - } - - private static boolean canMine(GeyserSession session, Vector3i vector) { - if (session.isHandsBusy()) { - session.setBreakingBlock(-1); - session.setBlockBreakStartTime(0); - - LevelEventPacket stopBreak = new LevelEventPacket(); - stopBreak.setType(LevelEvent.BLOCK_STOP_BREAK); - stopBreak.setPosition(vector.toFloat()); - stopBreak.setData(0); - session.setBreakingBlock(-1); - session.sendUpstreamPacket(stopBreak); - return false; - } - return true; - } - - private static void spawnBlockBreakParticles(GeyserSession session, Direction direction, Vector3i position, BlockState blockState) { - LevelEventPacket levelEventPacket = new LevelEventPacket(); - switch (direction) { - case UP -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_UP); - case DOWN -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_DOWN); - case NORTH -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_NORTH); - case EAST -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_EAST); - case SOUTH -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_SOUTH); - case WEST -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_WEST); - } - levelEventPacket.setPosition(position.toFloat()); - levelEventPacket.setData(session.getBlockMappings().getBedrockBlock(blockState).getRuntimeId()); - session.sendUpstreamPacket(levelEventPacket); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java index 8f0af5953..4b2719d1f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java @@ -29,45 +29,34 @@ import org.cloudburstmc.math.GenericMath; import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.InputMode; -import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.ItemUseTransaction; import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; -import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; -import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.BoatEntity; import org.geysermc.geyser.entity.type.Entity; -import org.geysermc.geyser.entity.type.ItemFrameEntity; import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity; import org.geysermc.geyser.entity.type.living.animal.horse.LlamaEntity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.entity.vehicle.ClientVehicle; -import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; -import org.geysermc.geyser.translator.protocol.bedrock.BedrockInventoryTransactionTranslator; import org.geysermc.geyser.util.CooldownUtils; -import org.geysermc.mcprotocollib.protocol.data.ProtocolState; -import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; -import org.geysermc.mcprotocollib.protocol.data.game.entity.player.InteractAction; -import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundClientTickEndPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundMoveVehiclePacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket; import java.util.HashSet; +import java.util.List; import java.util.Set; @Translator(packet = PlayerAuthInputPacket.class) @@ -81,6 +70,7 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator processItemUseTransaction(session, packet.getItemUseTransaction()); - case PERFORM_BLOCK_ACTIONS -> BedrockBlockActions.translate(session, packet.getPlayerActions()); + case PERFORM_ITEM_STACK_REQUEST -> session.getPlayerInventoryHolder().translateRequests(List.of(packet.getItemStackRequest())); case START_SWIMMING -> session.setSwimming(true); case STOP_SWIMMING -> session.setSwimming(false); case START_CRAWLING -> session.setCrawling(true); @@ -230,51 +220,8 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator levelEventPacket.setData(breakTime); - case STAGE_2 -> levelEventPacket.setData(breakTime * 2); - case STAGE_3 -> levelEventPacket.setData(breakTime * 3); - case STAGE_4 -> levelEventPacket.setData(breakTime * 4); - case STAGE_5 -> levelEventPacket.setData(breakTime * 5); - case STAGE_6 -> levelEventPacket.setData(breakTime * 6); - case STAGE_7 -> levelEventPacket.setData(breakTime * 7); - case STAGE_8 -> levelEventPacket.setData(breakTime * 8); - case STAGE_9 -> levelEventPacket.setData(breakTime * 9); - case STAGE_10 -> levelEventPacket.setData(breakTime * 10); - case RESET -> { - levelEventPacket.setType(LevelEvent.BLOCK_STOP_BREAK); - levelEventPacket.setData(0); - } + // First: Check if we know when the last packet for this position was sent - we'll use that for our estimation + Pair lastUpdate = session.getBlockBreakHandler().getDestructionStageCache().getIfPresent(packet.getPosition()); + if (lastUpdate == null) { + levelEventPacket.setType(LevelEvent.BLOCK_START_BREAK); + levelEventPacket.setData(65535 / 6000); // just a high value (5 mins), we'll update this once we get a new progress update + } else { + // Ticks since last update + int ticksSince = (int) (session.getClientTicks() - lastUpdate.first()); + int stagesSince = packet.getStage().compareTo(lastUpdate.second()); + int ticksPerStage = stagesSince == 0 ? ticksSince : ticksSince / stagesSince; + int remainingStages = 10 - packet.getStage().ordinal(); + + levelEventPacket.setType(LevelEvent.BLOCK_UPDATE_BREAK); + levelEventPacket.setData(65535 / Math.max(remainingStages, 1) * Math.max(ticksPerStage, 1)); } + + session.getBlockBreakHandler().getDestructionStageCache().put(packet.getPosition(), Pair.of(session.getClientTicks(), packet.getStage())); session.sendUpstreamPacket(levelEventPacket); } } diff --git a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java index d3b4f7b97..52d0499f0 100644 --- a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java @@ -25,31 +25,46 @@ package org.geysermc.geyser.util; +import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; -import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.cloudburstmc.protocol.bedrock.data.LevelEvent; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.level.block.property.Property; import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.block.type.SkullBlock; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.EntityEffectCache; +import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.AdventureModePredicate; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; +import java.util.List; +import java.util.Optional; + public final class BlockUtils { /** * Returns the total mining progress added by mining the block in a single tick + * Mirrors mojmap BlockBehaviour#getDestroyProgress + * * @return the mining progress added by this tick. */ public static float getBlockMiningProgressPerTick(GeyserSession session, Block block, GeyserItemStack itemInHand) { float destroySpeed = block.destroyTime(); - if (destroySpeed == -1) { + if (destroySpeed == -1.0F) { return 0; } int speedMultiplier = hasCorrectTool(session, block, itemInHand) ? 30 : 100; - return getPlayerDestroySpeed(session, block, itemInHand) / destroySpeed / speedMultiplier; + return getPlayerDestroySpeed(session, block, itemInHand) / destroySpeed / (float) speedMultiplier; } private static boolean hasCorrectTool(GeyserSession session, Block block, GeyserItemStack stack) { @@ -76,7 +91,7 @@ public final class BlockUtils { private static float getItemDestroySpeed(GeyserSession session, Block block, GeyserItemStack stack) { ToolData tool = stack.getComponent(DataComponentTypes.TOOL); if (tool == null) { - return 1f; + return 1.0F; } for (ToolData.Rule rule : tool.getRules()) { @@ -92,15 +107,15 @@ public final class BlockUtils { private static float getPlayerDestroySpeed(GeyserSession session, Block block, GeyserItemStack itemInHand) { float destroySpeed = getItemDestroySpeed(session, block, itemInHand); - EntityEffectCache effectCache = session.getEffectCache(); if (destroySpeed > 1.0F) { - destroySpeed += session.getPlayerEntity().attributeOrDefault(GeyserAttributeType.MINING_EFFICIENCY); + destroySpeed += (float) session.getPlayerEntity().getMiningEfficiency(); } + EntityEffectCache effectCache = session.getEffectCache(); int miningSpeedMultiplier = getMiningSpeedAmplification(effectCache); if (miningSpeedMultiplier > 0) { - destroySpeed *= miningSpeedMultiplier * 0.2F; + destroySpeed *= 1.0F + miningSpeedMultiplier * 0.2F; } if (effectCache.getMiningFatigue() != 0) { @@ -113,13 +128,13 @@ public final class BlockUtils { destroySpeed *= slowdown; } - destroySpeed *= session.getPlayerEntity().attributeOrDefault(GeyserAttributeType.BLOCK_BREAK_SPEED); + destroySpeed *= (float) session.getPlayerEntity().getBlockBreakSpeed(); if (session.getCollisionManager().isWaterInEyes()) { - destroySpeed *= session.getPlayerEntity().attributeOrDefault(GeyserAttributeType.SUBMERGED_MINING_SPEED); + destroySpeed *= (float) session.getPlayerEntity().getSubmergedMiningSpeed(); } if (!session.getPlayerEntity().isOnGround()) { - destroySpeed /= 5F; + destroySpeed /= 5.0F; } return destroySpeed; @@ -129,8 +144,8 @@ public final class BlockUtils { return Math.max(cache.getHaste(), cache.getConduitPower()); } - public static double getSessionBreakTimeTicks(GeyserSession session, Block block) { - return Math.ceil(1 / getBlockMiningProgressPerTick(session, block, session.getPlayerInventory().getItemInHand())); + public static double reciprocal(double progress) { + return Math.ceil(1 / progress); } /** @@ -174,8 +189,117 @@ public final class BlockUtils { return BlockRegistries.COLLISIONS.get(blockId); } - public static BlockCollision getCollisionAt(GeyserSession session, Vector3i blockPos) { - return getCollision(session.getGeyser().getWorldManager().getBlockAt(session, blockPos)); + public static void spawnBlockBreakParticles(GeyserSession session, Direction direction, Vector3i position, BlockState blockState) { + LevelEventPacket levelEventPacket = new LevelEventPacket(); + switch (direction) { + case UP -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_UP); + case DOWN -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_DOWN); + case NORTH -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_NORTH); + case EAST -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_EAST); + case SOUTH -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_SOUTH); + case WEST -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_WEST); + } + levelEventPacket.setPosition(position.toFloat()); + levelEventPacket.setData(session.getBlockMappings().getBedrockBlock(blockState).getRuntimeId()); + session.sendUpstreamPacket(levelEventPacket); + } + + public static void sendBedrockStopBlockBreak(GeyserSession session, Vector3f vector) { + LevelEventPacket stopBreak = new LevelEventPacket(); + stopBreak.setType(LevelEvent.BLOCK_STOP_BREAK); + stopBreak.setPosition(vector); + stopBreak.setData(0); + session.sendUpstreamPacket(stopBreak); + } + + public static void sendBedrockBlockDestroy(GeyserSession session, Vector3f vector, int blockState) { + LevelEventPacket blockBreakPacket = new LevelEventPacket(); + blockBreakPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK); + blockBreakPacket.setPosition(vector); + blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState)); + session.sendUpstreamPacket(blockBreakPacket); + } + + public static void restoreCorrectBlock(GeyserSession session, Vector3i vector, BlockState blockState) { + BlockDefinition bedrockBlock = session.getBlockMappings().getBedrockBlock(blockState); + + if (blockState.block() instanceof SkullBlock skullBlock && skullBlock.skullType() == SkullBlock.Type.PLAYER) { + // The changed block was a player skull so check if a custom block was defined for this skull + SkullCache.Skull skull = session.getSkullCache().getSkulls().get(vector); + if (skull != null && skull.getBlockDefinition() != null) { + bedrockBlock = skull.getBlockDefinition(); + } + } + + UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); + updateBlockPacket.setDataLayer(0); + updateBlockPacket.setBlockPosition(vector); + updateBlockPacket.setDefinition(bedrockBlock); + updateBlockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); + session.sendUpstreamPacket(updateBlockPacket); + + UpdateBlockPacket updateWaterPacket = new UpdateBlockPacket(); + updateWaterPacket.setDataLayer(1); + updateWaterPacket.setBlockPosition(vector); + updateWaterPacket.setDefinition(BlockRegistries.WATERLOGGED.get().get(blockState.javaId()) ? session.getBlockMappings().getBedrockWater() : session.getBlockMappings().getBedrockAir()); + updateWaterPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); + session.sendUpstreamPacket(updateWaterPacket); + + // Reset the item in hand to prevent "missing" blocks + session.getPlayerInventoryHolder().updateSlot(session.getPlayerInventory().getHeldItemSlot()); // TODO test + } + + public static void restoreCorrectBlock(GeyserSession session, Vector3i blockPos) { + restoreCorrectBlock(session, blockPos, session.getGeyser().getWorldManager().blockAt(session, blockPos)); + } + + public static void stopBreakAndRestoreBlock(GeyserSession session, Vector3i vector, BlockState blockState) { + sendBedrockStopBlockBreak(session, vector.toFloat()); + restoreCorrectBlock(session, vector, blockState); + } + + public static boolean blockMatchesPredicate(GeyserSession session, BlockState state, AdventureModePredicate.BlockPredicate predicate) { + if (predicate.getBlocks() != null && !session.getTagCache().isBlock(predicate.getBlocks(), state.block())) { + return false; + } else if (predicate.getProperties() != null) { + List matchers = predicate.getProperties(); + if (!matchers.isEmpty()) { + for (AdventureModePredicate.PropertyMatcher matcher : matchers) { + for (Property property : state.block().propertyKeys()) { + if (matcher.getName().equals(property.name())) { + if (!propertyMatchesPredicate(state, property, matcher)) { + return false; + } + } + } + } + } + } + // Not checking NBT or data components - assume the predicate matches + return true; + } + + private static > boolean propertyMatchesPredicate(BlockState state, Property property, AdventureModePredicate.PropertyMatcher matcher) { + T stateValue = state.getValue(property); + if (matcher.getValue() != null) { + Optional value = property.valueOf(matcher.getValue()); + return value.isPresent() && stateValue.equals(value.get()); + } else { + if (matcher.getMinValue() != null) { + Optional min = property.valueOf(matcher.getMinValue()); + if (min.isEmpty() || stateValue.compareTo(min.get()) < 0) { + return false; + } + } + if (matcher.getMaxValue() != null) { + Optional max = property.valueOf(matcher.getMaxValue()); + if (max.isEmpty() || stateValue.compareTo(max.get()) > 0) { + return false; + } + } + } + + return true; } private BlockUtils() { diff --git a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java index b4fd6b924..02e28167c 100644 --- a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java @@ -61,6 +61,7 @@ public class DimensionUtils { session.getLodestoneCache().clear(); session.getPistonCache().clear(); session.getSkullCache().clear(); + session.getBlockBreakHandler().reset(); changeDimension(session, bedrockDimension);