/* * Copyright (c) 2019-2021 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.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.packet.SetEntityMotionPacket; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.translator.level.block.entity.PistonBlockEntity; import org.geysermc.geyser.level.physics.Axis; import java.util.Map; @Getter public class PistonCache { @Getter(AccessLevel.PRIVATE) private final GeyserSession session; /** * Maps the position of a piston to its block entity */ private final Map pistons = new Object2ObjectOpenHashMap<>(); /** * Maps the position of a moving block to the piston moving it * Positions in this map represent the starting position of the block */ private final Map movingBlocksMap = new Object2ObjectOpenHashMap<>(); private Vector3d playerDisplacement = Vector3d.ZERO; @Setter private Vector3f playerMotion = Vector3f.ZERO; /** * Stores whether a player has/will collide with any moving blocks. */ @Setter private boolean playerCollided = false; /** * Stores whether a player has/will collide with any slime blocks. * This is used to prevent movement from being corrected when players * are about to hit a slime block. */ @Setter private boolean playerSlimeCollision = false; /** * Stores whether a player is standing on a honey block. * This is used to ignore movement from Bedrock to prevent them from * falling off. */ @Setter private boolean playerAttachedToHoney = false; public PistonCache(GeyserSession session) { this.session = session; } public void tick() { resetPlayerMovement(); if (!pistons.isEmpty()) { pistons.values().forEach(PistonBlockEntity::updateMovement); sendPlayerMovement(); sendPlayerMotion(); // Update blocks after movement, so that players don't get stuck inside blocks pistons.values().forEach(PistonBlockEntity::updateBlocks); pistons.entrySet().removeIf((entry) -> entry.getValue().canBeRemoved()); if (pistons.isEmpty() && !movingBlocksMap.isEmpty()) { session.getGeyser().getLogger().error("The moving block map has de-synced!"); for (Map.Entry entry : movingBlocksMap.entrySet()) { session.getGeyser().getLogger().error("Moving Block at " + entry.getKey() + " was previously owned by the piston at " + entry.getValue().getPosition()); } } } } private void resetPlayerMovement() { playerDisplacement = Vector3d.ZERO; playerMotion = Vector3f.ZERO; playerCollided = false; playerSlimeCollision = false; playerAttachedToHoney = false; } private void sendPlayerMovement() { if (!playerDisplacement.equals(Vector3d.ZERO) && playerMotion.equals(Vector3f.ZERO)) { SessionPlayerEntity playerEntity = session.getPlayerEntity(); boolean isOnGround = playerDisplacement.getY() > 0 || playerEntity.isOnGround(); Vector3d position = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter(); playerEntity.moveAbsolute(position.toFloat(), playerEntity.getYaw(), playerEntity.getPitch(), playerEntity.getHeadYaw(), isOnGround, true); } } private void sendPlayerMotion() { if (!playerMotion.equals(Vector3f.ZERO)) { SessionPlayerEntity playerEntity = session.getPlayerEntity(); playerEntity.setMotion(playerMotion); SetEntityMotionPacket setEntityMotionPacket = new SetEntityMotionPacket(); setEntityMotionPacket.setRuntimeEntityId(playerEntity.getGeyserId()); setEntityMotionPacket.setMotion(playerMotion); session.sendUpstreamPacket(setEntityMotionPacket); } } /** * Add to the player's displacement and move the player's bounding box * The total displacement is capped to a range of -0.51 to 0.51 per tick * * @param displacement The displacement to apply to the player's bounding box */ public void displacePlayer(Vector3d displacement) { Vector3d totalDisplacement = playerDisplacement.add(displacement); // Clamp to range -0.51 to 0.51 totalDisplacement = totalDisplacement.max(-0.51d, -0.51d, -0.51d).min(0.51d, 0.51d, 0.51d); Vector3d delta = totalDisplacement.sub(playerDisplacement); // Check if the piston is pushing a player into collision delta = session.getCollisionManager().correctPlayerMovement(delta, true, false); session.getCollisionManager().getPlayerBoundingBox().translate(delta.getX(), delta.getY(), delta.getZ()); playerDisplacement = totalDisplacement; } /** * @param blockPos The block position to test * @param boundingBox The bounding box that moves * @param axis The axis to apply the offset * @param offset The current maximum distance the bounding box can travel * @return The new maximum distance the bounding box can travel without colliding with the tested moving block */ public double computeCollisionOffset(Vector3i blockPos, BoundingBox boundingBox, Axis axis, double offset) { PistonBlockEntity piston = movingBlocksMap.get(blockPos); if (piston != null) { return piston.computeCollisionOffset(blockPos, boundingBox, axis, offset); } return offset; } public boolean checkCollision(Vector3i blockPos, BoundingBox boundingBox) { PistonBlockEntity piston = movingBlocksMap.get(blockPos); if (piston != null) { return piston.checkCollision(blockPos, boundingBox); } return false; } public void clear() { pistons.clear(); movingBlocksMap.clear(); } }