From de5741f69323f8cd73b2059fb603f937e9b712af Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Tue, 11 Jun 2024 00:50:47 +0800 Subject: [PATCH] feat: server auth block breaking (WIP) --- .../component/common/BlockBaseComponent.java | 21 +-- .../java/org/allaymc/api/world/Dimension.java | 6 +- ...ockEntityContainerHolderComponentImpl.java | 6 + .../PlayerActionPacketProcessor.java | 24 +++ .../PlayerAuthInputPacketProcessor.java | 136 +++++++++++++++-- docs/protocol_research/block_breaking.zh.md | 142 ------------------ .../block_breaking_logic.zh.md | 0 .../block_breaking_network_packets.zh.md | 78 ++++++++++ 8 files changed, 250 insertions(+), 163 deletions(-) delete mode 100644 docs/protocol_research/block_breaking.zh.md create mode 100644 docs/protocol_research/server_auth_block_breaking/block_breaking_logic.zh.md create mode 100644 docs/protocol_research/server_auth_block_breaking/block_breaking_network_packets.zh.md diff --git a/Allay-API/src/main/java/org/allaymc/api/block/component/common/BlockBaseComponent.java b/Allay-API/src/main/java/org/allaymc/api/block/component/common/BlockBaseComponent.java index 178c6f31e..f1a32d6f8 100644 --- a/Allay-API/src/main/java/org/allaymc/api/block/component/common/BlockBaseComponent.java +++ b/Allay-API/src/main/java/org/allaymc/api/block/component/common/BlockBaseComponent.java @@ -6,9 +6,7 @@ import org.allaymc.api.block.property.type.BlockPropertyType; import org.allaymc.api.block.type.BlockState; import org.allaymc.api.block.type.BlockType; -import org.allaymc.api.block.type.BlockTypes; import org.allaymc.api.container.FullContainerType; -import org.allaymc.api.container.impl.PlayerArmorContainer; import org.allaymc.api.data.VanillaItemTags; import org.allaymc.api.entity.Entity; import org.allaymc.api.entity.component.common.EntityContainerHolderComponent; @@ -17,7 +15,6 @@ import org.allaymc.api.item.ItemStack; import org.allaymc.api.item.enchantment.type.EnchantmentAquaAffinityType; import org.allaymc.api.item.enchantment.type.EnchantmentEfficiencyType; -import org.allaymc.api.item.tag.ItemTag; import org.allaymc.api.world.Dimension; /** @@ -52,6 +49,7 @@ default double calculateBreakTime(BlockState blockState, ItemStack usedItem, Ent double blockHardness = blockState.getBlockAttributes().hardness(); if (blockHardness == 0) return 0; boolean isCorrectTool = isCorrectTool(blockState, usedItem); + boolean canHarvestWithHand = canHarvestWithHand(); boolean hasAquaAffinity = false; boolean isInWater = false; boolean isOnGround = true; @@ -79,17 +77,13 @@ default double calculateBreakTime(BlockState blockState, ItemStack usedItem, Ent } // Calculate break time - double speed = 1.0d / blockHardness; + double baseTime = ((isCorrectTool || canHarvestWithHand) ? 1.5 : 5.0) * blockHardness; + double speed = 1.0d / baseTime; if (isCorrectTool) { - // 若是正确的工具,初始速度除以1.5 - speed /= 1.5d; // 工具等级(木制,石制,铁制,etc...)加成 speed *= toolBreakTimeBonus(usedItem, blockState); // 工具效率附魔加成 speed += speedBonusByEfficiency(efficiencyLevel); - } else { - // 若不是正确的工具,初始速度除以5 - speed /= 5.0d; } // 实体急迫药水效果加成 speed *= 1.0d + (0.2d * hasteEffectLevel); @@ -124,6 +118,15 @@ default boolean isCorrectTool(BlockState blockState, ItemStack usedItem) { return false; } + /** + * 部分方块(例如石头,黑曜石)不可以被徒手挖去,强行挖取它们不会掉落任何物品, + * 且挖掘速度会受到惩罚(baseTime增大5倍,正常是1.5倍) + * @return 是否可以徒手挖取 + */ + default boolean canHarvestWithHand() { + return true; + } + private void checkBlockType(BlockState blockState) { if (blockState.getBlockType() != getBlockType()) throw new IllegalArgumentException("Block type is not match! Expected: " + getBlockType().getIdentifier() + ", actual: " + blockState.getBlockType().getIdentifier()); } diff --git a/Allay-API/src/main/java/org/allaymc/api/world/Dimension.java b/Allay-API/src/main/java/org/allaymc/api/world/Dimension.java index 629d4c00f..ae73c928c 100644 --- a/Allay-API/src/main/java/org/allaymc/api/world/Dimension.java +++ b/Allay-API/src/main/java/org/allaymc/api/world/Dimension.java @@ -266,11 +266,11 @@ default BlockState[][][] getCollidingBlocks(AABBfc aabb, int layer, boolean igno return notEmpty ? blockStates : null; } - default void addLevelEvent(Vector3i pos, LevelEventType levelEventType, int data) { - var chunk = getChunkService().getChunk(pos.getX() >> 4, pos.getZ() >> 4); + default void addLevelEvent(float x, float y, float z, LevelEventType levelEventType, int data) { + var chunk = getChunkService().getChunk((int) x >> 4, (int) z >> 4); if (chunk == null) return; var levelEventPacket = new LevelEventPacket(); - levelEventPacket.setPosition(pos.toFloat()); + levelEventPacket.setPosition(Vector3f.from(x, y, z)); levelEventPacket.setType(levelEventType); levelEventPacket.setData(data); chunk.sendChunkPacket(levelEventPacket); diff --git a/Allay-Server/src/main/java/org/allaymc/server/blockentity/component/common/BlockEntityContainerHolderComponentImpl.java b/Allay-Server/src/main/java/org/allaymc/server/blockentity/component/common/BlockEntityContainerHolderComponentImpl.java index f8f016b93..ee4982bbc 100644 --- a/Allay-Server/src/main/java/org/allaymc/server/blockentity/component/common/BlockEntityContainerHolderComponentImpl.java +++ b/Allay-Server/src/main/java/org/allaymc/server/blockentity/component/common/BlockEntityContainerHolderComponentImpl.java @@ -5,6 +5,7 @@ import org.allaymc.api.blockentity.component.common.BlockEntityBaseComponent; import org.allaymc.api.blockentity.component.common.BlockEntityContainerHolderComponent; import org.allaymc.api.blockentity.component.event.BlockEntityLoadNBTEvent; +import org.allaymc.api.container.FullContainerType; import org.allaymc.api.utils.Identifier; import org.allaymc.api.component.annotation.ComponentIdentifier; import org.allaymc.api.component.annotation.Dependency; @@ -92,6 +93,11 @@ private void onReplace(BlockOnReplaceEvent event) { } } + @Override + public boolean hasContainer(FullContainerType type) { + return container.getContainerType() == type; + } + @Override public T getContainerBySlotType(ContainerSlotType slotType) { // BlockEntityContainerHolder can only hold one container in its lifetime diff --git a/Allay-Server/src/main/java/org/allaymc/server/network/processor/PlayerActionPacketProcessor.java b/Allay-Server/src/main/java/org/allaymc/server/network/processor/PlayerActionPacketProcessor.java index 08a31aa00..a2c9b7946 100644 --- a/Allay-Server/src/main/java/org/allaymc/server/network/processor/PlayerActionPacketProcessor.java +++ b/Allay-Server/src/main/java/org/allaymc/server/network/processor/PlayerActionPacketProcessor.java @@ -1,13 +1,21 @@ package org.allaymc.server.network.processor; +import lombok.extern.slf4j.Slf4j; import org.allaymc.api.entity.interfaces.EntityPlayer; import org.allaymc.api.math.location.Location3f; import org.allaymc.api.network.processor.PacketProcessor; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.GameType; +import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacketType; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket; import org.cloudburstmc.protocol.common.PacketSignal; +import static org.allaymc.api.block.type.BlockTypes.AIR_TYPE; + +@Slf4j public class PlayerActionPacketProcessor extends PacketProcessor { @Override public PacketSignal handleAsync(EntityPlayer player, PlayerActionPacket packet) { @@ -30,6 +38,22 @@ public PacketSignal handleAsync(EntityPlayer player, PlayerActionPacket packet) player.sendDimensionChangeSuccess(); yield PacketSignal.HANDLED; } + case DIMENSION_CHANGE_REQUEST_OR_CREATIVE_DESTROY_BLOCK -> { + if (player.getGameType() != GameType.CREATIVE) { + log.warn("Player {} tried to creative destroy block in non-creative mode", player.getOriginName()); + yield PacketSignal.HANDLED; + } + var pos = packet.getBlockPosition(); + var world = player.getDimension(); + var oldState = world.getBlockState(pos.getX(), pos.getY(), pos.getZ()); + var pk = new LevelEventPacket(); + pk.setType(LevelEvent.PARTICLE_DESTROY_BLOCK); + pk.setPosition(Vector3f.from(pos.getX() + 0.5f, pos.getY() + 0.5f, pos.getZ() + 0.5f)); + pk.setData(oldState.blockStateHash()); + player.getCurrentChunk().addChunkPacket(pk); + world.setBlockState(pos.getX(), pos.getY(), pos.getZ(), AIR_TYPE.getDefaultState()); + yield PacketSignal.HANDLED; + } default -> PacketSignal.UNHANDLED; }; } diff --git a/Allay-Server/src/main/java/org/allaymc/server/network/processor/PlayerAuthInputPacketProcessor.java b/Allay-Server/src/main/java/org/allaymc/server/network/processor/PlayerAuthInputPacketProcessor.java index b5e725061..aaf403c3a 100644 --- a/Allay-Server/src/main/java/org/allaymc/server/network/processor/PlayerAuthInputPacketProcessor.java +++ b/Allay-Server/src/main/java/org/allaymc/server/network/processor/PlayerAuthInputPacketProcessor.java @@ -1,29 +1,38 @@ package org.allaymc.server.network.processor; +import lombok.extern.slf4j.Slf4j; +import org.allaymc.api.block.data.BlockFace; +import org.allaymc.api.block.type.BlockState; +import org.allaymc.api.container.FullContainerType; import org.allaymc.api.entity.interfaces.EntityPlayer; import org.allaymc.api.math.location.Location3f; import org.allaymc.api.network.processor.PacketProcessor; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.GameType; 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.packet.BedrockPacketType; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; import org.cloudburstmc.protocol.common.PacketSignal; import java.util.List; +import java.util.Objects; import java.util.Set; import static org.allaymc.api.block.type.BlockTypes.AIR_TYPE; +import static org.cloudburstmc.protocol.bedrock.data.LevelEvent.*; /** * Allay Project 11/22/2023 * * @author Cool_Loong */ +@Slf4j public class PlayerAuthInputPacketProcessor extends PacketProcessor { - protected static void handleMovement(EntityPlayer player, Vector3f newPos, Vector3f newRot) { + protected void handleMovement(EntityPlayer player, Vector3f newPos, Vector3f newRot) { var world = player.getLocation().dimension(); world.getEntityPhysicsService().offerScheduledMove( player, @@ -35,24 +44,131 @@ protected static void handleMovement(EntityPlayer player, Vector3f newPos, Vecto ); } - protected static void handleBlockAction(EntityPlayer player, List blockActions) { + protected void handleBlockAction(EntityPlayer player, List blockActions) { if (blockActions.isEmpty()) return; - var world = player.getLocation().dimension(); for (var action : blockActions) { var pos = action.getBlockPosition(); switch (action.getAction()) { - case START_BREAK -> world.addLevelEvent(pos, LevelEvent.BLOCK_START_BREAK, 0); + case START_BREAK -> { + if (isInvalidGameType(player)) continue; + startBreak(player, pos.getX(), pos.getY(), pos.getZ(), action.getFace()); + } + case BLOCK_CONTINUE_DESTROY -> { + // 当玩家破坏一个方块一半时转而破坏另一个方块 + if (isInvalidGameType(player)) continue; + // HACK: 客户端不知道为什么会在BLOCK_PREDICT_DESTROY前发个无意义的BLOCK_CONTINUE_DESTROY,应该是bug,这里忽略掉 + if (beBreakingBlockX == pos.getX() && beBreakingBlockY == pos.getY() && beBreakingBlockZ == pos.getZ()) continue; + startBreak(player, pos.getX(), pos.getY(), pos.getZ(), action.getFace()); + } case BLOCK_PREDICT_DESTROY -> { - var oldState = world.getBlockState(pos.getX(), pos.getY(), pos.getZ()); - world.setBlockState(pos.getX(), pos.getY(), pos.getZ(), AIR_TYPE.getDefaultState()); - world.addLevelEvent(pos, LevelEvent.BLOCK_STOP_BREAK, 0); - world.addLevelEvent(pos, LevelEvent.PARTICLE_DESTROY_BLOCK, oldState.blockStateHash()); + if (isInvalidGameType(player)) continue; + completeBreak(player, pos.getX(), pos.getY(), pos.getZ()); + } + case ABORT_BREAK -> { + // 挖掘中断 + if (isInvalidGameType(player)) continue; + stopBreak(player); } } } } - protected static void handleInputData(EntityPlayer player, Set inputData) { + private static boolean isInvalidGameType(EntityPlayer player) { + return player.getGameType() == GameType.CREATIVE || player.getGameType() == GameType.SPECTATOR; + } + + protected int beBreakingBlockX = Integer.MAX_VALUE; + protected int beBreakingBlockY = Integer.MAX_VALUE; + protected int beBreakingBlockZ = Integer.MAX_VALUE; + protected int beBreakingFaceId; + protected BlockState beBreakingBlock; + protected long startBreakingTime; + protected double neededBreakingTime; + + protected boolean isBreakingBlock() { + return beBreakingBlock != null; + } + + protected void startBreak(EntityPlayer player, int x, int y, int z, int blockFaceId) { + if (beBreakingBlock != null) { + log.warn("Player {} tried to start breaking a block while already breaking one", player.getOriginName()); + stopBreak(player); + } + if (beBreakingBlockX == x && beBreakingBlockY == y && beBreakingBlockZ == z) { + log.warn("Player {} tried to start breaking the same block twice", player.getOriginName()); + return; + } + beBreakingBlockX = x; + beBreakingBlockY = y; + beBreakingBlockZ = z; + beBreakingFaceId = blockFaceId; + beBreakingBlock = player.getDimension().getBlockState(x, y, z); + startBreakingTime = player.getWorld().getTick(); + neededBreakingTime = beBreakingBlock.getBlockType().getBlockBehavior().calculateBreakTime(beBreakingBlock, player.getContainer(FullContainerType.PLAYER_INVENTORY).getItemInHand(), player); + var pk = new LevelEventPacket(); + pk.setType(BLOCK_START_BREAK); + pk.setPosition(Vector3f.from(x, y, z)); + pk.setData((int) (65535 / (neededBreakingTime * 20))); + player.getCurrentChunk().addChunkPacket(pk); + broadcastBreakingPracticeAndTime(player); + } + + protected void stopBreak(EntityPlayer player) { + var pk = new LevelEventPacket(); + pk.setType(BLOCK_STOP_BREAK); + pk.setPosition(Vector3f.from(beBreakingBlockX, beBreakingBlockY, beBreakingBlockZ)); + pk.setData(0); + player.getCurrentChunk().addChunkPacket(pk); + beBreakingBlockX = Integer.MAX_VALUE; + beBreakingBlockY = Integer.MAX_VALUE; + beBreakingBlockZ = Integer.MAX_VALUE; + beBreakingFaceId = 0; + beBreakingBlock = null; + startBreakingTime = 0; + neededBreakingTime = 0; + } + + protected static final int BLOCK_BREAKING_TIME_FAULT_TOLERANCE = 5; + + protected void completeBreak(EntityPlayer player, int x, int y, int z) { + if (beBreakingBlockX != x || beBreakingBlockY != y || beBreakingBlockZ != z) { + log.warn("Player {} tried to complete breaking a different block", player.getOriginName()); + return; + } + var currentTime = player.getWorld().getTick(); + var actualBlockBreakingTime = currentTime - startBreakingTime; + if (Math.abs(actualBlockBreakingTime - neededBreakingTime * 20L) <= BLOCK_BREAKING_TIME_FAULT_TOLERANCE) { + var world = player.getDimension(); + var oldState = world.getBlockState(beBreakingBlockX, beBreakingBlockY, beBreakingBlockZ); + var pk = new LevelEventPacket(); + pk.setType(LevelEvent.PARTICLE_DESTROY_BLOCK); + pk.setPosition(Vector3f.from(beBreakingBlockX + 0.5f, beBreakingBlockY + 0.5f, beBreakingBlockZ + 0.5f)); + pk.setData(oldState.blockStateHash()); + player.getCurrentChunk().addChunkPacket(pk); + world.setBlockState(beBreakingBlockX, beBreakingBlockY, beBreakingBlockZ, AIR_TYPE.getDefaultState()); + } else { + log.warn("Mismatch block breaking time! Expected: {}gt, actual: {}gt", neededBreakingTime * 20, actualBlockBreakingTime); + } + stopBreak(player); + } + + protected void broadcastBreakingPracticeAndTime(EntityPlayer player) { + var pk1 = new LevelEventPacket(); + pk1.setType(PARTICLE_CRACK_BLOCK); + var blockFaceOffset = Objects.requireNonNull(BlockFace.fromId(beBreakingFaceId)).getOffset(); + pk1.setPosition(Vector3f.from(beBreakingBlockX + 0.5f + blockFaceOffset.x(), beBreakingBlockY + 0.5f + blockFaceOffset.y(), beBreakingBlockZ + 0.5f + blockFaceOffset.z())); + pk1.setData(beBreakingBlock.blockStateHash()); + + var pk2 = new LevelEventPacket(); + pk2.setType(BLOCK_UPDATE_BREAK); + pk2.setPosition(Vector3f.from(beBreakingBlockX, beBreakingBlockY, beBreakingBlockZ)); + pk2.setData((int) (65535 / (neededBreakingTime * 20))); + + player.getCurrentChunk().addChunkPacket(pk1); + player.getCurrentChunk().addChunkPacket(pk2); + } + + protected void handleInputData(EntityPlayer player, Set inputData) { for (var input : inputData) { switch (input) { case START_SPRINTING -> player.setSprinting(true); @@ -72,6 +188,7 @@ protected static void handleInputData(EntityPlayer player, Set