Skip to content

Commit

Permalink
Start fixing our block breaking code
Browse files Browse the repository at this point in the history
  • Loading branch information
onebeastchris committed Mar 3, 2025
1 parent 1c1684c commit c08bde5
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public ItemMapping getMapping(ItemData data) {
return lightBlock;
}

boolean isBlock = data.getBlockDefinition() != null;
boolean isBlock = data.getBlockDefinition() != null && data.getBlockDefinition().getRuntimeId() != 0;
boolean hasDamage = data.getDamage() != 0;

for (ItemMapping mapping : this.items) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,18 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private BedrockDimension bedrockDimension = this.bedrockOverworldDimension;

/**
* Stores the blockstate of the block being currently broken.
*/
@Setter
private int breakingBlock;

/**
* Stores the block break position of the currently broken block.
*/
@Setter
private Vector3i blockBreakPosition;

@Setter
private Vector3i lastBlockPlacePosition;

Expand Down Expand Up @@ -1594,7 +1603,7 @@ private void startGame() {

startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.SERVER);
startGamePacket.setRewindHistorySize(0);
startGamePacket.setServerAuthoritativeBlockBreaking(false);
startGamePacket.setServerAuthoritativeBlockBreaking(true);

startGamePacket.setServerId("");
startGamePacket.setWorldId("");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,9 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
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, confirms durability change
break;
}
default:
return rejectRequest(request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public void translate(GeyserSession session, InventoryTransactionPacket packet)
break;
case ITEM_USE:
switch (packet.getActionType()) {
// Block placing
case 0 -> {
final Vector3i packetBlockPosition = packet.getBlockPosition();
Vector3i blockPos = BlockUtils.getBlockPosition(packetBlockPosition, packet.getBlockFace());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
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.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;
Expand All @@ -44,6 +46,7 @@
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.translator.item.CustomItemTranslator;
import org.geysermc.geyser.translator.protocol.bedrock.BedrockInventoryTransactionTranslator;
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;
Expand All @@ -62,6 +65,7 @@ static void translate(GeyserSession session, List<PlayerBlockActionData> playerA
session.getBookEditCache().checkForSend();

for (PlayerBlockActionData blockActionData : playerActions) {
GeyserImpl.getInstance().getLogger().error(blockActionData.toString());
handle(session, blockActionData);
}
}
Expand All @@ -70,14 +74,15 @@ private static void handle(GeyserSession session, PlayerBlockActionData blockAct
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
case START_BREAK -> startBlockBreak(session, vector, blockFace);
case BLOCK_CONTINUE_DESTROY -> {
if (session.getGameMode() == GameMode.CREATIVE) {
break;
}
Expand All @@ -86,62 +91,26 @@ private static void handle(GeyserSession session, PlayerBlockActionData blockAct
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 (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);
// The Bedrock client won't send a new start_break packet, but just continue breaking blocks
if (!vector.equals(session.getBlockBreakPosition())) {
GeyserImpl.getInstance().getLogger().error("Invalid block break position! Expected " + session.getBlockBreakPosition() + ", got " + vector);

spawnBlockBreakParticles(session, direction, vector, BlockState.of(blockState));
}
case CONTINUE_BREAK -> {
if (session.getGameMode() == GameMode.CREATIVE) {
// Start breaking new block
startBlockBreak(session, vector, blockFace);
break;
}

if (!canMine(session, vector)) {
return;
}

int breakingBlock = session.getBreakingBlock();
if (breakingBlock == -1) {
// TODO ??????
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());
Expand Down Expand Up @@ -186,6 +155,13 @@ private static void handle(GeyserSession session, PlayerBlockActionData blockAct
}
}

// Bedrock "confirms" that it stopped breaking blocks by sending an abort packet after breaking the block
if (session.getBlockBreakPosition() == null) {
break;
}

session.setBlockBreakPosition(null);

ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, 0);
session.sendDownstreamGamePacket(abortBreakingPacket);

Expand All @@ -198,11 +174,119 @@ private static void handle(GeyserSession session, PlayerBlockActionData blockAct
session.sendUpstreamPacket(stopBreak);
}
// Handled in BedrockInventoryTransactionTranslator
case STOP_BREAK -> {
case BLOCK_PREDICT_DESTROY -> {
breakBlock(session, vector, blockFace);
}
}
}

private static void startBlockBreak(GeyserSession session, Vector3i vector, int blockFace) {
session.setBlockBreakPosition(vector);

// Only send block breaking in the BLOCK_PREDICT_DESTROY case
if (session.getGameMode() == GameMode.CREATIVE) {
return;
}

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.setBlockBreakPosition(vector); // TODO account for fire workaround
session.setBlockBreakStartTime(0);
if (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 stopBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.FINISH_DIGGING, fireBlockPos,
// direction, session.getWorldCache().nextPredictionSequence());
// session.sendDownstreamGamePacket(stopBreakingPacket);
}

ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING,
vector, direction, session.getWorldCache().nextPredictionSequence());
session.sendDownstreamGamePacket(startBreakingPacket);

spawnBlockBreakParticles(session, direction, vector, BlockState.of(blockState));
}

private static void breakBlock(GeyserSession session, Vector3i vector, int blockFace) {
int blockState = session.getGameMode() == GameMode.CREATIVE ?
session.getGeyser().getWorldManager().getBlockAt(session, vector) : session.getBreakingBlock();

session.setLastBlockPlaced(null);
session.setLastBlockPlacePosition(null);

// Same deal with vanilla block placing as above.
if (!session.getWorldBorder().isInsideBorderBoundaries()) {
BedrockInventoryTransactionTranslator.restoreCorrectBlock(session, vector);
return;
}

Vector3f playerPosition = session.getPlayerEntity().getPosition();
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());

// why is this here??? move to start break
if (!BedrockInventoryTransactionTranslator.canInteractWithBlock(session, playerPosition, vector)) {
BedrockInventoryTransactionTranslator.restoreCorrectBlock(session, vector);
return;
}

int sequence = session.getWorldCache().nextPredictionSequence();
session.getWorldCache().markPositionInSequence(vector);
// -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(vector.toFloat());
blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState));
session.sendUpstreamPacket(blockBreakPacket);
session.setBreakingBlock(-1);
session.setBlockBreakPosition(null);

// TODO move
Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, vector);
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, vector, Direction.VALUES[blockFace], sequence);
session.sendDownstreamGamePacket(breakPacket);
}

private static boolean canMine(GeyserSession session, Vector3i vector) {
if (session.isHandsBusy()) {
session.setBreakingBlock(-1);
Expand Down
Loading

0 comments on commit c08bde5

Please sign in to comment.