1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-19 14:59:27 +00:00

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 <eclipse@eclipseisoffline.xyz>
This commit is contained in:
chris
2025-09-11 14:51:09 +02:00
committed by GitHub
parent 7dcdf5cbe6
commit 35be9072c7
17 changed files with 805 additions and 415 deletions

View File

@@ -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<GeyserAttributeType, AttributeData> 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<AttributeData> 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) {

View File

@@ -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<String> {
return (T) this.values;
}
@Override
public Optional<String> 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));
}

View File

@@ -25,6 +25,8 @@
package org.geysermc.geyser.level.block.property;
import java.util.Optional;
public final class BooleanProperty extends Property<Boolean> {
private BooleanProperty(String name) {
super(name);
@@ -40,6 +42,16 @@ public final class BooleanProperty extends Property<Boolean> {
return value ? 0 : 1;
}
@Override
public Optional<Boolean> 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);
}

View File

@@ -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<T extends Enum<T>> extends Property<T> {
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<T> valueOf(String string) {
for (T value : values) {
if (value.name().toLowerCase(Locale.ROOT).equals(string)) {
return Optional.of(value);
}
}
return Optional.empty();
}
@SafeVarargs

View File

@@ -25,6 +25,8 @@
package org.geysermc.geyser.level.block.property;
import java.util.Optional;
public final class IntegerProperty extends Property<Integer> {
private final int offset;
private final int valuesCount;
@@ -53,6 +55,16 @@ public final class IntegerProperty extends Property<Integer> {
return this.offset + this.valuesCount;
}
@Override
public Optional<Integer> 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);
}

View File

