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:
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 + "]";
|
||||
|
||||
@@ -223,7 +223,7 @@ public class Block {
|
||||
'}';
|
||||
}
|
||||
|
||||
Property<?>[] propertyKeys() {
|
||||
public Property<?>[] propertyKeys() {
|
||||
return propertyKeys;
|
||||
}
|
||||
|
||||
|
||||
@@ -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("");
|
||||
|
||||
524
core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java
vendored
Normal file
524
core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -61,6 +61,7 @@ public class DimensionUtils {
|
||||
session.getLodestoneCache().clear();
|
||||
session.getPistonCache().clear();
|
||||
session.getSkullCache().clear();
|
||||
session.getBlockBreakHandler().reset();
|
||||
|
||||
changeDimension(session, bedrockDimension);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user