/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * 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. */ package org.lanternpowered.server.entity.living.player; import com.flowpowered.math.vector.Vector3d; import com.flowpowered.math.vector.Vector3i; import org.lanternpowered.server.behavior.BehaviorContextImpl; import org.lanternpowered.server.behavior.Parameters; import org.lanternpowered.server.block.LanternBlockType; import org.lanternpowered.server.block.behavior.types.BreakBlockBehavior; import org.lanternpowered.server.block.behavior.types.InteractWithBlockBehavior; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.inventory.entity.OffHandSlot; import org.lanternpowered.server.inventory.slot.LanternSlot; import org.lanternpowered.server.item.LanternItemType; import org.lanternpowered.server.item.behavior.types.InteractWithItemBehavior; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayInPlayerBlockPlacement; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayInPlayerDigging; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayInPlayerUseItem; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutBlockBreakAnimation; import org.lanternpowered.server.world.LanternWorld; import org.spongepowered.api.block.BlockState; import org.spongepowered.api.block.BlockType; import org.spongepowered.api.block.BlockTypes; import org.spongepowered.api.data.key.Keys; import org.spongepowered.api.data.property.block.HardnessProperty; import org.spongepowered.api.data.property.block.UnbreakableProperty; import org.spongepowered.api.data.type.HandType; import org.spongepowered.api.data.type.HandTypes; import org.spongepowered.api.entity.living.player.gamemode.GameModes; import org.spongepowered.api.event.cause.Cause; import org.spongepowered.api.item.inventory.ItemStack; import org.spongepowered.api.util.Direction; import org.spongepowered.api.world.Location; import org.spongepowered.api.world.World; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; public final class PlayerInteractionHandler { private final LanternPlayer player; /** * The block that is being digged. */ @Nullable private Vector3i diggingBlock; @Nullable private BlockType diggingBlockType; /** * The time when the digging should end. */ private long diggingEndTime; /** * The amount of time the digging takes. */ private long diggingDuration; /** * The last send break state. */ private int lastBreakState = -1; private long lastInteractionTime = -1; public PlayerInteractionHandler(LanternPlayer player) { this.player = player; } /** * Resets the player interaction handler, is called when the * player gets teleported to a different world or is removed. */ void reset() { this.sendBreakUpdate(-1); } /** * Pulses the interaction handler. */ void pulse() { if (this.diggingBlock != null) { int breakState = (int) Math.round(((double) Math.max(0, this.diggingEndTime - System.nanoTime()) / (double) this.diggingDuration) * 10.0); if (this.lastBreakState != breakState) { this.sendBreakUpdate(breakState); this.lastBreakState = breakState; } } } public void handleUseItem(MessagePlayInPlayerUseItem message) { } /** * TODO: Maybe also send this to the player this handler is attached to for * custom break times? Only allowed by faster breaking. */ private void sendBreakUpdate(int breakState) { LanternWorld world = this.player.getWorld(); if (world == null) { return; } final Set<LanternPlayer> players = this.player.getWorld().getRawPlayers(); // Update for all the players except the breaker if (players.size() - 1 <= 0) { final MessagePlayOutBlockBreakAnimation message = new MessagePlayOutBlockBreakAnimation( this.diggingBlock, this.player.getNetworkId(), breakState); players.forEach(player -> { if (player != this.player) { player.getConnection().send(message); } }); } } /** * Handles the {@link MessagePlayInPlayerDigging}. * * @param message The message */ public void handleDigging(MessagePlayInPlayerDigging message) { final MessagePlayInPlayerDigging.Action action = message.getAction(); final Vector3i blockPos = message.getPosition(); if (action == MessagePlayInPlayerDigging.Action.START) { // Check if the block is within the players reach if (this.player.getPosition().distanceSquared(blockPos.toDouble().add(0.5, 2.0, 0.5)) > 6.0 * 6.0) { return; } if (this.diggingBlock != null) { Lantern.getLogger().warn("{} started breaking a block without finishing the last one.", this.player.getName()); } BlockType blockType = this.player.getWorld().getBlockType(blockPos); if (blockType == BlockTypes.AIR) { return; } this.diggingBlock = blockPos; this.diggingBlockType = blockType; this.diggingDuration = getDiggingDuration(blockPos, blockType); // The client won't send a finish message if (this.diggingDuration == 0) { handleBrokenBlock(); } else { this.diggingEndTime = this.diggingDuration == -1 ? -1 : System.nanoTime() + this.diggingDuration; } } else if (action == MessagePlayInPlayerDigging.Action.CANCEL) { if (this.diggingBlock == null || !this.diggingBlock.equals(blockPos)) { return; } if (this.lastBreakState != -1) { sendBreakUpdate(-1); } this.diggingBlock = null; this.diggingBlockType = null; } else { if (this.diggingBlock == null) { return; } BlockType blockType = this.player.getWorld().getBlockType(blockPos); if (blockType != this.diggingBlockType) { return; } if (this.diggingEndTime == -1) { Lantern.getLogger().warn("{} attempted to break a unbreakable block.", this.player.getName()); } else { long deltaTime = System.nanoTime() - this.diggingEndTime; if (deltaTime < 0) { Lantern.getLogger().warn("{} finished breaking a block too early, {}ms too fast.", this.player.getName(), -(deltaTime / 1000)); } this.handleBrokenBlock(); } } } private void handleBrokenBlock() { //noinspection ConstantConditions final Location<World> location = new Location<>(this.player.getWorld(), this.diggingBlock); final BehaviorContextImpl context = new BehaviorContextImpl(Cause.source(this.player).build()); context.set(Parameters.PLAYER, this.player); context.set(Parameters.INTERACTION_LOCATION, location); context.set(Parameters.BLOCK_LOCATION, location); final BlockState blockState = location.getBlock(); final LanternBlockType blockType = (LanternBlockType) blockState.getType(); if (context.process(blockType.getPipeline().pipeline(BreakBlockBehavior.class), (ctx, behavior) -> behavior.tryBreak(blockType.getPipeline(), ctx))) { context.accept(); this.diggingBlock = null; this.diggingBlockType = null; } else { // TODO: Resend tile entity data, action data, ... ??? this.player.sendBlockChange(this.diggingBlock, blockState); } if (this.lastBreakState != -1) { sendBreakUpdate(-1); } } /** * Gets the amount of time it should take to break a block. * * @return The digging duration */ private long getDiggingDuration(Vector3i pos, BlockType blockType) { if (this.player.get(Keys.GAME_MODE).get() == GameModes.CREATIVE) { return 0; } final World world = this.player.getWorld(); final Optional<UnbreakableProperty> optUnbreakableProperty = world.getProperty(pos, UnbreakableProperty.class); if (optUnbreakableProperty.isPresent() && optUnbreakableProperty.get().getValue() == Boolean.TRUE) { return -1L; } final Optional<HardnessProperty> optHardnessProperty = world.getProperty(pos, HardnessProperty.class); if (optHardnessProperty.isPresent()) { final Double value = optHardnessProperty.get().getValue(); double hardness = value == null ? 0 : value; // TODO: Calculate the duration return hardness <= 0 ? 0 : 1; } return 0; } public void handleBlockPlacing(MessagePlayInPlayerBlockPlacement message) { final HandType handType = message.getHandType(); // Ignore the off hand interaction type for now, a main hand message // will always be send before this message. So we will only listen for // the main hand message. if (handType == HandTypes.OFF_HAND) { return; } // Try the action of the hotbar item first final LanternSlot hotbarSlot = this.player.getInventory().getHotbar().getSelectedSlot(); final OffHandSlot offHandSlot = this.player.getInventory().getOffhand(); // The offset can round up to 1, causing // an incorrect clicked block location final Vector3d pos2 = message.getClickOffset(); final double dx = Math.min(pos2.getX(), 0.999); final double dy = Math.min(pos2.getY(), 0.999); final double dz = Math.min(pos2.getZ(), 0.999); final Location<World> clickedLocation = new Location<>(this.player.getWorld(), message.getPosition().toDouble().add(dx, dy, dz)); final Direction face = message.getFace(); final BehaviorContextImpl context = new BehaviorContextImpl(Cause.source(this.player).build()); context.set(Parameters.INTERACTION_FACE, face); context.set(Parameters.INTERACTION_LOCATION, clickedLocation); context.set(Parameters.PLAYER, this.player); final BehaviorContextImpl.Snapshot snapshot = context.createSnapshot(); if (!this.player.get(Keys.IS_SNEAKING).orElse(false)) { final BlockState blockState = this.player.getWorld().getBlock(message.getPosition()); final LanternBlockType blockType = (LanternBlockType) blockState.getType(); context.set(Parameters.BLOCK_TYPE, blockState.getType()); context.set(Parameters.USED_BLOCK_STATE, blockState); final BehaviorContextImpl.Snapshot snapshot1 = context.createSnapshot(); context.set(Parameters.USED_ITEM_STACK, hotbarSlot.peek().orElse(null)); context.set(Parameters.USED_SLOT, hotbarSlot); context.set(Parameters.INTERACTION_HAND, HandTypes.MAIN_HAND); if (context.process(blockType.getPipeline().pipeline(InteractWithBlockBehavior.class), (ctx, behavior) -> behavior.tryInteract(blockType.getPipeline(), ctx))) { context.accept(); return; } context.restoreSnapshot(snapshot1); context.set(Parameters.USED_ITEM_STACK, offHandSlot.peek().orElse(null)); context.set(Parameters.USED_SLOT, offHandSlot); context.set(Parameters.INTERACTION_HAND, HandTypes.OFF_HAND); if (context.process(blockType.getPipeline().pipeline(InteractWithBlockBehavior.class), (ctx, behavior) -> behavior.tryInteract(blockType.getPipeline(), ctx))) { context.accept(); return; } context.restoreSnapshot(snapshot); } handleItemInteraction(context, snapshot); } public void handleItemInteraction(MessagePlayInPlayerUseItem message) { final long time = System.currentTimeMillis(); if (this.lastInteractionTime != -1 && (time - this.lastInteractionTime) < 40) { return; } this.lastInteractionTime = time; final BehaviorContextImpl context = new BehaviorContextImpl(Cause.source(this.player).build()); context.set(Parameters.PLAYER, this.player); final BehaviorContextImpl.Snapshot snapshot = context.createSnapshot(); handleItemInteraction(context, snapshot); } private void handleItemInteraction(BehaviorContextImpl context, BehaviorContextImpl.Snapshot snapshot) { // Try the action of the hotbar item first final LanternSlot hotbarSlot = this.player.getInventory().getHotbar().getSelectedSlot(); final OffHandSlot offHandSlot = this.player.getInventory().getOffhand(); Optional<ItemStack> handItem = hotbarSlot.peek(); if (handItem.isPresent()) { final LanternItemType itemType = (LanternItemType) handItem.get().getItem(); context.set(Parameters.USED_ITEM_STACK, handItem.get()); context.set(Parameters.USED_SLOT, hotbarSlot); context.set(Parameters.INTERACTION_HAND, HandTypes.MAIN_HAND); context.set(Parameters.ITEM_TYPE, itemType); if (context.process(itemType.getPipeline().pipeline(InteractWithItemBehavior.class), (ctx, behavior) -> behavior.tryInteract(itemType.getPipeline(), ctx))) { context.accept(); return; } context.restoreSnapshot(snapshot); } handItem = offHandSlot.peek(); if (handItem.isPresent()) { final LanternItemType itemType = (LanternItemType) handItem.get().getItem(); context.set(Parameters.USED_ITEM_STACK, handItem.get()); context.set(Parameters.USED_SLOT, offHandSlot); context.set(Parameters.INTERACTION_HAND, HandTypes.OFF_HAND); context.set(Parameters.ITEM_TYPE, itemType); if (context.process(itemType.getPipeline().pipeline(InteractWithItemBehavior.class), (ctx, behavior) -> behavior.tryInteract(itemType.getPipeline(), ctx))) { context.accept(); } } } }