@@ -25,6 +25,8 @@
package org.geysermc.geyser.level.block.property;
import java.util.Optional;
public abstract class Property<T extends Comparable<T>> {
private final String name;
@@ -40,6 +42,8 @@ public abstract class Property<T extends Comparable<T>> {
public abstract int indexOf(T value);
public abstract Optional<T> valueOf(String string);
@Override
public String toString() {
return getClass().getSimpleName() + "[" + name + "]";

View File

@@ -223,7 +223,7 @@ public class Block {
'}';
}
Property<?>[] propertyKeys() {
public Property<?>[] propertyKeys() {
return propertyKeys;
}

View File

@@ -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("");

View File

@@ -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<Block> 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<Vector3i> 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<Vector3i, Pair<Long, BlockBreakStage>> 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;
}
}
}

View File

@@ -604,7 +604,8 @@ public abstract class InventoryTranslator<Type extends Inventory> {
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:

View File

@@ -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;
}

View File

@@ -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<Inve
default -> false;
};
if (isGodBridging) {
restoreCorrectBlock(session, blockPos);
BlockUtils.restoreCorrectBlock(session, blockPos);
return;
}
}
@@ -209,7 +201,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
int belowBlock = session.getGeyser().getWorldManager().getBlockAt(session, belowBlockPos);
BlockDefinition extendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(belowBlock);
if (extendedCollisionDefinition != null && (System.currentTimeMillis() - session.getLastInteractionTime()) < 200) {
restoreCorrectBlock(session, blockPos);
BlockUtils.restoreCorrectBlock(session, blockPos);
return;
}
}
@@ -229,7 +221,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
if (isIncorrectHeldItem(session, packet)) {
restoreCorrectBlock(session, blockPos);
BlockUtils.restoreCorrectBlock(session, blockPos);
return;
}
@@ -249,7 +241,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
*/
// Blocks cannot be placed or destroyed outside of the world border
if (!session.getWorldBorder().isInsideBorderBoundaries()) {
restoreCorrectBlock(session, blockPos);
BlockUtils.restoreCorrectBlock(session, blockPos);
return;
}
@@ -258,7 +250,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
if (!canInteractWithBlock(session, playerPosition, packetBlockPosition)) {
restoreCorrectBlock(session, blockPos);
BlockUtils.restoreCorrectBlock(session, blockPos);
return;
}
@@ -272,7 +264,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
double clickDistanceY = clickPositionFullY - blockCenter.getY();
double clickDistanceZ = clickPositionFullZ - blockCenter.getZ();
if (!(Math.abs(clickDistanceX) < 1.0000001D && Math.abs(clickDistanceY) < 1.0000001D && Math.abs(clickDistanceZ) < 1.0000001D)) {
restoreCorrectBlock(session, blockPos);
BlockUtils.restoreCorrectBlock(session, blockPos);
return;
}
@@ -553,42 +545,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
return ((diffX * diffX) + (diffY * diffY) + (diffZ * diffZ)) < (additionalRangeCheck * additionalRangeCheck);
}
/**
* Restore the correct block state from the server without updating the chunk cache.
*
* @param session the session of the Bedrock client
* @param blockPos the block position to restore
*/
public static void restoreCorrectBlock(GeyserSession session, Vector3i blockPos) {
BlockState javaBlockState = session.getGeyser().getWorldManager().blockAt(session, blockPos);
BlockDefinition bedrockBlock = session.getBlockMappings().getBedrockBlock(javaBlockState);
if (javaBlockState.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(blockPos);
if (skull != null && skull.getBlockDefinition() != null) {
bedrockBlock = skull.getBlockDefinition();
}
}
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(blockPos);
updateBlockPacket.setDefinition(bedrockBlock);
updateBlockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
session.sendUpstreamPacket(updateBlockPacket);
UpdateBlockPacket updateWaterPacket = new UpdateBlockPacket();
updateWaterPacket.setDataLayer(1);
updateWaterPacket.setBlockPosition(blockPos);
updateWaterPacket.setDefinition(BlockRegistries.WATERLOGGED.get().get(javaBlockState.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
}
private boolean isIncorrectHeldItem(GeyserSession session, InventoryTransactionPacket packet) {
int javaSlot = session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot());
ItemDefinition expectedItem = ItemTranslator.getBedrockItemDefinition(session, session.getPlayerInventory().getItem(javaSlot));

View File

@@ -1,237 +0,0 @@
/*
* Copyright (c) 2019-2024 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.translator.protocol.bedrock.entity.player.input;
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.PlayerActionType;
import org.cloudburstmc.protocol.bedrock.data.PlayerBlockActionData;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
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.session.cache.SkullCache;
import org.geysermc.geyser.translator.item.CustomItemTranslator;
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.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.packet.ingame.serverbound.player.ServerboundInteractPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import java.util.List;
final class BedrockBlockActions {
static void translate(GeyserSession session, List<PlayerBlockActionData> 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);
}
}

View File

@@ -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<Pla
boolean wasJumping = session.getInputCache().wasJumping();
session.getInputCache().processInputs(entity, packet);
session.getBlockBreakHandler().handlePlayerAuthInputPacket(packet);
ServerboundPlayerCommandPacket sprintPacket = null;
@@ -92,7 +82,7 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator<Pla
leftOverInputData.remove(input);
switch (input) {
case PERFORM_ITEM_INTERACTION -> 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<Pla
private static void processItemUseTransaction(GeyserSession session, ItemUseTransaction transaction) {
if (transaction.getActionType() == 2) {
int blockState = session.getGameMode() == GameMode.CREATIVE ?
session.getGeyser().getWorldManager().getBlockAt(session, transaction.getBlockPosition()) : session.getBreakingBlock();
session.setLastBlockPlaced(null);
session.setLastBlockPlacePosition(null);
// Same deal with vanilla block placing as above.
if (!session.getWorldBorder().isInsideBorderBoundaries()) {
BedrockInventoryTransactionTranslator.restoreCorrectBlock(session, transaction.getBlockPosition());
return;
}
Vector3f playerPosition = session.getPlayerEntity().getPosition();
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
if (!BedrockInventoryTransactionTranslator.canInteractWithBlock(session, playerPosition, transaction.getBlockPosition())) {
BedrockInventoryTransactionTranslator.restoreCorrectBlock(session, transaction.getBlockPosition());
return;
}
int sequence = session.getWorldCache().nextPredictionSequence();
session.getWorldCache().markPositionInSequence(transaction.getBlockPosition());
// -1 means we don't know what block they're breaking
if (blockState == -1) {
blockState = Block.JAVA_AIR_ID;
}
LevelEventPacket blockBreakPacket = new LevelEventPacket();
blockBreakPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK);
blockBreakPacket.setPosition(transaction.getBlockPosition().toFloat());
blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState));
session.sendUpstreamPacket(blockBreakPacket);
session.setBreakingBlock(-1);
Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, transaction.getBlockPosition());
if (itemFrameEntity != null) {
ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(itemFrameEntity.getEntityId(),
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamGamePacket(attackPacket);
return;
}
PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING;
ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, transaction.getBlockPosition(), Direction.VALUES[transaction.getBlockFace()], sequence);
session.sendDownstreamGamePacket(breakPacket);
} else {
session.getGeyser().getLogger().error("Unhandled item use transaction type!");
if (session.getGeyser().getLogger().isDebug()) {

View File

@@ -25,14 +25,14 @@
package org.geysermc.geyser.translator.protocol.java.level;
import it.unimi.dsi.fastutil.Pair;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.BlockUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.BlockBreakStage;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundBlockDestructionPacket;
@Translator(packet = ClientboundBlockDestructionPacket.class)
@@ -40,33 +40,34 @@ public class JavaBlockDestructionTranslator extends PacketTranslator<Clientbound
@Override
public void translate(GeyserSession session, ClientboundBlockDestructionPacket packet) {
int state = session.getGeyser().getWorldManager().getBlockAt(session, packet.getPosition().getX(), packet.getPosition().getY(), packet.getPosition().getZ());
int breakTime = 12; //(int) (65535 / Math.ceil(BlockUtils.getBreakTime(session, BlockState.of(state).block(), ItemMapping.AIR, null, false)));
// TODO we need to send a "total" time to Bedrock.
// Current plan:
// - start with block destroy time (if applicable)
// - track the time in ticks between stages
// - attempt to "extrapolate" to a value for Bedrock
if (packet.getStage() == BlockBreakStage.RESET) {
// Invalidate the position now that it's not being broken anymore
session.getBlockBreakHandler().getDestructionStageCache().invalidate(packet.getPosition());
BlockUtils.sendBedrockStopBlockBreak(session, packet.getPosition().toFloat());
return;
}
// Bedrock wants a total destruction time, not a stage - so we estimate!
LevelEventPacket levelEventPacket = new LevelEventPacket();
levelEventPacket.setPosition(packet.getPosition().toFloat());
levelEventPacket.setType(LevelEvent.BLOCK_START_BREAK);
switch (packet.getStage()) {
case STAGE_1 -> 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<Long, BlockBreakStage> 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);
}
}

View File

@@ -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<AdventureModePredicate.PropertyMatcher> 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 <T extends Comparable<T>> boolean propertyMatchesPredicate(BlockState state, Property<T> property, AdventureModePredicate.PropertyMatcher matcher) {
T stateValue = state.getValue(property);
if (matcher.getValue() != null) {
Optional<T> value = property.valueOf(matcher.getValue());
return value.isPresent() && stateValue.equals(value.get());
} else {
if (matcher.getMinValue() != null) {
Optional<T> min = property.valueOf(matcher.getMinValue());
if (min.isEmpty() || stateValue.compareTo(min.get()) < 0) {
return false;
}
}
if (matcher.getMaxValue() != null) {
Optional<T> max = property.valueOf(matcher.getMaxValue());
if (max.isEmpty() || stateValue.compareTo(max.get()) > 0) {
return false;
}
}
}
return true;
}
private BlockUtils() {

View File

@@ -61,6 +61,7 @@ public class DimensionUtils {
session.getLodestoneCache().clear();
session.getPistonCache().clear();
session.getSkullCache().clear();
session.getBlockBreakHandler().reset();
changeDimension(session, bedrockDimension);