Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2024-12-24 15:20:25 +01:00
Smooth Pistons (#1542)
With proper piston collision for players as well.
Dieser Commit ist enthalten in:
Ursprung
1199d50338
Commit
8461cf76b7
@ -48,6 +48,7 @@ import org.geysermc.geyser.adapters.spigot.SpigotAdapters;
|
||||
import org.geysermc.platform.spigot.command.GeyserSpigotCommandExecutor;
|
||||
import org.geysermc.platform.spigot.command.GeyserSpigotCommandManager;
|
||||
import org.geysermc.platform.spigot.command.SpigotCommandSender;
|
||||
import org.geysermc.platform.spigot.world.GeyserPistonListener;
|
||||
import org.geysermc.platform.spigot.world.GeyserSpigot1_11CraftingListener;
|
||||
import org.geysermc.platform.spigot.world.GeyserSpigotBlockPlaceListener;
|
||||
import org.geysermc.platform.spigot.world.manager.*;
|
||||
@ -227,6 +228,8 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
|
||||
GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(connector, this.geyserWorldManager);
|
||||
Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this);
|
||||
|
||||
Bukkit.getServer().getPluginManager().registerEvents(new GeyserPistonListener(connector, this.geyserWorldManager), this);
|
||||
|
||||
if (isPre1_12) {
|
||||
// Register events needed to send all recipes to the client
|
||||
Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigot1_11CraftingListener(connector), this);
|
||||
|
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.platform.spigot.world;
|
||||
|
||||
import com.github.steveice10.mc.protocol.data.game.world.block.value.PistonValueType;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockPistonEvent;
|
||||
import org.bukkit.event.block.BlockPistonExtendEvent;
|
||||
import org.bukkit.event.block.BlockPistonRetractEvent;
|
||||
import org.geysermc.connector.GeyserConnector;
|
||||
import org.geysermc.connector.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.session.cache.PistonCache;
|
||||
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||
import org.geysermc.connector.network.translators.world.block.entity.PistonBlockEntity;
|
||||
import org.geysermc.connector.utils.Direction;
|
||||
import org.geysermc.platform.spigot.world.manager.GeyserSpigotWorldManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GeyserPistonListener implements Listener {
|
||||
private final GeyserConnector connector;
|
||||
private final GeyserSpigotWorldManager worldManager;
|
||||
|
||||
public GeyserPistonListener(GeyserConnector connector, GeyserSpigotWorldManager worldManager) {
|
||||
this.connector = connector;
|
||||
this.worldManager = worldManager;
|
||||
}
|
||||
|
||||
// The handlers' parent class cannot be registered
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPistonExtend(BlockPistonExtendEvent event) {
|
||||
onPistonAction(event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPistonRetract(BlockPistonRetractEvent event) {
|
||||
onPistonAction(event);
|
||||
}
|
||||
|
||||
private void onPistonAction(BlockPistonEvent event) {
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
World world = event.getBlock().getWorld();
|
||||
boolean isExtend = event instanceof BlockPistonExtendEvent;
|
||||
|
||||
Location location = event.getBlock().getLocation();
|
||||
Vector3i position = getVector(location);
|
||||
PistonValueType type = isExtend ? PistonValueType.PUSHING : PistonValueType.PULLING;
|
||||
boolean sticky = event.isSticky();
|
||||
|
||||
Object2IntMap<Vector3i> attachedBlocks = new Object2IntOpenHashMap<>();
|
||||
boolean blocksFilled = false;
|
||||
|
||||
for (GeyserSession session : connector.getPlayers()) {
|
||||
Player player = Bukkit.getPlayer(session.getPlayerEntity().getUuid());
|
||||
if (player == null || !player.getWorld().equals(world)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int dX = Math.abs(location.getBlockX() - player.getLocation().getBlockX()) >> 4;
|
||||
int dZ = Math.abs(location.getBlockZ() - player.getLocation().getBlockZ()) >> 4;
|
||||
if ((dX * dX + dZ * dZ) > session.getRenderDistance() * session.getRenderDistance()) {
|
||||
// Ignore pistons outside the player's render distance
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trying to grab the blocks from the world like other platforms would result in the moving piston block
|
||||
// being returned instead.
|
||||
if (!blocksFilled) {
|
||||
// Blocks currently require a player for 1.12, so let's just leech off one player to get all blocks
|
||||
// and call it a day for the rest of the sessions (mostly to save on execution time)
|
||||
List<Block> blocks = isExtend ? ((BlockPistonExtendEvent) event).getBlocks() : ((BlockPistonRetractEvent) event).getBlocks();
|
||||
for (Block block : blocks) {
|
||||
Location attachedLocation = block.getLocation();
|
||||
attachedBlocks.put(getVector(attachedLocation), worldManager.getBlockNetworkId(player, block,
|
||||
attachedLocation.getBlockX(), attachedLocation.getBlockY(), attachedLocation.getBlockZ()));
|
||||
}
|
||||
blocksFilled = true;
|
||||
}
|
||||
|
||||
int pistonBlockId = worldManager.getBlockNetworkId(player, event.getBlock(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
// event.getDirection() is unreliable
|
||||
Direction orientation = BlockStateValues.getPistonOrientation(pistonBlockId);
|
||||
|
||||
session.executeInEventLoop(() -> {
|
||||
PistonCache pistonCache = session.getPistonCache();
|
||||
PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos ->
|
||||
new PistonBlockEntity(session, position, orientation, sticky, !isExtend));
|
||||
blockEntity.setAction(type, attachedBlocks);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3i getVector(Location location) {
|
||||
return Vector3i.from(location.getX(), location.getY(), location.getZ());
|
||||
}
|
||||
}
|
@ -66,7 +66,6 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public int getBlockAt(GeyserSession session, int x, int y, int z) {
|
||||
Player player = Bukkit.getPlayer(session.getPlayerEntity().getUsername());
|
||||
if (player == null) {
|
||||
@ -76,12 +75,19 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager {
|
||||
// Prevent nasty async errors if a player is loading in
|
||||
return BlockStateValues.JAVA_AIR_ID;
|
||||
}
|
||||
|
||||
Block block = player.getWorld().getBlockAt(x, y, z);
|
||||
return getBlockNetworkId(player, block, x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public int getBlockNetworkId(Player player, Block block, int x, int y, int z) {
|
||||
// Get block entity storage
|
||||
BlockStorage storage = Via.getManager().getConnectionManager().getConnectedClient(player.getUniqueId()).get(BlockStorage.class);
|
||||
Block block = player.getWorld().getBlockAt(x, y, z);
|
||||
// Black magic that gets the old block state ID
|
||||
int blockId = (block.getType().getId() << 4) | (block.getData() & 0xF);
|
||||
return getLegacyBlock(storage, blockId, x, y, z);
|
||||
int oldBlockId = (block.getType().getId() << 4) | (block.getData() & 0xF);
|
||||
return getLegacyBlock(storage, oldBlockId, x, y, z);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,7 +77,11 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
|
||||
return BlockStateValues.JAVA_AIR_ID;
|
||||
}
|
||||
|
||||
return BlockRegistries.JAVA_IDENTIFIERS.getOrDefault(world.getBlockAt(x, y, z).getBlockData().getAsString(), BlockStateValues.JAVA_AIR_ID);
|
||||
return getBlockNetworkId(bukkitPlayer, world.getBlockAt(x, y, z), x, y, z);
|
||||
}
|
||||
|
||||
public int getBlockNetworkId(Player player, Block block, int x, int y, int z) {
|
||||
return BlockRegistries.JAVA_IDENTIFIERS.getOrDefault(block.getBlockData().getAsString(), BlockStateValues.JAVA_AIR_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -103,7 +103,7 @@ public class FishingHookEntity extends ThrowableEntity {
|
||||
// TODO Push bounding box out of collision to improve movement
|
||||
collided = true;
|
||||
}
|
||||
blockCollision.setPosition(null);
|
||||
blockCollision.reset();
|
||||
}
|
||||
|
||||
int waterLevel = BlockStateValues.getWaterLevel(blockID);
|
||||
|
@ -147,6 +147,7 @@ public class GeyserSession implements CommandSender {
|
||||
private EntityEffectCache effectCache;
|
||||
private final FormCache formCache;
|
||||
private final LodestoneCache lodestoneCache;
|
||||
private final PistonCache pistonCache;
|
||||
private final PreferencesCache preferencesCache;
|
||||
private final TagCache tagCache;
|
||||
private WorldCache worldCache;
|
||||
@ -446,6 +447,7 @@ public class GeyserSession implements CommandSender {
|
||||
this.effectCache = new EntityEffectCache();
|
||||
this.formCache = new FormCache(this);
|
||||
this.lodestoneCache = new LodestoneCache();
|
||||
this.pistonCache = new PistonCache(this);
|
||||
this.preferencesCache = new PreferencesCache(this);
|
||||
this.tagCache = new TagCache();
|
||||
this.worldCache = new WorldCache(this);
|
||||
@ -912,6 +914,7 @@ public class GeyserSession implements CommandSender {
|
||||
*/
|
||||
protected void tick() {
|
||||
try {
|
||||
pistonCache.tick();
|
||||
// Check to see if the player's position needs updating - a position update should be sent once every 3 seconds
|
||||
if (spawned && (System.currentTimeMillis() - lastMovementTimestamp) > 3000) {
|
||||
// Recalculate in case something else changed position
|
||||
|
187
connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java
vendored
Normale Datei
187
connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java
vendored
Normale Datei
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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.connector.network.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.connector.entity.player.SessionPlayerEntity;
|
||||
import org.geysermc.connector.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.translators.collision.BoundingBox;
|
||||
import org.geysermc.connector.network.translators.world.block.entity.PistonBlockEntity;
|
||||
import org.geysermc.connector.utils.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<Vector3i, PistonBlockEntity> 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<Vector3i, PistonBlockEntity> 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.getConnector().getLogger().error("The moving block map has de-synced!");
|
||||
for (Map.Entry<Vector3i, PistonBlockEntity> entry : movingBlocksMap.entrySet()) {
|
||||
session.getConnector().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(session, position.toFloat(), playerEntity.getRotation(), 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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -92,9 +92,9 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
|
||||
|
||||
session.sendDownstreamPacket(playerRotationPacket);
|
||||
} else {
|
||||
if (isValidMove(session, packet.getMode(), entity.getPosition(), packet.getPosition())) {
|
||||
Vector3d position = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.isOnGround());
|
||||
if (position != null) { // A null return value cancels the packet
|
||||
if (isValidMove(session, packet.getMode(), entity.getPosition(), packet.getPosition())) {
|
||||
Packet movePacket;
|
||||
if (rotationChanged) {
|
||||
// Send rotation updates as well
|
||||
@ -134,13 +134,13 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
|
||||
session.sendUpstreamPacket(movePlayerPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not a valid move
|
||||
session.getConnector().getLogger().debug("Recalculating position...");
|
||||
session.getCollisionManager().recalculatePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move parrots to match if applicable
|
||||
if (entity.getLeftParrot() != null) {
|
||||
|
@ -25,11 +25,14 @@
|
||||
|
||||
package org.geysermc.connector.network.translators.collision;
|
||||
|
||||
import com.nukkitx.math.vector.Vector3d;
|
||||
import lombok.*;
|
||||
import org.geysermc.connector.utils.Axis;
|
||||
import org.geysermc.connector.utils.Direction;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class BoundingBox {
|
||||
public class BoundingBox implements Cloneable {
|
||||
private double middleX;
|
||||
private double middleY;
|
||||
private double middleZ;
|
||||
@ -44,9 +47,128 @@ public class BoundingBox {
|
||||
middleZ += z;
|
||||
}
|
||||
|
||||
public boolean checkIntersection(int offsetX, int offsetY, int offsetZ, BoundingBox otherBox) {
|
||||
public void extend(double x, double y, double z) {
|
||||
middleX += x / 2;
|
||||
middleY += y / 2;
|
||||
middleZ += z / 2;
|
||||
|
||||
sizeX += Math.abs(x);
|
||||
sizeY += Math.abs(y);
|
||||
sizeZ += Math.abs(z);
|
||||
}
|
||||
|
||||
public void extend(Vector3d extend) {
|
||||
extend(extend.getX(), extend.getY(), extend.getZ());
|
||||
}
|
||||
|
||||
public boolean checkIntersection(double offsetX, double offsetY, double offsetZ, BoundingBox otherBox) {
|
||||
return (Math.abs((middleX + offsetX) - otherBox.getMiddleX()) * 2 < (sizeX + otherBox.getSizeX())) &&
|
||||
(Math.abs((middleY + offsetY) - otherBox.getMiddleY()) * 2 < (sizeY + otherBox.getSizeY())) &&
|
||||
(Math.abs((middleZ + offsetZ) - otherBox.getMiddleZ()) * 2 < (sizeZ + otherBox.getSizeZ()));
|
||||
}
|
||||
|
||||
public boolean checkIntersection(Vector3d offset, BoundingBox otherBox) {
|
||||
return checkIntersection(offset.getX(), offset.getY(), offset.getZ(), otherBox);
|
||||
}
|
||||
|
||||
public Vector3d getMin() {
|
||||
double x = middleX - sizeX / 2;
|
||||
double y = middleY - sizeY / 2;
|
||||
double z = middleZ - sizeZ / 2;
|
||||
return Vector3d.from(x, y, z);
|
||||
}
|
||||
|
||||
public Vector3d getMax() {
|
||||
double x = middleX + sizeX / 2;
|
||||
double y = middleY + sizeY / 2;
|
||||
double z = middleZ + sizeZ / 2;
|
||||
return Vector3d.from(x, y, z);
|
||||
}
|
||||
|
||||
public Vector3d getBottomCenter() {
|
||||
return Vector3d.from(middleX, middleY - sizeY / 2, middleZ);
|
||||
}
|
||||
|
||||
private boolean checkOverlapInAxis(Vector3d offset, BoundingBox otherBox, Axis axis) {
|
||||
switch (axis) {
|
||||
case X:
|
||||
return Math.abs((middleX + offset.getX()) - otherBox.getMiddleX()) * 2 < (sizeX + otherBox.getSizeX());
|
||||
case Y:
|
||||
return Math.abs((middleY + offset.getY()) - otherBox.getMiddleY()) * 2 < (sizeY + otherBox.getSizeY());
|
||||
case Z:
|
||||
return Math.abs((middleZ + offset.getZ()) - otherBox.getMiddleZ()) * 2 < (sizeZ + otherBox.getSizeZ());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the maximum offset of another bounding box in an axis that will not collide with this bounding box
|
||||
*
|
||||
* @param boxOffset The offset of this bounding box
|
||||
* @param otherBoundingBox The bounding box that is moving
|
||||
* @param axis The axis of movement
|
||||
* @param offset The current max offset
|
||||
* @return The new max offset
|
||||
*/
|
||||
public double getMaxOffset(Vector3d boxOffset, BoundingBox otherBoundingBox, Axis axis, double offset) {
|
||||
// Make sure that the bounding box overlaps in the other axes
|
||||
for (Axis a : Axis.VALUES) {
|
||||
if (a != axis && !checkOverlapInAxis(boxOffset, otherBoundingBox, a)) {
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
if (offset > 0) {
|
||||
double min = axis.choose(getMin().add(boxOffset));
|
||||
double max = axis.choose(otherBoundingBox.getMax());
|
||||
if ((min - max) >= -2.0 * CollisionManager.COLLISION_TOLERANCE) {
|
||||
offset = Math.min(min - max, offset);
|
||||
}
|
||||
} else if (offset < 0) {
|
||||
double min = axis.choose(otherBoundingBox.getMin());
|
||||
double max = axis.choose(getMax().add(boxOffset));
|
||||
if ((min - max) >= -2.0 * CollisionManager.COLLISION_TOLERANCE) {
|
||||
offset = Math.max(max - min, offset);
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the distance required to move this bounding box to one of otherBoundingBox's sides
|
||||
*
|
||||
* @param otherBoundingBox The stationary bounding box
|
||||
* @param side The side of otherBoundingBox to snap this bounding box to
|
||||
* @return The distance to move in the direction of {@code side}
|
||||
*/
|
||||
public double getIntersectionSize(BoundingBox otherBoundingBox, Direction side) {
|
||||
switch (side) {
|
||||
case DOWN:
|
||||
return getMax().getY() - otherBoundingBox.getMin().getY();
|
||||
case UP:
|
||||
return otherBoundingBox.getMax().getY() - getMin().getY();
|
||||
case NORTH:
|
||||
return getMax().getZ() - otherBoundingBox.getMin().getZ();
|
||||
case SOUTH:
|
||||
return otherBoundingBox.getMax().getZ() - getMin().getZ();
|
||||
case WEST:
|
||||
return getMax().getX() - otherBoundingBox.getMin().getX();
|
||||
case EAST:
|
||||
return otherBoundingBox.getMax().getX() - getMin().getX();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@SneakyThrows(CloneNotSupportedException.class)
|
||||
@Override
|
||||
public BoundingBox clone() {
|
||||
BoundingBox clone = (BoundingBox) super.clone();
|
||||
clone.middleX = middleX;
|
||||
clone.middleY = middleY;
|
||||
clone.middleZ = middleZ;
|
||||
|
||||
clone.sizeX = sizeX;
|
||||
clone.sizeY = sizeY;
|
||||
clone.sizeZ = sizeZ;
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
@ -31,17 +31,19 @@ import com.nukkitx.math.vector.Vector3i;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlags;
|
||||
import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
|
||||
import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
|
||||
import com.nukkitx.protocol.bedrock.packet.*;
|
||||
import com.nukkitx.protocol.bedrock.v448.Bedrock_v448;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.geysermc.connector.entity.player.PlayerEntity;
|
||||
import org.geysermc.connector.entity.type.EntityType;
|
||||
import org.geysermc.connector.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.session.cache.PistonCache;
|
||||
import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
|
||||
import org.geysermc.connector.network.translators.collision.translators.ScaffoldingCollision;
|
||||
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||
import org.geysermc.connector.utils.BlockUtils;
|
||||
import org.geysermc.connector.utils.Axis;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
@ -79,6 +81,14 @@ public class CollisionManager {
|
||||
*/
|
||||
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#####", new DecimalFormatSymbols(Locale.ENGLISH));
|
||||
|
||||
private static final double PLAYER_STEP_UP = 0.6;
|
||||
|
||||
/**
|
||||
* The maximum squared distance between a Bedrock players' movement and our predicted movement before
|
||||
* the player is teleported to the correct position
|
||||
*/
|
||||
private static final double INCORRECT_MOVEMENT_THRESHOLD = 0.08;
|
||||
|
||||
public CollisionManager(GeyserSession session) {
|
||||
this.session = session;
|
||||
this.playerBoundingBox = new BoundingBox(0, 0, 0, 0.6, 1.8, 0.6);
|
||||
@ -119,13 +129,18 @@ public class CollisionManager {
|
||||
|
||||
/**
|
||||
* Adjust the Bedrock position before sending to the Java server to account for inaccuracies in movement between
|
||||
* the two versions.
|
||||
* the two versions. Will also send corrected movement packets back to Bedrock if they collide with pistons.
|
||||
*
|
||||
* @param bedrockPosition the current Bedrock position of the client
|
||||
* @param onGround whether the Bedrock player is on the ground
|
||||
* @return the position to send to the Java server, or null to cancel sending the packet
|
||||
*/
|
||||
public Vector3d adjustBedrockPosition(Vector3f bedrockPosition, boolean onGround) {
|
||||
PistonCache pistonCache = session.getPistonCache();
|
||||
// Bedrock clients tend to fall off of honey blocks, so we need to teleport them to the new position
|
||||
if (pistonCache.isPlayerAttachedToHoney()) {
|
||||
return null;
|
||||
}
|
||||
// We need to parse the float as a string since casting a float to a double causes us to
|
||||
// lose precision and thus, causes players to get stuck when walking near walls
|
||||
double javaY = bedrockPosition.getY() - EntityType.PLAYER.getOffset();
|
||||
@ -133,18 +148,33 @@ public class CollisionManager {
|
||||
Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY,
|
||||
Double.parseDouble(Float.toString(bedrockPosition.getZ())));
|
||||
|
||||
updatePlayerBoundingBox(position);
|
||||
|
||||
Vector3d startingPos = playerBoundingBox.getBottomCenter();
|
||||
Vector3d movement = position.sub(startingPos);
|
||||
Vector3d adjustedMovement = correctPlayerMovement(movement, false);
|
||||
playerBoundingBox.translate(adjustedMovement.getX(), adjustedMovement.getY(), adjustedMovement.getZ());
|
||||
playerBoundingBox.translate(pistonCache.getPlayerMotion().getX(), pistonCache.getPlayerMotion().getY(), pistonCache.getPlayerMotion().getZ());
|
||||
// Correct player position
|
||||
if (!correctPlayerPosition()) {
|
||||
// Cancel the movement if it needs to be cancelled
|
||||
recalculatePosition();
|
||||
return null;
|
||||
}
|
||||
// The server can't complain about our movement if we never send it
|
||||
// TODO get rid of this and handle teleports smoothly
|
||||
if (pistonCache.isPlayerCollided()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
position = Vector3d.from(playerBoundingBox.getMiddleX(),
|
||||
playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2),
|
||||
playerBoundingBox.getMiddleZ());
|
||||
position = playerBoundingBox.getBottomCenter();
|
||||
|
||||
boolean newOnGround = adjustedMovement.getY() != movement.getY() && movement.getY() < 0 || onGround;
|
||||
// Send corrected position to Bedrock if they differ by too much to prevent de-syncs
|
||||
if (onGround != newOnGround || movement.distanceSquared(adjustedMovement) > INCORRECT_MOVEMENT_THRESHOLD) {
|
||||
PlayerEntity playerEntity = session.getPlayerEntity();
|
||||
if (pistonCache.getPlayerMotion().equals(Vector3f.ZERO) && !pistonCache.isPlayerSlimeCollision()) {
|
||||
playerEntity.moveAbsolute(session, position.toFloat(), playerEntity.getRotation(), newOnGround, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!onGround) {
|
||||
// Trim the position to prevent rounding errors that make Java think we are clipping into a block
|
||||
@ -178,17 +208,20 @@ public class CollisionManager {
|
||||
box.getMiddleY() - (box.getSizeY() / 2),
|
||||
box.getMiddleZ());
|
||||
|
||||
// Expand volume by 1 in each direction to include moving blocks
|
||||
double pistonExpand = session.getPistonCache().getPistons().isEmpty() ? 0 : 1;
|
||||
|
||||
// Loop through all blocks that could collide
|
||||
int minCollisionX = (int) Math.floor(position.getX() - ((box.getSizeX() / 2) + COLLISION_TOLERANCE));
|
||||
int maxCollisionX = (int) Math.floor(position.getX() + (box.getSizeX() / 2) + COLLISION_TOLERANCE);
|
||||
int minCollisionX = (int) Math.floor(position.getX() - ((box.getSizeX() / 2) + COLLISION_TOLERANCE + pistonExpand));
|
||||
int maxCollisionX = (int) Math.floor(position.getX() + (box.getSizeX() / 2) + COLLISION_TOLERANCE + pistonExpand);
|
||||
|
||||
// Y extends 0.5 blocks down because of fence hitboxes
|
||||
int minCollisionY = (int) Math.floor(position.getY() - 0.5);
|
||||
int minCollisionY = (int) Math.floor(position.getY() - 0.5 - COLLISION_TOLERANCE - pistonExpand / 2.0);
|
||||
|
||||
int maxCollisionY = (int) Math.floor(position.getY() + box.getSizeY());
|
||||
int maxCollisionY = (int) Math.floor(position.getY() + box.getSizeY() + pistonExpand);
|
||||
|
||||
int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE));
|
||||
int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE);
|
||||
int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE + pistonExpand));
|
||||
int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE + pistonExpand);
|
||||
|
||||
for (int y = minCollisionY; y < maxCollisionY + 1; y++) {
|
||||
for (int x = minCollisionX; x < maxCollisionX + 1; x++) {
|
||||
@ -223,7 +256,7 @@ public class CollisionManager {
|
||||
BlockCollision blockCollision = BlockUtils.getCollisionAt(session, blockPos);
|
||||
if (blockCollision != null) {
|
||||
blockCollision.beforeCorrectPosition(playerBoundingBox);
|
||||
blockCollision.setPosition(null);
|
||||
blockCollision.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,7 +267,7 @@ public class CollisionManager {
|
||||
if (!blockCollision.correctPosition(session, playerBoundingBox)) {
|
||||
return false;
|
||||
}
|
||||
blockCollision.setPosition(null);
|
||||
blockCollision.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,6 +276,106 @@ public class CollisionManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
public Vector3d correctPlayerMovement(Vector3d movement, boolean checkWorld) {
|
||||
if (!checkWorld && session.getPistonCache().getPistons().isEmpty()) { // There is nothing to check
|
||||
return movement;
|
||||
}
|
||||
return correctMovement(movement, playerBoundingBox, session.getPlayerEntity().isOnGround(), PLAYER_STEP_UP, checkWorld);
|
||||
}
|
||||
|
||||
public Vector3d correctMovement(Vector3d movement, BoundingBox boundingBox, boolean onGround, double stepUp, boolean checkWorld) {
|
||||
Vector3d adjustedMovement = movement;
|
||||
if (!movement.equals(Vector3d.ZERO)) {
|
||||
adjustedMovement = correctMovementForCollisions(movement, boundingBox, checkWorld);
|
||||
}
|
||||
|
||||
boolean verticalCollision = adjustedMovement.getY() != movement.getY();
|
||||
boolean horizontalCollision = adjustedMovement.getX() != movement.getX() || adjustedMovement.getZ() != movement.getZ();
|
||||
boolean falling = movement.getY() < 0;
|
||||
onGround = onGround || (verticalCollision && falling);
|
||||
if (onGround && horizontalCollision) {
|
||||
Vector3d horizontalMovement = Vector3d.from(movement.getX(), 0, movement.getZ());
|
||||
Vector3d stepUpMovement = correctMovementForCollisions(horizontalMovement.up(stepUp), boundingBox, checkWorld);
|
||||
|
||||
BoundingBox stretchedBoundingBox = boundingBox.clone();
|
||||
stretchedBoundingBox.extend(horizontalMovement);
|
||||
double maxStepUp = correctMovementForCollisions(Vector3d.from(0, stepUp, 0), stretchedBoundingBox, checkWorld).getY();
|
||||
if (maxStepUp < stepUp) { // The player collided with a block above them
|
||||
boundingBox.translate(0, maxStepUp, 0);
|
||||
Vector3d adjustedStepUpMovement = correctMovementForCollisions(horizontalMovement, boundingBox, checkWorld);
|
||||
boundingBox.translate(0, -maxStepUp, 0);
|
||||
|
||||
if (squaredHorizontalLength(adjustedStepUpMovement) > squaredHorizontalLength(stepUpMovement)) {
|
||||
stepUpMovement = adjustedStepUpMovement.up(maxStepUp);
|
||||
}
|
||||
}
|
||||
|
||||
if (squaredHorizontalLength(stepUpMovement) > squaredHorizontalLength(adjustedMovement)) {
|
||||
boundingBox.translate(stepUpMovement.getX(), stepUpMovement.getY(), stepUpMovement.getZ());
|
||||
// Apply the player's remaining vertical movement
|
||||
double verticalMovement = correctMovementForCollisions(Vector3d.from(0, movement.getY() - stepUpMovement.getY(), 0), boundingBox, checkWorld).getY();
|
||||
boundingBox.translate(-stepUpMovement.getX(), -stepUpMovement.getY(), -stepUpMovement.getZ());
|
||||
|
||||
stepUpMovement = stepUpMovement.up(verticalMovement);
|
||||
adjustedMovement = stepUpMovement;
|
||||
}
|
||||
}
|
||||
return adjustedMovement;
|
||||
}
|
||||
|
||||
private double squaredHorizontalLength(Vector3d vector) {
|
||||
return vector.getX() * vector.getX() + vector.getZ() * vector.getZ();
|
||||
}
|
||||
|
||||
private Vector3d correctMovementForCollisions(Vector3d movement, BoundingBox boundingBox, boolean checkWorld) {
|
||||
double movementX = movement.getX();
|
||||
double movementY = movement.getY();
|
||||
double movementZ = movement.getZ();
|
||||
|
||||
BoundingBox movementBoundingBox = boundingBox.clone();
|
||||
movementBoundingBox.extend(movement);
|
||||
|
||||
List<Vector3i> collidableBlocks = getCollidableBlocks(movementBoundingBox);
|
||||
|
||||
if (Math.abs(movementY) > CollisionManager.COLLISION_TOLERANCE) {
|
||||
movementY = computeCollisionOffset(boundingBox, Axis.Y, movementY, collidableBlocks, checkWorld);
|
||||
boundingBox.translate(0, movementY, 0);
|
||||
}
|
||||
boolean checkZFirst = Math.abs(movementZ) > Math.abs(movementX);
|
||||
if (checkZFirst && Math.abs(movementZ) > CollisionManager.COLLISION_TOLERANCE) {
|
||||
movementZ = computeCollisionOffset(boundingBox, Axis.Z, movementZ, collidableBlocks, checkWorld);
|
||||
boundingBox.translate(0, 0, movementZ);
|
||||
}
|
||||
if (Math.abs(movementX) > CollisionManager.COLLISION_TOLERANCE) {
|
||||
movementX = computeCollisionOffset(boundingBox, Axis.X, movementX, collidableBlocks, checkWorld);
|
||||
boundingBox.translate(movementX, 0, 0);
|
||||
}
|
||||
if (!checkZFirst && Math.abs(movementZ) > CollisionManager.COLLISION_TOLERANCE) {
|
||||
movementZ = computeCollisionOffset(boundingBox, Axis.Z, movementZ, collidableBlocks, checkWorld);
|
||||
boundingBox.translate(0, 0, movementZ);
|
||||
}
|
||||
|
||||
boundingBox.translate(-movementX, -movementY, -movementZ);
|
||||
return Vector3d.from(movementX, movementY, movementZ);
|
||||
}
|
||||
|
||||
private double computeCollisionOffset(BoundingBox boundingBox, Axis axis, double offset, List<Vector3i> collidableBlocks, boolean checkWorld) {
|
||||
for (Vector3i blockPos : collidableBlocks) {
|
||||
if (checkWorld) {
|
||||
BlockCollision blockCollision = BlockUtils.getCollisionAt(session, blockPos);
|
||||
if (blockCollision != null && !(blockCollision instanceof ScaffoldingCollision)) {
|
||||
offset = blockCollision.computeCollisionOffset(boundingBox, axis, offset);
|
||||
blockCollision.reset();
|
||||
}
|
||||
}
|
||||
offset = session.getPistonCache().computeCollisionOffset(blockPos, boundingBox, axis, offset);
|
||||
if (Math.abs(offset) < COLLISION_TOLERANCE) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the block located at the player's floor position plus 1 would intersect with the player,
|
||||
* were they not sneaking
|
||||
@ -260,7 +393,8 @@ public class CollisionManager {
|
||||
playerBoundingBox.setSizeY(EntityType.PLAYER.getHeight());
|
||||
playerBoundingBox.setMiddleY(standingY);
|
||||
boolean result = collision.checkIntersection(playerBoundingBox);
|
||||
collision.setPosition(null);
|
||||
result |= session.getPistonCache().checkCollision(position, playerBoundingBox);
|
||||
collision.reset();
|
||||
playerBoundingBox.setSizeY(originalHeight);
|
||||
playerBoundingBox.setMiddleY(originalY);
|
||||
return result;
|
||||
|
@ -30,8 +30,9 @@ import com.nukkitx.math.vector.Vector3i;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.geysermc.connector.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.translators.collision.CollisionManager;
|
||||
import org.geysermc.connector.network.translators.collision.BoundingBox;
|
||||
import org.geysermc.connector.network.translators.collision.CollisionManager;
|
||||
import org.geysermc.connector.utils.Axis;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class BlockCollision {
|
||||
@ -42,6 +43,13 @@ public class BlockCollision {
|
||||
@EqualsAndHashCode.Exclude
|
||||
protected final ThreadLocal<Vector3i> position;
|
||||
|
||||
/**
|
||||
* Store a Vector3d to allow the collision to be offset by a fractional amount
|
||||
* This is used only in {@link #checkIntersection(BoundingBox)} and {@link #computeCollisionOffset(BoundingBox, Axis, double)}
|
||||
*/
|
||||
@EqualsAndHashCode.Exclude
|
||||
protected final ThreadLocal<Vector3d> positionOffset;
|
||||
|
||||
/**
|
||||
* This is used for the step up logic.
|
||||
* Usually, the player can only step up a block if they are on the same Y level as its bottom face or higher
|
||||
@ -61,12 +69,22 @@ public class BlockCollision {
|
||||
protected BlockCollision(BoundingBox[] boxes) {
|
||||
this.boundingBoxes = boxes;
|
||||
this.position = new ThreadLocal<>();
|
||||
this.positionOffset = new ThreadLocal<>();
|
||||
}
|
||||
|
||||
public void setPosition(Vector3i newPosition) {
|
||||
this.position.set(newPosition);
|
||||
}
|
||||
|
||||
public void setPositionOffset(Vector3d newOffset) {
|
||||
this.positionOffset.set(newOffset);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.position.set(null);
|
||||
this.positionOffset.set(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden in classes like SnowCollision and GrassPathCollision when correction code needs to be run before the
|
||||
* main correction
|
||||
@ -156,17 +174,33 @@ public class BlockCollision {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean checkIntersection(BoundingBox playerCollision) {
|
||||
private Vector3d getFullPos() {
|
||||
Vector3i blockPos = this.position.get();
|
||||
int x = blockPos.getX();
|
||||
int y = blockPos.getY();
|
||||
int z = blockPos.getZ();
|
||||
Vector3d blockOffset = this.positionOffset.get();
|
||||
if (blockOffset != null && blockOffset != Vector3d.ZERO) {
|
||||
return blockOffset.add(blockPos.getX(), blockPos.getY(), blockPos.getZ());
|
||||
}
|
||||
return blockPos.toDouble();
|
||||
}
|
||||
|
||||
public boolean checkIntersection(BoundingBox playerCollision) {
|
||||
Vector3d blockPos = getFullPos();
|
||||
for (BoundingBox b : boundingBoxes) {
|
||||
if (b.checkIntersection(x, y, z, playerCollision)) {
|
||||
if (b.checkIntersection(blockPos, playerCollision)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public double computeCollisionOffset(BoundingBox boundingBox, Axis axis, double offset) {
|
||||
Vector3d blockPos = getFullPos();
|
||||
for (BoundingBox b : boundingBoxes) {
|
||||
offset = b.getMaxOffset(blockPos, boundingBox, axis, offset);
|
||||
if (Math.abs(offset) < CollisionManager.COLLISION_TOLERANCE) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
}
|
@ -33,13 +33,16 @@ import com.nukkitx.nbt.NbtMap;
|
||||
import com.nukkitx.nbt.NbtMapBuilder;
|
||||
import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
|
||||
import com.nukkitx.protocol.bedrock.packet.BlockEventPacket;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
|
||||
import org.geysermc.common.PlatformType;
|
||||
import org.geysermc.connector.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.session.cache.PistonCache;
|
||||
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||
import org.geysermc.connector.network.translators.Translator;
|
||||
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||
import org.geysermc.connector.network.translators.world.block.entity.NoteblockBlockEntityTranslator;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.geysermc.connector.network.translators.world.block.entity.PistonBlockEntity;
|
||||
import org.geysermc.connector.utils.Direction;
|
||||
|
||||
@Translator(packet = ServerBlockValuePacket.class)
|
||||
public class JavaBlockValueTranslator extends PacketTranslator<ServerBlockValuePacket> {
|
||||
@ -60,15 +63,41 @@ public class JavaBlockValueTranslator extends PacketTranslator<ServerBlockValueP
|
||||
} else if (packet.getValue() instanceof NoteBlockValue) {
|
||||
NoteblockBlockEntityTranslator.translate(session, packet.getPosition());
|
||||
} else if (packet.getValue() instanceof PistonValue) {
|
||||
PistonValueType type = (PistonValueType) packet.getType();
|
||||
|
||||
// Unlike everything else, pistons need a block entity packet to convey motion
|
||||
// TODO: Doesn't register on chunk load; needs to be interacted with first
|
||||
PistonValueType action = (PistonValueType) packet.getType();
|
||||
Direction direction = Direction.fromPistonValue((PistonValue) packet.getValue());
|
||||
Vector3i position = Vector3i.from(packet.getPosition().getX(), packet.getPosition().getY(), packet.getPosition().getZ());
|
||||
if (type == PistonValueType.PUSHING) {
|
||||
extendPiston(session, position, 0.0f, 0.0f);
|
||||
PistonCache pistonCache = session.getPistonCache();
|
||||
|
||||
if (session.getConnector().getPlatformType() == PlatformType.SPIGOT) {
|
||||
// Mostly handled in the GeyserPistonEvents class
|
||||
// Retracting sticky pistons is an exception, since the event is not called on Spigot from 1.13.2 - 1.17.1
|
||||
// See https://github.com/PaperMC/Paper/blob/6fa1983e9ce177a4a412d5b950fd978620174777/patches/server/0304-Fire-BlockPistonRetractEvent-for-all-empty-pistons.patch
|
||||
if (action == PistonValueType.PULLING || action == PistonValueType.CANCELLED_MID_PUSH) {
|
||||
int pistonBlock = session.getConnector().getWorldManager().getBlockAt(session, position);
|
||||
if (!BlockStateValues.isStickyPiston(pistonBlock)) {
|
||||
return;
|
||||
}
|
||||
if (action != PistonValueType.CANCELLED_MID_PUSH) {
|
||||
Vector3i blockInFrontPos = position.add(direction.getUnitVector());
|
||||
int blockInFront = session.getConnector().getWorldManager().getBlockAt(session, blockInFrontPos);
|
||||
if (blockInFront != BlockStateValues.JAVA_AIR_ID) {
|
||||
// Piston pulled something
|
||||
return;
|
||||
}
|
||||
}
|
||||
PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos -> new PistonBlockEntity(session, pos, direction, true, true));
|
||||
if (blockEntity.getAction() != action) {
|
||||
blockEntity.setAction(action, Object2IntMaps.emptyMap());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
retractPiston(session, position, 1.0f, 1.0f);
|
||||
PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos -> {
|
||||
int blockId = session.getConnector().getWorldManager().getBlockAt(session, position);
|
||||
boolean sticky = BlockStateValues.isStickyPiston(blockId);
|
||||
boolean extended = action != PistonValueType.PUSHING;
|
||||
return new PistonBlockEntity(session, pos, direction, sticky, extended);
|
||||
});
|
||||
blockEntity.setAction(action);
|
||||
}
|
||||
} else if (packet.getValue() instanceof MobSpawnerValue) {
|
||||
blockEventPacket.setEventType(1);
|
||||
@ -111,65 +140,4 @@ public class JavaBlockValueTranslator extends PacketTranslator<ServerBlockValueP
|
||||
session.sendUpstreamPacket(blockEntityPacket);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulating a piston extending
|
||||
* @param session GeyserSession
|
||||
* @param position Block position
|
||||
* @param progress How far the piston is
|
||||
* @param lastProgress How far the piston last was
|
||||
*/
|
||||
private void extendPiston(GeyserSession session, Vector3i position, float progress, float lastProgress) {
|
||||
BlockEntityDataPacket blockEntityDataPacket = new BlockEntityDataPacket();
|
||||
blockEntityDataPacket.setBlockPosition(position);
|
||||
byte state = (byte) ((progress == 1.0f && lastProgress == 1.0f) ? 2 : 1);
|
||||
blockEntityDataPacket.setData(buildPistonTag(position, progress, lastProgress, state));
|
||||
session.sendUpstreamPacket(blockEntityDataPacket);
|
||||
if (lastProgress != 1.0f) {
|
||||
session.getConnector().getGeneralThreadPool().schedule(() ->
|
||||
extendPiston(session, position, (progress >= 1.0f) ? 1.0f : progress + 0.5f, progress),
|
||||
20, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulate a piston retracting.
|
||||
* @param session GeyserSession
|
||||
* @param position Block position
|
||||
* @param progress Current progress of piston
|
||||
* @param lastProgress Last progress of piston
|
||||
*/
|
||||
private void retractPiston(GeyserSession session, Vector3i position, float progress, float lastProgress) {
|
||||
BlockEntityDataPacket blockEntityDataPacket = new BlockEntityDataPacket();
|
||||
blockEntityDataPacket.setBlockPosition(position);
|
||||
byte state = (byte) ((progress == 0.0f && lastProgress == 0.0f) ? 0 : 3);
|
||||
blockEntityDataPacket.setData(buildPistonTag(position, progress, lastProgress, state));
|
||||
session.sendUpstreamPacket(blockEntityDataPacket);
|
||||
if (lastProgress != 0.0f) {
|
||||
session.getConnector().getGeneralThreadPool().schedule(() ->
|
||||
retractPiston(session, position, (progress <= 0.0f) ? 0.0f : progress - 0.5f, progress),
|
||||
20, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a piston tag
|
||||
* @param position Piston position
|
||||
* @param progress Current progress of piston
|
||||
* @param lastProgress Last progress of piston
|
||||
* @param state
|
||||
* @return Bedrock CompoundTag of piston
|
||||
*/
|
||||
private NbtMap buildPistonTag(Vector3i position, float progress, float lastProgress, byte state) {
|
||||
NbtMapBuilder builder = NbtMap.builder()
|
||||
.putInt("x", position.getX())
|
||||
.putInt("y", position.getY())
|
||||
.putInt("z", position.getZ())
|
||||
.putFloat("Progress", progress)
|
||||
.putFloat("LastProgress", lastProgress)
|
||||
.putString("id", "PistonArm")
|
||||
.putByte("NewState", state)
|
||||
.putByte("State", state);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,12 @@ package org.geysermc.connector.network.translators.world.block;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import org.geysermc.connector.registry.BlockRegistries;
|
||||
import org.geysermc.connector.registry.type.BlockMapping;
|
||||
import org.geysermc.connector.utils.Direction;
|
||||
import org.geysermc.connector.utils.PistonBehavior;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
@ -43,8 +48,12 @@ public class BlockStateValues {
|
||||
private static final Int2ObjectMap<String> FLOWER_POT_VALUES = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2BooleanMap LECTERN_BOOK_STATES = new Int2BooleanOpenHashMap();
|
||||
private static final Int2IntMap NOTEBLOCK_PITCHES = new Int2IntOpenHashMap();
|
||||
private static final Int2BooleanMap IS_STICKY_PISTON = new Int2BooleanOpenHashMap();
|
||||
private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap();
|
||||
private static final Int2BooleanMap IS_STICKY_PISTON = new Int2BooleanOpenHashMap();
|
||||
private static final Object2IntMap<Direction> PISTON_HEADS = new Object2IntOpenHashMap<>();
|
||||
private static final Int2ObjectMap<Direction> PISTON_ORIENTATION = new Int2ObjectOpenHashMap<>();
|
||||
private static final IntSet ALL_PISTON_HEADS = new IntOpenHashSet();
|
||||
private static final IntSet MOVING_PISTONS = new IntOpenHashSet();
|
||||
private static final Int2ByteMap SKULL_VARIANTS = new Int2ByteOpenHashMap();
|
||||
private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap();
|
||||
private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap();
|
||||
@ -57,6 +66,8 @@ public class BlockStateValues {
|
||||
public static int JAVA_COBWEB_ID;
|
||||
public static int JAVA_FURNACE_ID;
|
||||
public static int JAVA_FURNACE_LIT_ID;
|
||||
public static int JAVA_HONEY_BLOCK_ID;
|
||||
public static int JAVA_SLIME_BLOCK_ID;
|
||||
public static int JAVA_SPAWNER_ID;
|
||||
public static int JAVA_WATER_ID;
|
||||
|
||||
@ -115,10 +126,20 @@ public class BlockStateValues {
|
||||
return;
|
||||
}
|
||||
|
||||
if (javaId.contains("piston")) {
|
||||
// True if extended, false if not
|
||||
if (javaId.contains("piston[")) { // minecraft:moving_piston, minecraft:sticky_piston, minecraft:piston
|
||||
if (javaId.startsWith("minecraft:moving_piston")) {
|
||||
MOVING_PISTONS.add(javaBlockState);
|
||||
} else {
|
||||
PISTON_VALUES.put(javaBlockState, javaId.contains("extended=true"));
|
||||
}
|
||||
IS_STICKY_PISTON.put(javaBlockState, javaId.contains("sticky"));
|
||||
PISTON_ORIENTATION.put(javaBlockState, getBlockDirection(javaId));
|
||||
return;
|
||||
} else if (javaId.startsWith("minecraft:piston_head")) {
|
||||
ALL_PISTON_HEADS.add(javaBlockState);
|
||||
if (javaId.contains("short=false")) {
|
||||
PISTON_HEADS.put(getBlockDirection(javaId), javaBlockState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -249,6 +270,81 @@ public class BlockStateValues {
|
||||
return IS_STICKY_PISTON.get(blockState);
|
||||
}
|
||||
|
||||
public static boolean isPistonHead(int state) {
|
||||
return ALL_PISTON_HEADS.contains(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Java Block State for a piston head for a specific direction
|
||||
* This is used in PistonBlockEntity to get the BlockCollision for the piston head.
|
||||
*
|
||||
* @param direction Direction the piston head points in
|
||||
* @return Block state for the piston head
|
||||
*/
|
||||
public static int getPistonHead(Direction direction) {
|
||||
return PISTON_HEADS.getOrDefault(direction, BlockStateValues.JAVA_AIR_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is a minecraft:moving_piston
|
||||
* This is used in ChunkUtils to prevent them from being placed as it causes
|
||||
* pistons to flicker and it is not needed
|
||||
*
|
||||
* @param state Block state of the block
|
||||
* @return True if the block is a moving_piston
|
||||
*/
|
||||
public static boolean isMovingPiston(int state) {
|
||||
return MOVING_PISTONS.contains(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used in GeyserPistonEvents.java and accepts minecraft:piston,
|
||||
* minecraft:sticky_piston, and minecraft:moving_piston.
|
||||
*
|
||||
* @param state The block state of the piston base
|
||||
* @return The direction in which the piston faces
|
||||
*/
|
||||
public static Direction getPistonOrientation(int state) {
|
||||
return PISTON_ORIENTATION.get(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block sticks to other blocks
|
||||
* (Slime and honey blocks)
|
||||
*
|
||||
* @param state The block state
|
||||
* @return True if the block sticks to adjacent blocks
|
||||
*/
|
||||
public static boolean isBlockSticky(int state) {
|
||||
return state == JAVA_SLIME_BLOCK_ID || state == JAVA_HONEY_BLOCK_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two blocks are attached to each other.
|
||||
*
|
||||
* @param stateA The block state of block a
|
||||
* @param stateB The block state of block b
|
||||
* @return True if the blocks are attached to each other
|
||||
*/
|
||||
public static boolean isBlockAttached(int stateA, int stateB) {
|
||||
boolean aSticky = isBlockSticky(stateA);
|
||||
boolean bSticky = isBlockSticky(stateB);
|
||||
if (aSticky && bSticky) {
|
||||
// Only matching sticky blocks are attached together
|
||||
// Honey + Honey & Slime + Slime
|
||||
return stateA == stateB;
|
||||
}
|
||||
return aSticky || bSticky;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param state The block state of the block
|
||||
* @return true if a piston can break the block
|
||||
*/
|
||||
public static boolean canPistonDestroyBlock(int state) {
|
||||
return BlockRegistries.JAVA_BLOCKS.getOrDefault(state, BlockMapping.AIR).getPistonBehavior() == PistonBehavior.DESTROY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skull variations are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock.
|
||||
* This gives a byte variant ID that Bedrock can use.
|
||||
@ -323,4 +419,21 @@ public class BlockStateValues {
|
||||
}
|
||||
return 0.6f;
|
||||
}
|
||||
|
||||
private static Direction getBlockDirection(String javaId) {
|
||||
if (javaId.contains("down")) {
|
||||
return Direction.DOWN;
|
||||
} else if (javaId.contains("up")) {
|
||||
return Direction.UP;
|
||||
} else if (javaId.contains("south")) {
|
||||
return Direction.SOUTH;
|
||||
} else if (javaId.contains("west")) {
|
||||
return Direction.WEST;
|
||||
} else if (javaId.contains("north")) {
|
||||
return Direction.NORTH;
|
||||
} else if (javaId.contains("east")) {
|
||||
return Direction.EAST;
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,832 @@
|
||||
/*
|
||||
* 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.connector.network.translators.world.block.entity;
|
||||
|
||||
import com.github.steveice10.mc.protocol.data.game.world.block.value.PistonValueType;
|
||||
import com.nukkitx.math.vector.Vector3d;
|
||||
import com.nukkitx.math.vector.Vector3f;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import com.nukkitx.nbt.NbtMap;
|
||||
import com.nukkitx.nbt.NbtMapBuilder;
|
||||
import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
import lombok.Getter;
|
||||
import org.geysermc.common.PlatformType;
|
||||
import org.geysermc.connector.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.session.cache.PistonCache;
|
||||
import org.geysermc.connector.network.translators.collision.BoundingBox;
|
||||
import org.geysermc.connector.network.translators.collision.CollisionManager;
|
||||
import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
|
||||
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||
import org.geysermc.connector.registry.BlockRegistries;
|
||||
import org.geysermc.connector.registry.Registries;
|
||||
import org.geysermc.connector.registry.type.BlockMapping;
|
||||
import org.geysermc.connector.utils.*;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
public class PistonBlockEntity {
|
||||
private final GeyserSession session;
|
||||
@Getter
|
||||
private final Vector3i position;
|
||||
private final Direction orientation;
|
||||
private final boolean sticky;
|
||||
|
||||
@Getter
|
||||
private PistonValueType action;
|
||||
|
||||
/**
|
||||
* A map of attached block positions to Java ids.
|
||||
*/
|
||||
private final Object2IntMap<Vector3i> attachedBlocks = new Object2IntOpenHashMap<>();
|
||||
/**
|
||||
* A flattened array of the positions of attached blocks, stored in XYZ order.
|
||||
*/
|
||||
private int[] flattenedAttachedBlocks = new int[0];
|
||||
|
||||
private boolean placedFinalBlocks = true;
|
||||
|
||||
/**
|
||||
* The position of the piston head
|
||||
*/
|
||||
private float progress;
|
||||
private float lastProgress;
|
||||
|
||||
private long timeSinceCompletion = 0;
|
||||
|
||||
private static final BoundingBox SOLID_BOUNDING_BOX = new BoundingBox(0.5, 0.5, 0.5, 1, 1, 1);
|
||||
private static final BoundingBox HONEY_BOUNDING_BOX;
|
||||
|
||||
/**
|
||||
* The number of ticks to wait after a piston finishes its movement before
|
||||
* it can be removed
|
||||
*/
|
||||
private static final int REMOVAL_DELAY = 5;
|
||||
|
||||
static {
|
||||
// Create a ~1 x ~0.5 x ~1 bounding box above the honey block
|
||||
BlockCollision blockCollision = Registries.COLLISIONS.get(BlockStateValues.JAVA_HONEY_BLOCK_ID);
|
||||
if (blockCollision == null) {
|
||||
throw new RuntimeException("Failed to find honey block collision");
|
||||
}
|
||||
BoundingBox blockBoundingBox = blockCollision.getBoundingBoxes()[0];
|
||||
|
||||
double honeyHeight = blockBoundingBox.getMax().getY();
|
||||
double boundingBoxHeight = 1.5 - honeyHeight;
|
||||
HONEY_BOUNDING_BOX = new BoundingBox(0.5, honeyHeight + boundingBoxHeight / 2, 0.5, blockBoundingBox.getSizeX(), boundingBoxHeight, blockBoundingBox.getSizeZ());
|
||||
}
|
||||
|
||||
public PistonBlockEntity(GeyserSession session, Vector3i position, Direction orientation, boolean sticky, boolean extended) {
|
||||
this.session = session;
|
||||
this.position = position;
|
||||
this.orientation = orientation;
|
||||
this.sticky = sticky;
|
||||
|
||||
if (extended) {
|
||||
// Fully extended
|
||||
this.action = PistonValueType.PUSHING;
|
||||
this.progress = 1.0f;
|
||||
} else {
|
||||
// Fully retracted
|
||||
this.action = PistonValueType.PULLING;
|
||||
this.progress = 0.0f;
|
||||
}
|
||||
this.lastProgress = this.progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the piston is pulling or pushing blocks
|
||||
*
|
||||
* @param action PULLING or PUSHING or CANCELED_MID_PUSH
|
||||
*/
|
||||
public void setAction(PistonValueType action) {
|
||||
if (this.action == action) {
|
||||
return;
|
||||
}
|
||||
placeFinalBlocks();
|
||||
removeMovingBlocks();
|
||||
|
||||
this.action = action;
|
||||
if (action == PistonValueType.PUSHING || (action == PistonValueType.PULLING && sticky)) {
|
||||
// Blocks only move when pushing or pulling with sticky pistons
|
||||
findAffectedBlocks();
|
||||
removeBlocks();
|
||||
createMovingBlocks();
|
||||
} else {
|
||||
removePistonHead();
|
||||
}
|
||||
placedFinalBlocks = false;
|
||||
|
||||
// Set progress and lastProgress to allow 0 tick pistons to animate
|
||||
switch (action) {
|
||||
case PUSHING:
|
||||
progress = 0;
|
||||
break;
|
||||
case PULLING:
|
||||
case CANCELLED_MID_PUSH:
|
||||
progress = 1;
|
||||
break;
|
||||
}
|
||||
lastProgress = progress;
|
||||
|
||||
BlockEntityUtils.updateBlockEntity(session, buildPistonTag(), position);
|
||||
}
|
||||
|
||||
public void setAction(PistonValueType action, Object2IntMap<Vector3i> attachedBlocks) {
|
||||
// Don't check if this.action == action, since on some Paper versions BlockPistonRetractEvent is called multiple times
|
||||
// with the first 1-2 events being empty.
|
||||
placeFinalBlocks();
|
||||
removeMovingBlocks();
|
||||
|
||||
this.action = action;
|
||||
if (action == PistonValueType.PUSHING || (action == PistonValueType.PULLING && sticky)) {
|
||||
// Blocks only move when pushing or pulling with sticky pistons
|
||||
if (attachedBlocks.size() <= 12) {
|
||||
this.attachedBlocks.putAll(attachedBlocks);
|
||||
flattenPositions();
|
||||
}
|
||||
removeBlocks();
|
||||
createMovingBlocks();
|
||||
} else {
|
||||
removePistonHead();
|
||||
}
|
||||
placedFinalBlocks = false;
|
||||
|
||||
// Set progress and lastProgress to allow 0 tick pistons to animate
|
||||
switch (action) {
|
||||
case PUSHING:
|
||||
progress = 0;
|
||||
break;
|
||||
case PULLING:
|
||||
case CANCELLED_MID_PUSH:
|
||||
progress = 1;
|
||||
break;
|
||||
}
|
||||
lastProgress = progress;
|
||||
|
||||
BlockEntityUtils.updateBlockEntity(session, buildPistonTag(), position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position of the piston head, moving blocks, and players.
|
||||
*/
|
||||
public void updateMovement() {
|
||||
if (isDone()) {
|
||||
timeSinceCompletion++;
|
||||
return;
|
||||
} else {
|
||||
timeSinceCompletion = 0;
|
||||
}
|
||||
updateProgress();
|
||||
pushPlayer();
|
||||
BlockEntityUtils.updateBlockEntity(session, buildPistonTag(), position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Place attached blocks in their final position when done pushing or pulling
|
||||
*/
|
||||
public void updateBlocks() {
|
||||
if (isDone()) {
|
||||
// Update blocks only once
|
||||
if (timeSinceCompletion == 0) {
|
||||
placeFinalBlocks();
|
||||
}
|
||||
// Give a few ticks for player collisions to be fully resolved
|
||||
if (timeSinceCompletion >= REMOVAL_DELAY) {
|
||||
removeMovingBlocks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removePistonHead() {
|
||||
Vector3i blockInFront = position.add(orientation.getUnitVector());
|
||||
int blockId = session.getConnector().getWorldManager().getBlockAt(session, blockInFront);
|
||||
if (BlockStateValues.isPistonHead(blockId)) {
|
||||
ChunkUtils.updateBlock(session, BlockStateValues.JAVA_AIR_ID, blockInFront);
|
||||
} else if (session.getConnector().getPlatformType() == PlatformType.SPIGOT && blockId == BlockStateValues.JAVA_AIR_ID) {
|
||||
// Spigot removes the piston head from the cache, but we need to send the block update ourselves
|
||||
ChunkUtils.updateBlock(session, BlockStateValues.JAVA_AIR_ID, blockInFront);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the blocks that will be pushed or pulled by the piston
|
||||
*/
|
||||
private void findAffectedBlocks() {
|
||||
Set<Vector3i> blocksChecked = new ObjectOpenHashSet<>();
|
||||
Queue<Vector3i> blocksToCheck = new LinkedList<>();
|
||||
|
||||
Vector3i directionOffset = orientation.getUnitVector();
|
||||
Vector3i movement = getMovement();
|
||||
blocksChecked.add(position); // Don't check the piston itself
|
||||
if (action == PistonValueType.PULLING) {
|
||||
blocksChecked.add(getPistonHeadPos()); // Don't check the piston head
|
||||
blocksToCheck.add(position.add(directionOffset.mul(2)));
|
||||
} else if (action == PistonValueType.PUSHING) {
|
||||
removePistonHead(); // Remove lingering piston heads
|
||||
blocksToCheck.add(position.add(directionOffset));
|
||||
}
|
||||
|
||||
boolean moveBlocks = true;
|
||||
while (!blocksToCheck.isEmpty() && attachedBlocks.size() <= 12) {
|
||||
Vector3i blockPos = blocksToCheck.remove();
|
||||
// Skip blocks we've already checked
|
||||
if (!blocksChecked.add(blockPos)) {
|
||||
continue;
|
||||
}
|
||||
int blockId = session.getConnector().getWorldManager().getBlockAt(session, blockPos);
|
||||
if (blockId == BlockStateValues.JAVA_AIR_ID) {
|
||||
continue;
|
||||
}
|
||||
if (canMoveBlock(blockId, action == PistonValueType.PUSHING)) {
|
||||
attachedBlocks.put(blockPos, blockId);
|
||||
if (BlockStateValues.isBlockSticky(blockId)) {
|
||||
// For honey blocks and slime blocks check the blocks adjacent to it
|
||||
for (Direction direction : Direction.VALUES) {
|
||||
Vector3i offset = direction.getUnitVector();
|
||||
// Only check blocks that aren't being pushed by the current block
|
||||
if (offset.equals(movement)) {
|
||||
continue;
|
||||
}
|
||||
Vector3i adjacentPos = blockPos.add(offset);
|
||||
// Ignore the piston block itself
|
||||
if (adjacentPos.equals(position)) {
|
||||
continue;
|
||||
}
|
||||
// Ignore the piston head
|
||||
if (action == PistonValueType.PULLING && position.add(directionOffset).equals(adjacentPos)) {
|
||||
continue;
|
||||
}
|
||||
int adjacentBlockId = session.getConnector().getWorldManager().getBlockAt(session, adjacentPos);
|
||||
if (adjacentBlockId != BlockStateValues.JAVA_AIR_ID && BlockStateValues.isBlockAttached(blockId, adjacentBlockId) && canMoveBlock(adjacentBlockId, false)) {
|
||||
// If it is another slime/honey block we need to check its adjacent blocks
|
||||
if (BlockStateValues.isBlockSticky(adjacentBlockId)) {
|
||||
blocksToCheck.add(adjacentPos);
|
||||
} else {
|
||||
attachedBlocks.put(adjacentPos, adjacentBlockId);
|
||||
blocksChecked.add(adjacentPos);
|
||||
blocksToCheck.add(adjacentPos.add(movement));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check next block in line
|
||||
blocksToCheck.add(blockPos.add(movement));
|
||||
} else if (!BlockStateValues.canPistonDestroyBlock(blockId)) {
|
||||
// Block can't be moved or destroyed, so it blocks all block movement
|
||||
moveBlocks = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!moveBlocks || attachedBlocks.size() > 12) {
|
||||
attachedBlocks.clear();
|
||||
} else {
|
||||
flattenPositions();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canMoveBlock(int javaId, boolean isPushing) {
|
||||
if (javaId == BlockStateValues.JAVA_AIR_ID) {
|
||||
return true;
|
||||
}
|
||||
// Pistons can only be moved if they aren't extended
|
||||
if (PistonBlockEntityTranslator.isBlock(javaId)) {
|
||||
return !BlockStateValues.getPistonValues().get(javaId);
|
||||
}
|
||||
BlockMapping block = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaId, BlockMapping.AIR);
|
||||
// Bedrock, End portal frames, etc. can't be moved
|
||||
if (block.getHardness() == -1.0d) {
|
||||
return false;
|
||||
}
|
||||
switch (block.getPistonBehavior()) {
|
||||
case BLOCK:
|
||||
case DESTROY:
|
||||
return false;
|
||||
case PUSH_ONLY: // Glazed terracotta can only be pushed
|
||||
return isPushing;
|
||||
}
|
||||
// Pistons can't move block entities
|
||||
return !block.isBlockEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unit vector for the direction of movement
|
||||
*
|
||||
* @return The movement of the blocks
|
||||
*/
|
||||
private Vector3i getMovement() {
|
||||
if (action == PistonValueType.PULLING) {
|
||||
return orientation.reversed().getUnitVector();
|
||||
}
|
||||
return orientation.getUnitVector(); // PUSHING and CANCELLED_MID_PUSH
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all attached blocks with air
|
||||
*/
|
||||
private void removeBlocks() {
|
||||
for (Vector3i blockPos : attachedBlocks.keySet()) {
|
||||
ChunkUtils.updateBlock(session, BlockStateValues.JAVA_AIR_ID, blockPos);
|
||||
}
|
||||
if (action != PistonValueType.PUSHING) {
|
||||
removePistonHead();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the player
|
||||
* If the player is pushed, the displacement is added to playerDisplacement in PistonCache
|
||||
* If the player contacts a slime block, playerMotion in PistonCache is updated
|
||||
*/
|
||||
public void pushPlayer() {
|
||||
Vector3i direction = orientation.getUnitVector();
|
||||
double blockMovement = lastProgress;
|
||||
if (action == PistonValueType.PULLING || action == PistonValueType.CANCELLED_MID_PUSH) {
|
||||
blockMovement = 1f - lastProgress;
|
||||
}
|
||||
|
||||
BoundingBox playerBoundingBox = session.getCollisionManager().getPlayerBoundingBox();
|
||||
// Shrink the collision in the other axes slightly, to avoid false positives when pressed up against the side of blocks
|
||||
Vector3d shrink = Vector3i.ONE.sub(direction.abs()).toDouble().mul(CollisionManager.COLLISION_TOLERANCE * 2);
|
||||
playerBoundingBox.setSizeX(playerBoundingBox.getSizeX() - shrink.getX());
|
||||
playerBoundingBox.setSizeY(playerBoundingBox.getSizeY() - shrink.getY());
|
||||
playerBoundingBox.setSizeZ(playerBoundingBox.getSizeZ() - shrink.getZ());
|
||||
|
||||
// Resolve collision with the piston head
|
||||
int pistonHeadId = BlockStateValues.getPistonHead(orientation);
|
||||
pushPlayerBlock(pistonHeadId, getPistonHeadPos().toDouble(), blockMovement, playerBoundingBox);
|
||||
|
||||
// Resolve collision with any attached moving blocks, but skip slime blocks
|
||||
// This prevents players from being launched by slime blocks covered by other blocks
|
||||
for (Object2IntMap.Entry<Vector3i> entry : attachedBlocks.object2IntEntrySet()) {
|
||||
int blockId = entry.getIntValue();
|
||||
if (blockId != BlockStateValues.JAVA_SLIME_BLOCK_ID) {
|
||||
Vector3d blockPos = entry.getKey().toDouble();
|
||||
pushPlayerBlock(blockId, blockPos, blockMovement, playerBoundingBox);
|
||||
}
|
||||
}
|
||||
// Resolve collision with slime blocks
|
||||
for (Object2IntMap.Entry<Vector3i> entry : attachedBlocks.object2IntEntrySet()) {
|
||||
int blockId = entry.getIntValue();
|
||||
if (blockId == BlockStateValues.JAVA_SLIME_BLOCK_ID) {
|
||||
Vector3d blockPos = entry.getKey().toDouble();
|
||||
pushPlayerBlock(blockId, blockPos, blockMovement, playerBoundingBox);
|
||||
}
|
||||
}
|
||||
|
||||
// Undo shrink
|
||||
playerBoundingBox.setSizeX(playerBoundingBox.getSizeX() + shrink.getX());
|
||||
playerBoundingBox.setSizeY(playerBoundingBox.getSizeY() + shrink.getY());
|
||||
playerBoundingBox.setSizeZ(playerBoundingBox.getSizeZ() + shrink.getZ());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player is attached to the top of a honey block
|
||||
*
|
||||
* @param blockPos The position of the honey block
|
||||
* @param playerBoundingBox The player's bounding box
|
||||
* @return True if the player attached, otherwise false
|
||||
*/
|
||||
private boolean isPlayerAttached(Vector3d blockPos, BoundingBox playerBoundingBox) {
|
||||
if (orientation.isVertical()) {
|
||||
return false;
|
||||
}
|
||||
return session.getPlayerEntity().isOnGround() && HONEY_BOUNDING_BOX.checkIntersection(blockPos, playerBoundingBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a player if the player is on the pushing side of the slime block
|
||||
*
|
||||
* @param blockPos The position of the slime block
|
||||
* @param playerPos The player's position
|
||||
*/
|
||||
private void applySlimeBlockMotion(Vector3d blockPos, Vector3d playerPos) {
|
||||
Direction movementDirection = orientation;
|
||||
// Invert direction when pulling
|
||||
if (action == PistonValueType.PULLING) {
|
||||
movementDirection = movementDirection.reversed();
|
||||
}
|
||||
|
||||
Vector3f movement = getMovement().toFloat();
|
||||
Vector3f motion = session.getPistonCache().getPlayerMotion();
|
||||
double motionX = motion.getX();
|
||||
double motionY = motion.getY();
|
||||
double motionZ = motion.getZ();
|
||||
blockPos = blockPos.add(0.5, 0.5, 0.5); // Move to the center of the slime block
|
||||
switch (movementDirection) {
|
||||
case DOWN:
|
||||
if (playerPos.getY() < blockPos.getY()) {
|
||||
motionY = movement.getY();
|
||||
}
|
||||
break;
|
||||
case UP:
|
||||
if (playerPos.getY() > blockPos.getY()) {
|
||||
motionY = movement.getY();
|
||||
}
|
||||
break;
|
||||
case NORTH:
|
||||
if (playerPos.getZ() < blockPos.getZ()) {
|
||||
motionZ = movement.getZ();
|
||||
}
|
||||
break;
|
||||
case SOUTH:
|
||||
if (playerPos.getZ() > blockPos.getZ()) {
|
||||
motionZ = movement.getZ();
|
||||
}
|
||||
break;
|
||||
case WEST:
|
||||
if (playerPos.getX() < blockPos.getX()) {
|
||||
motionX = movement.getX();
|
||||
}
|
||||
break;
|
||||
case EAST:
|
||||
if (playerPos.getX() > blockPos.getX()) {
|
||||
motionX = movement.getX();
|
||||
}
|
||||
break;
|
||||
}
|
||||
session.getPistonCache().setPlayerMotion(Vector3f.from(motionX, motionY, motionZ));
|
||||
}
|
||||
|
||||
private double getBlockIntersection(BlockCollision blockCollision, Vector3d blockPos, Vector3d extend, BoundingBox boundingBox, Direction direction) {
|
||||
Direction oppositeDirection = direction.reversed();
|
||||
double maxIntersection = 0;
|
||||
for (BoundingBox b : blockCollision.getBoundingBoxes()) {
|
||||
b = b.clone();
|
||||
b.extend(extend);
|
||||
b.translate(blockPos.getX(), blockPos.getY(), blockPos.getZ());
|
||||
if (b.checkIntersection(Vector3d.ZERO, boundingBox)) {
|
||||
double intersection = boundingBox.getIntersectionSize(b, direction);
|
||||
double oppositeIntersection = boundingBox.getIntersectionSize(b, oppositeDirection);
|
||||
if (intersection < oppositeIntersection) {
|
||||
maxIntersection = Math.max(intersection, maxIntersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxIntersection;
|
||||
}
|
||||
|
||||
private void pushPlayerBlock(int javaId, Vector3d startingPos, double blockMovement, BoundingBox playerBoundingBox) {
|
||||
PistonCache pistonCache = session.getPistonCache();
|
||||
Vector3d movement = getMovement().toDouble();
|
||||
// Check if the player collides with the movingBlock block entity
|
||||
Vector3d finalBlockPos = startingPos.add(movement);
|
||||
if (SOLID_BOUNDING_BOX.checkIntersection(finalBlockPos, playerBoundingBox)) {
|
||||
pistonCache.setPlayerCollided(true);
|
||||
|
||||
if (javaId == BlockStateValues.JAVA_SLIME_BLOCK_ID) {
|
||||
pistonCache.setPlayerSlimeCollision(true);
|
||||
applySlimeBlockMotion(finalBlockPos, Vector3d.from(playerBoundingBox.getMiddleX(), playerBoundingBox.getMiddleY(), playerBoundingBox.getMiddleZ()));
|
||||
}
|
||||
}
|
||||
|
||||
Vector3d blockPos = startingPos.add(movement.mul(blockMovement));
|
||||
if (javaId == BlockStateValues.JAVA_HONEY_BLOCK_ID && isPlayerAttached(blockPos, playerBoundingBox)) {
|
||||
pistonCache.setPlayerCollided(true);
|
||||
pistonCache.setPlayerAttachedToHoney(true);
|
||||
|
||||
double delta = Math.abs(progress - lastProgress);
|
||||
pistonCache.displacePlayer(movement.mul(delta));
|
||||
} else {
|
||||
// Move the player out of collision
|
||||
BlockCollision blockCollision = Registries.COLLISIONS.get(javaId);
|
||||
if (blockCollision != null) {
|
||||
Vector3d extend = movement.mul(Math.min(1 - blockMovement, 0.5));
|
||||
Direction movementDirection = orientation;
|
||||
if (action == PistonValueType.PULLING) {
|
||||
movementDirection = orientation.reversed();
|
||||
}
|
||||
|
||||
double intersection = getBlockIntersection(blockCollision, blockPos, extend, playerBoundingBox, movementDirection);
|
||||
if (intersection > 0) {
|
||||
pistonCache.setPlayerCollided(true);
|
||||
pistonCache.displacePlayer(movement.mul(intersection + 0.01d));
|
||||
|
||||
if (javaId == BlockStateValues.JAVA_SLIME_BLOCK_ID) {
|
||||
pistonCache.setPlayerSlimeCollision(true);
|
||||
applySlimeBlockMotion(blockPos, Vector3d.from(playerBoundingBox.getMiddleX(), playerBoundingBox.getMiddleY(), playerBoundingBox.getMiddleZ()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BlockCollision getCollision(Vector3i blockPos) {
|
||||
int blockId = getAttachedBlockId(blockPos);
|
||||
if (blockId != BlockStateValues.JAVA_AIR_ID) {
|
||||
double movementProgress = progress;
|
||||
if (action == PistonValueType.PULLING || action == PistonValueType.CANCELLED_MID_PUSH) {
|
||||
movementProgress = 1f - progress;
|
||||
}
|
||||
Vector3d offset = getMovement().toDouble().mul(movementProgress);
|
||||
return BlockUtils.getCollision(blockId, blockPos, offset);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the maximum movement of a bounding box that won't collide with the moving block attached to this piston
|
||||
*
|
||||
* @param blockPos The position of the moving block
|
||||
* @param boundingBox The bounding box of the moving entity
|
||||
* @param axis The axis of movement
|
||||
* @param movement The movement in the axis
|
||||
* @return The adjusted movement
|
||||
*/
|
||||
public double computeCollisionOffset(Vector3i blockPos, BoundingBox boundingBox, Axis axis, double movement) {
|
||||
BlockCollision blockCollision = getCollision(blockPos);
|
||||
if (blockCollision != null) {
|
||||
double adjustedMovement = blockCollision.computeCollisionOffset(boundingBox, axis, movement);
|
||||
blockCollision.reset();
|
||||
if (getAttachedBlockId(blockPos) == BlockStateValues.JAVA_SLIME_BLOCK_ID && adjustedMovement != movement) {
|
||||
session.getPistonCache().setPlayerSlimeCollision(true);
|
||||
}
|
||||
return adjustedMovement;
|
||||
}
|
||||
return movement;
|
||||
}
|
||||
|
||||
public boolean checkCollision(Vector3i blockPos, BoundingBox boundingBox) {
|
||||
BlockCollision blockCollision = getCollision(blockPos);
|
||||
if (blockCollision != null) {
|
||||
boolean result = blockCollision.checkIntersection(boundingBox);
|
||||
blockCollision.reset();
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int getAttachedBlockId(Vector3i blockPos) {
|
||||
if (blockPos.equals(getPistonHeadPos())) {
|
||||
return BlockStateValues.getPistonHead(orientation);
|
||||
} else {
|
||||
return attachedBlocks.getOrDefault(blockPos, BlockStateValues.JAVA_AIR_ID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create moving block entities for each attached block
|
||||
*/
|
||||
private void createMovingBlocks() {
|
||||
// Map the final position of each block to this block entity
|
||||
Map<Vector3i, PistonBlockEntity> movingBlockMap = session.getPistonCache().getMovingBlocksMap();
|
||||
attachedBlocks.forEach((blockPos, javaId) -> movingBlockMap.put(blockPos, this));
|
||||
movingBlockMap.put(getPistonHeadPos(), this);
|
||||
|
||||
Vector3i movement = getMovement();
|
||||
BoundingBox playerBoundingBox = session.getCollisionManager().getPlayerBoundingBox().clone();
|
||||
if (orientation == Direction.UP) {
|
||||
// Extend the bounding box down, to catch collisions when the player is falling down
|
||||
playerBoundingBox.extend(0, -256, 0);
|
||||
playerBoundingBox.setSizeX(playerBoundingBox.getSizeX() + 0.5);
|
||||
playerBoundingBox.setSizeZ(playerBoundingBox.getSizeZ() + 0.5);
|
||||
}
|
||||
attachedBlocks.forEach((blockPos, javaId) -> {
|
||||
Vector3i newPos = blockPos.add(movement);
|
||||
if (SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), playerBoundingBox) ||
|
||||
SOLID_BOUNDING_BOX.checkIntersection(newPos.toDouble(), playerBoundingBox)) {
|
||||
session.getPistonCache().setPlayerCollided(true);
|
||||
if (javaId == BlockStateValues.JAVA_SLIME_BLOCK_ID) {
|
||||
session.getPistonCache().setPlayerSlimeCollision(true);
|
||||
}
|
||||
// Don't place moving blocks that collide with the player
|
||||
// because of https://bugs.mojang.com/browse/MCPE-96035
|
||||
return;
|
||||
}
|
||||
// Place a moving block at the new location of the block
|
||||
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
|
||||
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS);
|
||||
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK);
|
||||
updateBlockPacket.setBlockPosition(newPos);
|
||||
updateBlockPacket.setRuntimeId(session.getBlockMappings().getBedrockMovingBlockId());
|
||||
updateBlockPacket.setDataLayer(0);
|
||||
session.sendUpstreamPacket(updateBlockPacket);
|
||||
// Update moving block with correct details
|
||||
BlockEntityUtils.updateBlockEntity(session, buildMovingBlockTag(newPos, javaId, position), newPos);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Place blocks that don't collide with the player into their final position
|
||||
* otherwise the player will fall off the block.
|
||||
* The Java server will handle updating the blocks that do collide later.
|
||||
*/
|
||||
private void placeFinalBlocks() {
|
||||
// Prevent blocks from being placed multiple times since it is called in
|
||||
// setAction and updateBlocks
|
||||
if (placedFinalBlocks) {
|
||||
return;
|
||||
}
|
||||
placedFinalBlocks = true;
|
||||
Vector3i movement = getMovement();
|
||||
attachedBlocks.forEach((blockPos, javaId) -> {
|
||||
blockPos = blockPos.add(movement);
|
||||
// Send a final block entity packet to detach blocks
|
||||
BlockEntityUtils.updateBlockEntity(session, buildMovingBlockTag(blockPos, javaId, Direction.DOWN.getUnitVector()), blockPos);
|
||||
// Don't place blocks that collide with the player
|
||||
if (!SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) {
|
||||
ChunkUtils.updateBlock(session, javaId, blockPos);
|
||||
}
|
||||
});
|
||||
if (action == PistonValueType.PUSHING) {
|
||||
Vector3i pistonHeadPos = getPistonHeadPos().add(movement);
|
||||
if (!SOLID_BOUNDING_BOX.checkIntersection(pistonHeadPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) {
|
||||
ChunkUtils.updateBlock(session, BlockStateValues.getPistonHead(orientation), pistonHeadPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove moving blocks from the piston cache
|
||||
*/
|
||||
private void removeMovingBlocks() {
|
||||
Map<Vector3i, PistonBlockEntity> movingBlockMap = session.getPistonCache().getMovingBlocksMap();
|
||||
attachedBlocks.forEach((blockPos, javaId) -> movingBlockMap.remove(blockPos));
|
||||
attachedBlocks.clear();
|
||||
movingBlockMap.remove(getPistonHeadPos());
|
||||
flattenedAttachedBlocks = new int[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten the positions of attached blocks into a 1D array
|
||||
*/
|
||||
private void flattenPositions() {
|
||||
flattenedAttachedBlocks = new int[3 * attachedBlocks.size()];
|
||||
int i = 0;
|
||||
for (Vector3i position : attachedBlocks.keySet()) {
|
||||
flattenedAttachedBlocks[3 * i] = position.getX();
|
||||
flattenedAttachedBlocks[3 * i + 1] = position.getY();
|
||||
flattenedAttachedBlocks[3 * i + 2] = position.getZ();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bedrock state of the piston
|
||||
*
|
||||
* @return 0 - Fully retracted, 1 - Extending, 2 - Fully extended, 3 - Retracting
|
||||
*/
|
||||
private byte getState() {
|
||||
switch (action) {
|
||||
case PUSHING:
|
||||
return (byte) (isDone() ? 2 : 1);
|
||||
case PULLING:
|
||||
return (byte) (isDone() ? 0 : 3);
|
||||
default:
|
||||
if (progress == 1.0f) {
|
||||
return 2;
|
||||
}
|
||||
return (byte) (isDone() ? 0 : 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The starting position of the piston head
|
||||
*/
|
||||
private Vector3i getPistonHeadPos() {
|
||||
if (action == PistonValueType.PUSHING) {
|
||||
return position;
|
||||
}
|
||||
return position.add(orientation.getUnitVector());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the progress or position of the piston head
|
||||
*/
|
||||
private void updateProgress() {
|
||||
switch (action) {
|
||||
case PUSHING:
|
||||
lastProgress = progress;
|
||||
progress += 0.5f;
|
||||
if (progress >= 1.0f) {
|
||||
progress = 1.0f;
|
||||
}
|
||||
break;
|
||||
case CANCELLED_MID_PUSH:
|
||||
case PULLING:
|
||||
lastProgress = progress;
|
||||
progress -= 0.5f;
|
||||
if (progress <= 0.0f) {
|
||||
progress = 0.0f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the piston has finished its movement, otherwise false
|
||||
*/
|
||||
public boolean isDone() {
|
||||
switch (action) {
|
||||
case PUSHING:
|
||||
return progress == 1.0f && lastProgress == 1.0f;
|
||||
case PULLING:
|
||||
case CANCELLED_MID_PUSH:
|
||||
return progress == 0.0f && lastProgress == 0.0f;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canBeRemoved() {
|
||||
return isDone() && timeSinceCompletion > REMOVAL_DELAY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a piston data tag with the data in this block entity
|
||||
*
|
||||
* @return A piston data tag
|
||||
*/
|
||||
private NbtMap buildPistonTag() {
|
||||
NbtMapBuilder builder = NbtMap.builder()
|
||||
.putString("id", "PistonArm")
|
||||
.putIntArray("AttachedBlocks", flattenedAttachedBlocks)
|
||||
.putFloat("Progress", progress)
|
||||
.putFloat("LastProgress", lastProgress)
|
||||
.putByte("NewState", getState())
|
||||
.putByte("State", getState())
|
||||
.putByte("Sticky", (byte) (sticky ? 1 : 0))
|
||||
.putByte("isMovable", (byte) 0)
|
||||
.putInt("x", position.getX())
|
||||
.putInt("y", position.getY())
|
||||
.putInt("z", position.getZ());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a piston data tag that has fully extended/retracted
|
||||
*
|
||||
* @param position The position for the base of the piston
|
||||
* @param extended Whether the piston is extended or retracted
|
||||
* @param sticky Whether the piston is a sticky piston or a regular piston
|
||||
* @return A piston data tag for a fully extended/retracted piston
|
||||
*/
|
||||
public static NbtMap buildStaticPistonTag(Vector3i position, boolean extended, boolean sticky) {
|
||||
NbtMapBuilder builder = NbtMap.builder()
|
||||
.putString("id", "PistonArm")
|
||||
.putFloat("Progress", extended ? 1.0f : 0.0f)
|
||||
.putFloat("LastProgress", extended ? 1.0f : 0.0f)
|
||||
.putByte("NewState", (byte) (extended ? 2 : 0))
|
||||
.putByte("State", (byte) (extended ? 2 : 0))
|
||||
.putByte("Sticky", (byte) (sticky ? 1 : 0))
|
||||
.putByte("isMovable", (byte) 0)
|
||||
.putInt("x", position.getX())
|
||||
.putInt("y", position.getY())
|
||||
.putInt("z", position.getZ());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a moving block tag of a block that will be moved by a piston
|
||||
*
|
||||
* @param position The ending position of the block (The location of the movingBlock block entity)
|
||||
* @param javaId The Java Id of the block that is moving
|
||||
* @param pistonPosition The position for the base of the piston that's moving the block
|
||||
* @return A moving block data tag
|
||||
*/
|
||||
private NbtMap buildMovingBlockTag(Vector3i position, int javaId, Vector3i pistonPosition) {
|
||||
// Get Bedrock block state data
|
||||
NbtMap movingBlock = session.getBlockMappings().getBedrockBlockStates().get(session.getBlockMappings().getBedrockBlockId(javaId));
|
||||
NbtMapBuilder builder = NbtMap.builder()
|
||||
.putString("id", "MovingBlock")
|
||||
.putCompound("movingBlock", movingBlock)
|
||||
.putByte("isMovable", (byte) 1)
|
||||
.putInt("pistonPosX", pistonPosition.getX())
|
||||
.putInt("pistonPosY", pistonPosition.getY())
|
||||
.putInt("pistonPosZ", pistonPosition.getZ())
|
||||
.putInt("x", position.getX())
|
||||
.putInt("y", position.getY())
|
||||
.putInt("z", position.getZ());
|
||||
if (PistonBlockEntityTranslator.isBlock(javaId)) {
|
||||
builder.putCompound("movingEntity", PistonBlockEntityTranslator.getTag(javaId, position));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
@ -27,7 +27,6 @@ package org.geysermc.connector.network.translators.world.block.entity;
|
||||
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import com.nukkitx.nbt.NbtMap;
|
||||
import com.nukkitx.nbt.NbtMapBuilder;
|
||||
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||
|
||||
/**
|
||||
@ -52,19 +51,8 @@ public class PistonBlockEntityTranslator {
|
||||
* @return Bedrock tag of piston.
|
||||
*/
|
||||
public static NbtMap getTag(int blockState, Vector3i position) {
|
||||
NbtMapBuilder tagBuilder = NbtMap.builder()
|
||||
.putInt("x", position.getX())
|
||||
.putInt("y", position.getY())
|
||||
.putInt("z", position.getZ())
|
||||
.putByte("isMovable", (byte) 1)
|
||||
.putString("id", "PistonArm");
|
||||
|
||||
boolean extended = BlockStateValues.getPistonValues().get(blockState);
|
||||
// 1f if extended, otherwise 0f
|
||||
tagBuilder.putFloat("Progress", (extended) ? 1.0f : 0.0f);
|
||||
// 1 if sticky, 0 if not
|
||||
tagBuilder.putByte("Sticky", (byte) ((BlockStateValues.isStickyPiston(blockState)) ? 1 : 0));
|
||||
|
||||
return tagBuilder.build();
|
||||
boolean sticky = BlockStateValues.isStickyPiston(blockState);
|
||||
return PistonBlockEntity.buildStaticPistonTag(position, extended, sticky);
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ import org.geysermc.connector.registry.type.BlockMapping;
|
||||
import org.geysermc.connector.registry.type.BlockMappings;
|
||||
import org.geysermc.connector.utils.BlockUtils;
|
||||
import org.geysermc.connector.utils.FileUtils;
|
||||
import org.geysermc.connector.utils.PistonBehavior;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.InputStream;
|
||||
@ -133,6 +134,7 @@ public class BlockRegistryPopulator {
|
||||
int commandBlockRuntimeId = -1;
|
||||
int javaRuntimeId = -1;
|
||||
int waterRuntimeId = -1;
|
||||
int movingBlockRuntimeId = -1;
|
||||
Iterator<Map.Entry<String, JsonNode>> blocksIterator = BLOCKS_JSON.fields();
|
||||
|
||||
BiFunction<String, NbtMapBuilder, String> stateMapper = STATE_MAPPER.getOrDefault(palette.getKey(), (i, s) -> null);
|
||||
@ -166,6 +168,8 @@ public class BlockRegistryPopulator {
|
||||
case "minecraft:command_block[conditional=false,facing=north]":
|
||||
commandBlockRuntimeId = bedrockRuntimeId;
|
||||
break;
|
||||
case "minecraft:moving_piston[facing=north,type=normal]":
|
||||
movingBlockRuntimeId = bedrockRuntimeId;
|
||||
}
|
||||
|
||||
if (javaId.contains("jigsaw")) {
|
||||
@ -209,6 +213,11 @@ public class BlockRegistryPopulator {
|
||||
}
|
||||
builder.bedrockAirId(airRuntimeId);
|
||||
|
||||
if (movingBlockRuntimeId == -1) {
|
||||
throw new AssertionError("Unable to find moving block in palette");
|
||||
}
|
||||
builder.bedrockMovingBlockId(movingBlockRuntimeId);
|
||||
|
||||
// Loop around again to find all item frame runtime IDs
|
||||
for (Object2IntMap.Entry<NbtMap> entry : blockStateOrderedMap.object2IntEntrySet()) {
|
||||
String name = entry.getKey().getString("name");
|
||||
@ -248,6 +257,8 @@ public class BlockRegistryPopulator {
|
||||
int cobwebBlockId = -1;
|
||||
int furnaceRuntimeId = -1;
|
||||
int furnaceLitRuntimeId = -1;
|
||||
int honeyBlockRuntimeId = -1;
|
||||
int slimeBlockRuntimeId = -1;
|
||||
int spawnerRuntimeId = -1;
|
||||
int uniqueJavaId = -1;
|
||||
int waterRuntimeId = -1;
|
||||
@ -281,6 +292,24 @@ public class BlockRegistryPopulator {
|
||||
builder.pickItem(pickItemNode.textValue().intern());
|
||||
}
|
||||
|
||||
if (javaId.equals("minecraft:obsidian") || javaId.equals("minecraft:crying_obsidian") || javaId.startsWith("minecraft:respawn_anchor")) {
|
||||
builder.pistonBehavior(PistonBehavior.BLOCK);
|
||||
} else {
|
||||
JsonNode pistonBehaviorNode = entry.getValue().get("piston_behavior");
|
||||
if (pistonBehaviorNode != null) {
|
||||
builder.pistonBehavior(PistonBehavior.getByName(pistonBehaviorNode.textValue()));
|
||||
} else {
|
||||
builder.pistonBehavior(PistonBehavior.NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode hasBlockEntityNode = entry.getValue().get("has_block_entity");
|
||||
if (hasBlockEntityNode != null) {
|
||||
builder.isBlockEntity(hasBlockEntityNode.booleanValue());
|
||||
} else {
|
||||
builder.isBlockEntity(false);
|
||||
}
|
||||
|
||||
BlockStateValues.storeBlockStateValues(entry.getKey(), javaRuntimeId, entry.getValue());
|
||||
|
||||
String cleanJavaIdentifier = BlockUtils.getCleanIdentifier(entry.getKey());
|
||||
@ -320,6 +349,10 @@ public class BlockRegistryPopulator {
|
||||
|
||||
} else if ("minecraft:water[level=0]".equals(javaId)) {
|
||||
waterRuntimeId = javaRuntimeId;
|
||||
} else if (javaId.equals("minecraft:honey_block")) {
|
||||
honeyBlockRuntimeId = javaRuntimeId;
|
||||
} else if (javaId.equals("minecraft:slime_block")) {
|
||||
slimeBlockRuntimeId = javaRuntimeId;
|
||||
}
|
||||
}
|
||||
if (bellBlockId == -1) {
|
||||
@ -342,6 +375,16 @@ public class BlockRegistryPopulator {
|
||||
}
|
||||
BlockStateValues.JAVA_FURNACE_LIT_ID = furnaceLitRuntimeId;
|
||||
|
||||
if (honeyBlockRuntimeId == -1) {
|
||||
throw new AssertionError("Unable to find honey block in palette");
|
||||
}
|
||||
BlockStateValues.JAVA_HONEY_BLOCK_ID = honeyBlockRuntimeId;
|
||||
|
||||
if (slimeBlockRuntimeId == -1) {
|
||||
throw new AssertionError("Unable to find slime block in palette");
|
||||
}
|
||||
BlockStateValues.JAVA_SLIME_BLOCK_ID = slimeBlockRuntimeId;
|
||||
|
||||
if (spawnerRuntimeId == -1) {
|
||||
throw new AssertionError("Unable to find spawner in palette");
|
||||
}
|
||||
|
@ -28,7 +28,9 @@ package org.geysermc.connector.registry.type;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import org.geysermc.connector.utils.BlockUtils;
|
||||
import org.geysermc.connector.utils.PistonBehavior;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Builder
|
||||
@ -51,6 +53,10 @@ public class BlockMapping {
|
||||
int collisionIndex;
|
||||
@Nullable String pickItem;
|
||||
|
||||
@Nonnull
|
||||
PistonBehavior pistonBehavior;
|
||||
boolean isBlockEntity;
|
||||
|
||||
/**
|
||||
* @return the identifier without the additional block states
|
||||
*/
|
||||
|
@ -40,6 +40,7 @@ import java.util.Map;
|
||||
public class BlockMappings {
|
||||
int bedrockAirId;
|
||||
int bedrockWaterId;
|
||||
int bedrockMovingBlockId;
|
||||
|
||||
int blockStateVersion;
|
||||
|
||||
|
50
connector/src/main/java/org/geysermc/connector/utils/Axis.java
Normale Datei
50
connector/src/main/java/org/geysermc/connector/utils/Axis.java
Normale Datei
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.connector.utils;
|
||||
|
||||
import com.nukkitx.math.vector.Vector3d;
|
||||
|
||||
public enum Axis {
|
||||
X, Y, Z;
|
||||
|
||||
public static final Axis[] VALUES = values();
|
||||
|
||||
/**
|
||||
* @param vector The vector
|
||||
* @return The component of the vector in this axis
|
||||
*/
|
||||
public double choose(Vector3d vector) {
|
||||
switch (this) {
|
||||
case X:
|
||||
return vector.getX();
|
||||
case Y:
|
||||
return vector.getY();
|
||||
case Z:
|
||||
return vector.getZ();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ package org.geysermc.connector.utils;
|
||||
|
||||
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.nukkitx.math.vector.Vector3d;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import org.geysermc.connector.inventory.GeyserItemStack;
|
||||
import org.geysermc.connector.inventory.PlayerInventory;
|
||||
@ -247,6 +248,16 @@ public class BlockUtils {
|
||||
BlockCollision collision = Registries.COLLISIONS.get(blockId);
|
||||
if (collision != null) {
|
||||
collision.setPosition(blockPos);
|
||||
collision.setPositionOffset(null);
|
||||
}
|
||||
return collision;
|
||||
}
|
||||
|
||||
public static BlockCollision getCollision(int blockId, Vector3i blockPos, Vector3d blockOffset) {
|
||||
BlockCollision collision = Registries.COLLISIONS.get(blockId);
|
||||
if (collision != null) {
|
||||
collision.setPosition(blockPos);
|
||||
collision.setPositionOffset(blockOffset);
|
||||
}
|
||||
return collision;
|
||||
}
|
||||
|
@ -367,6 +367,9 @@ public class ChunkUtils {
|
||||
skull.despawnEntity(session, position);
|
||||
}
|
||||
|
||||
// Prevent moving_piston from being placed
|
||||
// It's used for extending piston heads, but it isn't needed on Bedrock and causes pistons to flicker
|
||||
if (!BlockStateValues.isMovingPiston(blockState)) {
|
||||
int blockId = session.getBlockMappings().getBedrockBlockId(blockState);
|
||||
|
||||
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
|
||||
@ -386,6 +389,7 @@ public class ChunkUtils {
|
||||
waterPacket.setRuntimeId(session.getBlockMappings().getBedrockAirId());
|
||||
}
|
||||
session.sendUpstreamPacket(waterPacket);
|
||||
}
|
||||
|
||||
BlockStateValues.getLecternBookStates().compute(blockState, (key, newLecternHasBook) -> {
|
||||
// Determine if this block is a lectern
|
||||
|
@ -65,6 +65,7 @@ public class DimensionUtils {
|
||||
session.getItemFrameCache().clear();
|
||||
session.getLecternCache().clear();
|
||||
session.getLodestoneCache().clear();
|
||||
session.getPistonCache().clear();
|
||||
session.getSkullCache().clear();
|
||||
|
||||
Vector3f pos = Vector3f.from(0, Short.MAX_VALUE, 0);
|
||||
|
80
connector/src/main/java/org/geysermc/connector/utils/Direction.java
Normale Datei
80
connector/src/main/java/org/geysermc/connector/utils/Direction.java
Normale Datei
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.connector.utils;
|
||||
|
||||
import com.github.steveice10.mc.protocol.data.game.world.block.value.PistonValue;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public enum Direction {
|
||||
DOWN(1, Vector3i.from(0, -1, 0), Axis.Y, PistonValue.DOWN),
|
||||
UP(0, Vector3i.UNIT_Y, Axis.Y, PistonValue.UP),
|
||||
NORTH(3, Vector3i.from(0, 0, -1), Axis.Z, PistonValue.NORTH),
|
||||
SOUTH(2, Vector3i.UNIT_Z, Axis.Z, PistonValue.SOUTH),
|
||||
WEST(5, Vector3i.from(-1, 0, 0), Axis.X, PistonValue.WEST),
|
||||
EAST(4, Vector3i.UNIT_X, Axis.X, PistonValue.EAST);
|
||||
|
||||
public static final Direction[] VALUES = values();
|
||||
|
||||
private final int reversedId;
|
||||
@Getter
|
||||
private final Vector3i unitVector;
|
||||
@Getter
|
||||
private final Axis axis;
|
||||
@Getter
|
||||
private final PistonValue pistonValue;
|
||||
|
||||
Direction(int reversedId, Vector3i unitVector, Axis axis, PistonValue pistonValue) {
|
||||
this.reversedId = reversedId;
|
||||
this.unitVector = unitVector;
|
||||
this.axis = axis;
|
||||
this.pistonValue = pistonValue;
|
||||
}
|
||||
|
||||
public Direction reversed() {
|
||||
return VALUES[reversedId];
|
||||
}
|
||||
|
||||
public boolean isVertical() {
|
||||
return axis == Axis.Y;
|
||||
}
|
||||
|
||||
public boolean isHorizontal() {
|
||||
return axis == Axis.X || axis == Axis.Z;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Direction fromPistonValue(PistonValue pistonValue) {
|
||||
for (Direction direction : VALUES) {
|
||||
if (direction.pistonValue == pistonValue) {
|
||||
return direction;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.connector.utils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum PistonBehavior {
|
||||
NORMAL,
|
||||
BLOCK,
|
||||
DESTROY,
|
||||
PUSH_ONLY;
|
||||
|
||||
public static final PistonBehavior[] VALUES = values();
|
||||
|
||||
public static PistonBehavior getByName(String name) {
|
||||
String upperCase = name.toUpperCase(Locale.ROOT);
|
||||
for (PistonBehavior type : VALUES) {
|
||||
if (type.name().equals(upperCase)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return NORMAL;
|
||||
}
|
||||
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren