mirror of https://github.com/Minestom/Minestom.git
188 lines
9.4 KiB
Java
188 lines
9.4 KiB
Java
package net.minestom.server.listener;
|
|
|
|
import net.minestom.server.MinecraftServer;
|
|
import net.minestom.server.collision.CollisionUtils;
|
|
import net.minestom.server.coordinate.Point;
|
|
import net.minestom.server.coordinate.Vec;
|
|
import net.minestom.server.entity.Entity;
|
|
import net.minestom.server.entity.GameMode;
|
|
import net.minestom.server.entity.Player;
|
|
import net.minestom.server.event.EventDispatcher;
|
|
import net.minestom.server.event.player.PlayerBlockInteractEvent;
|
|
import net.minestom.server.event.player.PlayerBlockPlaceEvent;
|
|
import net.minestom.server.event.player.PlayerUseItemOnBlockEvent;
|
|
import net.minestom.server.instance.Chunk;
|
|
import net.minestom.server.instance.Instance;
|
|
import net.minestom.server.instance.block.Block;
|
|
import net.minestom.server.instance.block.BlockFace;
|
|
import net.minestom.server.instance.block.BlockHandler;
|
|
import net.minestom.server.instance.block.BlockManager;
|
|
import net.minestom.server.instance.block.rule.BlockPlacementRule;
|
|
import net.minestom.server.inventory.PlayerInventory;
|
|
import net.minestom.server.item.ItemStack;
|
|
import net.minestom.server.item.Material;
|
|
import net.minestom.server.network.packet.client.play.ClientPlayerBlockPlacementPacket;
|
|
import net.minestom.server.network.packet.server.play.AcknowledgeBlockChangePacket;
|
|
import net.minestom.server.network.packet.server.play.BlockChangePacket;
|
|
import net.minestom.server.utils.chunk.ChunkUtils;
|
|
import net.minestom.server.utils.validate.Check;
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
public class BlockPlacementListener {
|
|
private static final BlockManager BLOCK_MANAGER = MinecraftServer.getBlockManager();
|
|
|
|
public static void listener(ClientPlayerBlockPlacementPacket packet, Player player) {
|
|
final PlayerInventory playerInventory = player.getInventory();
|
|
final Player.Hand hand = packet.hand();
|
|
final BlockFace blockFace = packet.blockFace();
|
|
Point blockPosition = packet.blockPosition();
|
|
|
|
final Instance instance = player.getInstance();
|
|
if (instance == null)
|
|
return;
|
|
|
|
// Prevent outdated/modified client data
|
|
final Chunk interactedChunk = instance.getChunkAt(blockPosition);
|
|
if (!ChunkUtils.isLoaded(interactedChunk)) {
|
|
// Client tried to place a block in an unloaded chunk, ignore the request
|
|
return;
|
|
}
|
|
|
|
final ItemStack usedItem = player.getItemInHand(hand);
|
|
final Block interactedBlock = instance.getBlock(blockPosition);
|
|
|
|
final Point cursorPosition = new Vec(packet.cursorPositionX(), packet.cursorPositionY(), packet.cursorPositionZ());
|
|
|
|
// Interact at block
|
|
// FIXME: onUseOnBlock
|
|
PlayerBlockInteractEvent playerBlockInteractEvent = new PlayerBlockInteractEvent(player, hand, interactedBlock, blockPosition, cursorPosition, blockFace);
|
|
EventDispatcher.call(playerBlockInteractEvent);
|
|
boolean blockUse = playerBlockInteractEvent.isBlockingItemUse();
|
|
if (!playerBlockInteractEvent.isCancelled()) {
|
|
final var handler = interactedBlock.handler();
|
|
if (handler != null) {
|
|
blockUse |= !handler.onInteract(new BlockHandler.Interaction(interactedBlock, instance, blockPosition, cursorPosition, player, hand));
|
|
}
|
|
}
|
|
if (blockUse) {
|
|
refresh(player, interactedChunk);
|
|
return;
|
|
}
|
|
|
|
final Material useMaterial = usedItem.material();
|
|
if (!useMaterial.isBlock()) {
|
|
// Player didn't try to place a block but interacted with one
|
|
PlayerUseItemOnBlockEvent event = new PlayerUseItemOnBlockEvent(player, hand, usedItem, blockPosition, cursorPosition, blockFace);
|
|
EventDispatcher.call(event);
|
|
// Ack the block change. This is required to reset the client prediction to the server state.
|
|
player.sendPacket(new AcknowledgeBlockChangePacket(packet.sequence()));
|
|
return;
|
|
}
|
|
|
|
// Verify if the player can place the block
|
|
boolean canPlaceBlock = true;
|
|
// Check if the player is allowed to place blocks based on their game mode
|
|
if (player.getGameMode() == GameMode.SPECTATOR) {
|
|
canPlaceBlock = false; // Spectators can't place blocks
|
|
} else if (player.getGameMode() == GameMode.ADVENTURE) {
|
|
//Check if the block can be placed on the block
|
|
canPlaceBlock = usedItem.meta().canPlaceOn(interactedBlock);
|
|
}
|
|
|
|
// Get the newly placed block position
|
|
//todo it feels like it should be possible to have better replacement rules than this, feels pretty scuffed.
|
|
Point placementPosition = blockPosition;
|
|
var interactedPlacementRule = BLOCK_MANAGER.getBlockPlacementRule(interactedBlock);
|
|
if (interactedPlacementRule == null || !interactedPlacementRule.isSelfReplaceable(
|
|
new BlockPlacementRule.Replacement(interactedBlock, blockFace, cursorPosition, useMaterial))) {
|
|
// If the block is not replaceable, try to place next to it.
|
|
final int offsetX = blockFace == BlockFace.WEST ? -1 : blockFace == BlockFace.EAST ? 1 : 0;
|
|
final int offsetY = blockFace == BlockFace.BOTTOM ? -1 : blockFace == BlockFace.TOP ? 1 : 0;
|
|
final int offsetZ = blockFace == BlockFace.NORTH ? -1 : blockFace == BlockFace.SOUTH ? 1 : 0;
|
|
placementPosition = blockPosition.add(offsetX, offsetY, offsetZ);
|
|
|
|
var placementBlock = instance.getBlock(placementPosition);
|
|
var placementRule = BLOCK_MANAGER.getBlockPlacementRule(placementBlock);
|
|
if (!placementBlock.registry().isReplaceable() && (placementRule == null || !placementRule.isSelfReplaceable(
|
|
new BlockPlacementRule.Replacement(placementBlock, blockFace, cursorPosition, useMaterial)))) {
|
|
// If the block is still not replaceable, cancel the placement
|
|
canPlaceBlock = false;
|
|
}
|
|
}
|
|
|
|
if (!canPlaceBlock) {
|
|
// Send a block change with the real block in the instance to keep the client in sync,
|
|
// using refreshChunk results in the client not being in sync
|
|
// after rapid invalid block placements
|
|
final Block block = instance.getBlock(placementPosition);
|
|
player.sendPacket(new BlockChangePacket(placementPosition, block));
|
|
return;
|
|
}
|
|
|
|
final Chunk chunk = instance.getChunkAt(placementPosition);
|
|
Check.stateCondition(!ChunkUtils.isLoaded(chunk),
|
|
"A player tried to place a block in the border of a loaded chunk {0}", placementPosition);
|
|
if (chunk.isReadOnly()) {
|
|
refresh(player, chunk);
|
|
return;
|
|
}
|
|
|
|
final Block placedBlock = useMaterial.block();
|
|
Entity collisionEntity = CollisionUtils.canPlaceBlockAt(instance, placementPosition, placedBlock);
|
|
if (collisionEntity != null) {
|
|
// If a player is trying to place a block on themselves, the client will send a block change but will not set the block on the client
|
|
// For this reason, the block doesn't need to be updated for the client
|
|
|
|
// Client also doesn't predict placement of blocks on entities, but we need to refresh for cases where bounding boxes on the server don't match the client
|
|
if (collisionEntity != player)
|
|
refresh(player, chunk);
|
|
|
|
return;
|
|
}
|
|
|
|
// BlockPlaceEvent check
|
|
PlayerBlockPlaceEvent playerBlockPlaceEvent = new PlayerBlockPlaceEvent(player, placedBlock, blockFace, placementPosition, packet.hand());
|
|
playerBlockPlaceEvent.consumeBlock(player.getGameMode() != GameMode.CREATIVE);
|
|
EventDispatcher.call(playerBlockPlaceEvent);
|
|
if (playerBlockPlaceEvent.isCancelled()) {
|
|
refresh(player, chunk);
|
|
return;
|
|
}
|
|
|
|
// BlockPlacementRule check
|
|
Block resultBlock = playerBlockPlaceEvent.getBlock();
|
|
final BlockPlacementRule blockPlacementRule = BLOCK_MANAGER.getBlockPlacementRule(resultBlock);
|
|
if (blockPlacementRule != null && playerBlockPlaceEvent.shouldDoBlockUpdates()) {
|
|
// Get id from block placement rule instead of the event
|
|
resultBlock = blockPlacementRule.blockPlace(new BlockPlacementRule.PlacementState(
|
|
instance, resultBlock, blockFace,
|
|
placementPosition, cursorPosition,
|
|
player.getPosition(), usedItem.meta(), player.isSneaking())
|
|
);
|
|
}
|
|
if (resultBlock == null) {
|
|
refresh(player, chunk);
|
|
return;
|
|
}
|
|
// Place the block
|
|
player.sendPacket(new AcknowledgeBlockChangePacket(packet.sequence()));
|
|
instance.placeBlock(new BlockHandler.PlayerPlacement(resultBlock, instance, placementPosition, player, hand, blockFace,
|
|
packet.cursorPositionX(), packet.cursorPositionY(), packet.cursorPositionZ()), playerBlockPlaceEvent.shouldDoBlockUpdates());
|
|
// Block consuming
|
|
if (playerBlockPlaceEvent.doesConsumeBlock()) {
|
|
// Consume the block in the player's hand
|
|
final ItemStack newUsedItem = usedItem.consume(1);
|
|
playerInventory.setItemInHand(hand, newUsedItem);
|
|
} else {
|
|
// Prevent invisible item on client
|
|
playerInventory.update();
|
|
}
|
|
}
|
|
|
|
private static void refresh(Player player, Chunk chunk) {
|
|
player.getInventory().update();
|
|
chunk.sendChunk(player);
|
|
}
|
|
}
|