From 55523cfcfcd14a77b0edfa5cdd240085aa4c9a6f Mon Sep 17 00:00:00 2001 From: Bukkit/Spigot Date: Fri, 26 Oct 2018 19:59:36 +1100 Subject: [PATCH] Add ray tracing and bounding box API By: blablubbabc --- .../java/org/bukkit/FluidCollisionMode.java | 20 + paper-api/src/main/java/org/bukkit/World.java | 225 +++- .../src/main/java/org/bukkit/block/Block.java | 15 + .../main/java/org/bukkit/block/BlockFace.java | 15 + .../ConfigurationSerialization.java | 2 + .../main/java/org/bukkit/entity/Entity.java | 11 + .../java/org/bukkit/entity/LivingEntity.java | 75 +- .../java/org/bukkit/util/BlockIterator.java | 16 +- .../java/org/bukkit/util/BoundingBox.java | 1030 +++++++++++++++++ .../java/org/bukkit/util/RayTraceResult.java | 157 +++ .../test/java/org/bukkit/LocationTest.java | 12 +- .../java/org/bukkit/util/BoundingBoxTest.java | 206 ++++ 12 files changed, 1770 insertions(+), 14 deletions(-) create mode 100644 paper-api/src/main/java/org/bukkit/FluidCollisionMode.java create mode 100644 paper-api/src/main/java/org/bukkit/util/BoundingBox.java create mode 100644 paper-api/src/main/java/org/bukkit/util/RayTraceResult.java create mode 100644 paper-api/src/test/java/org/bukkit/util/BoundingBoxTest.java diff --git a/paper-api/src/main/java/org/bukkit/FluidCollisionMode.java b/paper-api/src/main/java/org/bukkit/FluidCollisionMode.java new file mode 100644 index 0000000000..ae28958941 --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/FluidCollisionMode.java @@ -0,0 +1,20 @@ +package org.bukkit; + +/** + * Determines the collision behavior when fluids get hit during ray tracing. + */ +public enum FluidCollisionMode { + + /** + * Ignore fluids. + */ + NEVER, + /** + * Only collide with source fluid blocks. + */ + SOURCE_ONLY, + /** + * Collide with all fluids. + */ + ALWAYS; +} diff --git a/paper-api/src/main/java/org/bukkit/World.java b/paper-api/src/main/java/org/bukkit/World.java index 93ff5f5ad5..89a08cc93a 100644 --- a/paper-api/src/main/java/org/bukkit/World.java +++ b/paper-api/src/main/java/org/bukkit/World.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Predicate; import org.bukkit.block.Biome; import org.bukkit.block.Block; @@ -17,7 +18,9 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.material.MaterialData; import org.bukkit.metadata.Metadatable; import org.bukkit.plugin.messaging.PluginMessageRecipient; +import org.bukkit.util.BoundingBox; import org.bukkit.util.Consumer; +import org.bukkit.util.RayTraceResult; import org.bukkit.util.Vector; /** @@ -425,18 +428,232 @@ public interface World extends PluginMessageRecipient, Metadatable { public List getPlayers(); /** - * Returns a list of entities within a bounding box centered around a Location. - * - * Some implementations may impose artificial restrictions on the size of the search bounding box. + * Returns a list of entities within a bounding box centered around a + * Location. + *

+ * This may not consider entities in currently unloaded chunks. Some + * implementations may impose artificial restrictions on the size of the + * search bounding box. * * @param location The center of the bounding box * @param x 1/2 the size of the box along x axis * @param y 1/2 the size of the box along y axis * @param z 1/2 the size of the box along z axis - * @return the collection of entities near location. This will always be a non-null collection. + * @return the collection of entities near location. This will always be a + * non-null collection. */ public Collection getNearbyEntities(Location location, double x, double y, double z); + /** + * Returns a list of entities within a bounding box centered around a + * Location. + *

+ * This may not consider entities in currently unloaded chunks. Some + * implementations may impose artificial restrictions on the size of the + * search bounding box. + * + * @param location The center of the bounding box + * @param x 1/2 the size of the box along x axis + * @param y 1/2 the size of the box along y axis + * @param z 1/2 the size of the box along z axis + * @param filter only entities that fulfill this predicate are considered, + * or null to consider all entities + * @return the collection of entities near location. This will always be a + * non-null collection. + */ + public Collection getNearbyEntities(Location location, double x, double y, double z, Predicate filter); + + /** + * Returns a list of entities within the given bounding box. + *

+ * This may not consider entities in currently unloaded chunks. Some + * implementations may impose artificial restrictions on the size of the + * search bounding box. + * + * @param boundingBox the bounding box + * @return the collection of entities within the bounding box, will always + * be a non-null collection + */ + public Collection getNearbyEntities(BoundingBox boundingBox); + + /** + * Returns a list of entities within the given bounding box. + *

+ * This may not consider entities in currently unloaded chunks. Some + * implementations may impose artificial restrictions on the size of the + * search bounding box. + * + * @param boundingBox the bounding box + * @param filter only entities that fulfill this predicate are considered, + * or null to consider all entities + * @return the collection of entities within the bounding box, will always + * be a non-null collection + */ + public Collection getNearbyEntities(BoundingBox boundingBox, Predicate filter); + + /** + * Performs a ray trace that checks for entity collisions. + *

+ * This may not consider entities in currently unloaded chunks. Some + * implementations may impose artificial restrictions on the maximum + * distance. + * + * @param start the start position + * @param direction the ray direction + * @param maxDistance the maximum distance + * @return the closest ray trace hit result, or null if there + * is no hit + * @see #rayTraceEntities(Location, Vector, double, double, Predicate) + */ + public RayTraceResult rayTraceEntities(Location start, Vector direction, double maxDistance); + + /** + * Performs a ray trace that checks for entity collisions. + *

+ * This may not consider entities in currently unloaded chunks. Some + * implementations may impose artificial restrictions on the maximum + * distance. + * + * @param start the start position + * @param direction the ray direction + * @param maxDistance the maximum distance + * @param raySize entity bounding boxes will be uniformly expanded (or + * shrinked) by this value before doing collision checks + * @return the closest ray trace hit result, or null if there + * is no hit + * @see #rayTraceEntities(Location, Vector, double, double, Predicate) + */ + public RayTraceResult rayTraceEntities(Location start, Vector direction, double maxDistance, double raySize); + + /** + * Performs a ray trace that checks for entity collisions. + *

+ * This may not consider entities in currently unloaded chunks. Some + * implementations may impose artificial restrictions on the maximum + * distance. + * + * @param start the start position + * @param direction the ray direction + * @param maxDistance the maximum distance + * @param filter only entities that fulfill this predicate are considered, + * or null to consider all entities + * @return the closest ray trace hit result, or null if there + * is no hit + * @see #rayTraceEntities(Location, Vector, double, double, Predicate) + */ + public RayTraceResult rayTraceEntities(Location start, Vector direction, double maxDistance, Predicate filter); + + /** + * Performs a ray trace that checks for entity collisions. + *

+ * This may not consider entities in currently unloaded chunks. Some + * implementations may impose artificial restrictions on the maximum + * distance. + * + * @param start the start position + * @param direction the ray direction + * @param maxDistance the maximum distance + * @param raySize entity bounding boxes will be uniformly expanded (or + * shrinked) by this value before doing collision checks + * @param filter only entities that fulfill this predicate are considered, + * or null to consider all entities + * @return the closest ray trace hit result, or null if there + * is no hit + */ + public RayTraceResult rayTraceEntities(Location start, Vector direction, double maxDistance, double raySize, Predicate filter); + + /** + * Performs a ray trace that checks for block collisions using the blocks' + * precise collision shapes. + *

+ * This takes collisions with passable blocks into account, but ignores + * fluids. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param start the start location + * @param direction the ray direction + * @param maxDistance the maximum distance + * @return the ray trace hit result, or null if there is no hit + * @see #rayTraceBlocks(Location, Vector, double, FluidCollisionMode, boolean) + */ + public RayTraceResult rayTraceBlocks(Location start, Vector direction, double maxDistance); + + /** + * Performs a ray trace that checks for block collisions using the blocks' + * precise collision shapes. + *

+ * This takes collisions with passable blocks into account. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param start the start location + * @param direction the ray direction + * @param maxDistance the maximum distance + * @param fluidCollisionMode the fluid collision mode + * @return the ray trace hit result, or null if there is no hit + * @see #rayTraceBlocks(Location, Vector, double, FluidCollisionMode, boolean) + */ + public RayTraceResult rayTraceBlocks(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode); + + /** + * Performs a ray trace that checks for block collisions using the blocks' + * precise collision shapes. + *

+ * If collisions with passable blocks are ignored, fluid collisions are + * ignored as well regardless of the fluid collision mode. + *

+ * Portal blocks are only considered passable if the ray starts within + * them. Apart from that collisions with portal blocks will be considered + * even if collisions with passable blocks are otherwise ignored. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param start the start location + * @param direction the ray direction + * @param maxDistance the maximum distance + * @param fluidCollisionMode the fluid collision mode + * @param ignorePassableBlocks whether to ignore passable but collidable + * blocks (ex. tall grass, signs, fluids, ..) + * @return the ray trace hit result, or null if there is no hit + */ + public RayTraceResult rayTraceBlocks(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode, boolean ignorePassableBlocks); + + /** + * Performs a ray trace that checks for both block and entity collisions. + *

+ * Block collisions use the blocks' precise collision shapes. The + * raySize parameter is only taken into account for entity + * collision checks. + *

+ * If collisions with passable blocks are ignored, fluid collisions are + * ignored as well regardless of the fluid collision mode. + *

+ * Portal blocks are only considered passable if the ray starts within them. + * Apart from that collisions with portal blocks will be considered even if + * collisions with passable blocks are otherwise ignored. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param start the start location + * @param direction the ray direction + * @param maxDistance the maximum distance + * @param fluidCollisionMode the fluid collision mode + * @param ignorePassableBlocks whether to ignore passable but collidable + * blocks (ex. tall grass, signs, fluids, ..) + * @param raySize entity bounding boxes will be uniformly expanded (or + * shrinked) by this value before doing collision checks + * @param filter only entities that fulfill this predicate are considered, + * or null to consider all entities + * @return the closest ray trace hit result with either a block or an + * entity, or null if there is no hit + */ + public RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode, boolean ignorePassableBlocks, double raySize, Predicate filter); + /** * Gets the unique name of this world * diff --git a/paper-api/src/main/java/org/bukkit/block/Block.java b/paper-api/src/main/java/org/bukkit/block/Block.java index f3fe0b47b2..24100a6a0b 100644 --- a/paper-api/src/main/java/org/bukkit/block/Block.java +++ b/paper-api/src/main/java/org/bukkit/block/Block.java @@ -3,12 +3,15 @@ package org.bukkit.block; import java.util.Collection; import org.bukkit.Chunk; +import org.bukkit.FluidCollisionMode; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.Location; import org.bukkit.block.data.BlockData; import org.bukkit.inventory.ItemStack; import org.bukkit.metadata.Metadatable; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Vector; /** * Represents a block. This is a live object, and only one Block may exist for @@ -369,4 +372,16 @@ public interface Block extends Metadatable { * @return true if passable */ boolean isPassable(); + + /** + * Performs a ray trace that checks for collision with this specific block + * in its current state using its precise collision shape. + * + * @param start the start location + * @param direction the ray direction + * @param maxDistance the maximum distance + * @param fluidCollisionMode the fluid collision mode + * @return the ray trace hit result, or null if there is no hit + */ + RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode); } diff --git a/paper-api/src/main/java/org/bukkit/block/BlockFace.java b/paper-api/src/main/java/org/bukkit/block/BlockFace.java index 58fb195d01..959ee3a657 100644 --- a/paper-api/src/main/java/org/bukkit/block/BlockFace.java +++ b/paper-api/src/main/java/org/bukkit/block/BlockFace.java @@ -1,5 +1,7 @@ package org.bukkit.block; +import org.bukkit.util.Vector; + /** * Represents the face of a block */ @@ -67,6 +69,19 @@ public enum BlockFace { return modZ; } + /** + * Gets the normal vector corresponding to this block face. + * + * @return the normal vector + */ + public Vector getDirection() { + Vector direction = new Vector(modX, modY, modZ); + if (modX != 0 || modY != 0 || modZ != 0) { + direction.normalize(); + } + return direction; + } + public BlockFace getOppositeFace() { switch (this) { case NORTH: diff --git a/paper-api/src/main/java/org/bukkit/configuration/serialization/ConfigurationSerialization.java b/paper-api/src/main/java/org/bukkit/configuration/serialization/ConfigurationSerialization.java index f5441bfde3..1803d57ac6 100644 --- a/paper-api/src/main/java/org/bukkit/configuration/serialization/ConfigurationSerialization.java +++ b/paper-api/src/main/java/org/bukkit/configuration/serialization/ConfigurationSerialization.java @@ -18,6 +18,7 @@ import org.bukkit.block.banner.Pattern; import org.bukkit.configuration.Configuration; import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; +import org.bukkit.util.BoundingBox; import org.bukkit.util.BlockVector; import org.bukkit.util.Vector; @@ -39,6 +40,7 @@ public class ConfigurationSerialization { registerClass(Pattern.class); registerClass(Location.class); registerClass(AttributeModifier.class); + registerClass(BoundingBox.class); } protected ConfigurationSerialization(Class clazz) { diff --git a/paper-api/src/main/java/org/bukkit/entity/Entity.java b/paper-api/src/main/java/org/bukkit/entity/Entity.java index c798998cc2..1a7ec6da88 100644 --- a/paper-api/src/main/java/org/bukkit/entity/Entity.java +++ b/paper-api/src/main/java/org/bukkit/entity/Entity.java @@ -9,6 +9,7 @@ import org.bukkit.block.BlockFace; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.material.Directional; import org.bukkit.metadata.Metadatable; +import org.bukkit.util.BoundingBox; import org.bukkit.util.Vector; import java.util.List; @@ -69,6 +70,16 @@ public interface Entity extends Metadatable, CommandSender, Nameable { */ public double getWidth(); + /** + * Gets the entity's current bounding box. + *

+ * The returned bounding box reflects the entity's current location and + * size. + * + * @return the entity's current bounding box + */ + public BoundingBox getBoundingBox(); + /** * Returns true if the entity is supported by a block. This value is a * state updated by the server and is not recalculated unless the entity diff --git a/paper-api/src/main/java/org/bukkit/entity/LivingEntity.java b/paper-api/src/main/java/org/bukkit/entity/LivingEntity.java index 6a5de671dd..ed1d5064d3 100644 --- a/paper-api/src/main/java/org/bukkit/entity/LivingEntity.java +++ b/paper-api/src/main/java/org/bukkit/entity/LivingEntity.java @@ -5,14 +5,18 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import org.bukkit.FluidCollisionMode; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.attribute.Attributable; import org.bukkit.block.Block; import org.bukkit.inventory.EntityEquipment; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.projectiles.ProjectileSource; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Vector; /** * Represents a living entity, such as a monster or player @@ -46,7 +50,7 @@ public interface LivingEntity extends Attributable, Entity, Damageable, Projecti * Gets all blocks along the living entity's line of sight. *

* This list contains all blocks from the living entity's eye position to - * target inclusive. + * target inclusive. This method considers all blocks as 1x1x1 in size. * * @param transparent HashSet containing all transparent block Materials (set to * null for only air) @@ -59,6 +63,10 @@ public interface LivingEntity extends Attributable, Entity, Damageable, Projecti /** * Gets the block that the living entity has targeted. + *

+ * This method considers all blocks as 1x1x1 in size. To take exact block + * collision shapes into account, see {@link #getTargetBlockExact(int, + * FluidCollisionMode)}. * * @param transparent HashSet containing all transparent block Materials (set to * null for only air) @@ -71,7 +79,8 @@ public interface LivingEntity extends Attributable, Entity, Damageable, Projecti /** * Gets the last two blocks along the living entity's line of sight. *

- * The target block will be the last block in the list. + * The target block will be the last block in the list. This method + * considers all blocks as 1x1x1 in size. * * @param transparent HashSet containing all transparent block Materials (set to * null for only air) @@ -82,6 +91,68 @@ public interface LivingEntity extends Attributable, Entity, Damageable, Projecti */ public List getLastTwoTargetBlocks(Set transparent, int maxDistance); + /** + * Gets the block that the living entity has targeted. + *

+ * This takes the blocks' precise collision shapes into account. Fluids are + * ignored. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param maxDistance the maximum distance to scan + * @return block that the living entity has targeted + * @see #getTargetBlockExact(int, org.bukkit.FluidCollisionMode) + */ + public Block getTargetBlockExact(int maxDistance); + + /** + * Gets the block that the living entity has targeted. + *

+ * This takes the blocks' precise collision shapes into account. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param maxDistance the maximum distance to scan + * @param fluidCollisionMode the fluid collision mode + * @return block that the living entity has targeted + * @see #rayTraceBlocks(double, FluidCollisionMode) + */ + public Block getTargetBlockExact(int maxDistance, FluidCollisionMode fluidCollisionMode); + + /** + * Performs a ray trace that provides information on the targeted block. + *

+ * This takes the blocks' precise collision shapes into account. Fluids are + * ignored. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param maxDistance the maximum distance to scan + * @return information on the targeted block, or null if there + * is no targeted block in range + * @see #rayTraceBlocks(double, FluidCollisionMode) + */ + public RayTraceResult rayTraceBlocks(double maxDistance); + + /** + * Performs a ray trace that provides information on the targeted block. + *

+ * This takes the blocks' precise collision shapes into account. + *

+ * This may cause loading of chunks! Some implementations may impose + * artificial restrictions on the maximum distance. + * + * @param maxDistance the maximum distance to scan + * @param fluidCollisionMode the fluid collision mode + * @return information on the targeted block, or null if there + * is no targeted block in range + * @see World#rayTraceBlocks(Location, Vector, double, FluidCollisionMode) + */ + public RayTraceResult rayTraceBlocks(double maxDistance, FluidCollisionMode fluidCollisionMode); + /** * Returns the amount of air that the living entity has remaining, in * ticks. diff --git a/paper-api/src/main/java/org/bukkit/util/BlockIterator.java b/paper-api/src/main/java/org/bukkit/util/BlockIterator.java index 5c85778cca..ec672e7f4f 100644 --- a/paper-api/src/main/java/org/bukkit/util/BlockIterator.java +++ b/paper-api/src/main/java/org/bukkit/util/BlockIterator.java @@ -39,7 +39,9 @@ public class BlockIterator implements Iterator { private BlockFace thirdFace; /** - * Constructs the BlockIterator + * Constructs the BlockIterator. + *

+ * This considers all blocks as 1x1x1 in size. * * @param world The world to use for tracing * @param start A Vector giving the initial location for the trace @@ -220,7 +222,9 @@ public class BlockIterator implements Iterator { } /** - * Constructs the BlockIterator + * Constructs the BlockIterator. + *

+ * This considers all blocks as 1x1x1 in size. * * @param loc The location for the start of the ray trace * @param yOffset The trace begins vertically offset from the start vector @@ -235,6 +239,8 @@ public class BlockIterator implements Iterator { /** * Constructs the BlockIterator. + *

+ * This considers all blocks as 1x1x1 in size. * * @param loc The location for the start of the ray trace * @param yOffset The trace begins vertically offset from the start vector @@ -247,6 +253,8 @@ public class BlockIterator implements Iterator { /** * Constructs the BlockIterator. + *

+ * This considers all blocks as 1x1x1 in size. * * @param loc The location for the start of the ray trace */ @@ -257,6 +265,8 @@ public class BlockIterator implements Iterator { /** * Constructs the BlockIterator. + *

+ * This considers all blocks as 1x1x1 in size. * * @param entity Information from the entity is used to set up the trace * @param maxDistance This is the maximum distance in blocks for the @@ -270,6 +280,8 @@ public class BlockIterator implements Iterator { /** * Constructs the BlockIterator. + *

+ * This considers all blocks as 1x1x1 in size. * * @param entity Information from the entity is used to set up the trace */ diff --git a/paper-api/src/main/java/org/bukkit/util/BoundingBox.java b/paper-api/src/main/java/org/bukkit/util/BoundingBox.java new file mode 100644 index 0000000000..67a3322f5d --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/util/BoundingBox.java @@ -0,0 +1,1030 @@ +package org.bukkit.util; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.commons.lang.Validate; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.SerializableAs; + +/** + * A mutable axis aligned bounding box (AABB). + *

+ * This basically represents a rectangular box (specified by minimum and maximum + * corners) that can for example be used to describe the position and extents of + * an object (such as an entity, block, or rectangular region) in 3D space. Its + * edges and faces are parallel to the axes of the cartesian coordinate system. + *

+ * The bounding box may be degenerate (one or more sides having the length 0). + *

+ * Because bounding boxes are mutable, storing them long term may be dangerous + * if they get modified later. If you want to keep around a bounding box, it may + * be wise to call {@link #clone()} in order to get a copy. + */ +@SerializableAs("BoundingBox") +public class BoundingBox implements Cloneable, ConfigurationSerializable { + + /** + * Creates a new bounding box using the coordinates of the given vectors as + * corners. + * + * @param corner1 the first corner + * @param corner2 the second corner + * @return the bounding box + */ + public static BoundingBox of(Vector corner1, Vector corner2) { + Validate.notNull(corner1, "Corner1 is null!"); + Validate.notNull(corner2, "Corner2 is null!"); + return new BoundingBox(corner1.getX(), corner1.getY(), corner1.getZ(), corner2.getX(), corner2.getY(), corner2.getZ()); + } + + /** + * Creates a new bounding box using the coordinates of the given locations + * as corners. + * + * @param corner1 the first corner + * @param corner2 the second corner + * @return the bounding box + */ + public static BoundingBox of(Location corner1, Location corner2) { + Validate.notNull(corner1, "Corner1 is null!"); + Validate.notNull(corner2, "Corner2 is null!"); + Validate.isTrue(Objects.equals(corner1.getWorld(), corner2.getWorld()), "Locations from different worlds!"); + return new BoundingBox(corner1.getX(), corner1.getY(), corner1.getZ(), corner2.getX(), corner2.getY(), corner2.getZ()); + } + + /** + * Creates a new bounding box using the coordinates of the given blocks as + * corners. + *

+ * The bounding box will be sized to fully contain both blocks. + * + * @param corner1 the first corner block + * @param corner2 the second corner block + * @return the bounding box + */ + public static BoundingBox of(Block corner1, Block corner2) { + Validate.notNull(corner1, "Corner1 is null!"); + Validate.notNull(corner2, "Corner2 is null!"); + Validate.isTrue(Objects.equals(corner1.getWorld(), corner2.getWorld()), "Blocks from different worlds!"); + + int x1 = corner1.getX(); + int y1 = corner1.getY(); + int z1 = corner1.getZ(); + int x2 = corner2.getX(); + int y2 = corner2.getY(); + int z2 = corner2.getZ(); + + int minX = Math.min(x1, x2); + int minY = Math.min(y1, y2); + int minZ = Math.min(z1, z2); + int maxX = Math.max(x1, x2) + 1; + int maxY = Math.max(y1, y2) + 1; + int maxZ = Math.max(z1, z2) + 1; + + return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + } + + /** + * Creates a new 1x1x1 sized bounding box containing the given block. + * + * @param block the block + * @return the bounding box + */ + public static BoundingBox of(Block block) { + Validate.notNull(block, "Block is null!"); + return new BoundingBox(block.getX(), block.getY(), block.getZ(), block.getX() + 1, block.getY() + 1, block.getZ() + 1); + } + + /** + * Creates a new bounding box using the given center and extents. + * + * @param center the center + * @param x 1/2 the size of the bounding box along the x axis + * @param y 1/2 the size of the bounding box along the y axis + * @param z 1/2 the size of the bounding box along the z axis + * @return the bounding box + */ + public static BoundingBox of(Vector center, double x, double y, double z) { + Validate.notNull(center, "Center is null!"); + return new BoundingBox(center.getX() - x, center.getY() - y, center.getZ() - z, center.getX() + x, center.getY() + y, center.getZ() + z); + } + + /** + * Creates a new bounding box using the given center and extents. + * + * @param center the center + * @param x 1/2 the size of the bounding box along the x axis + * @param y 1/2 the size of the bounding box along the y axis + * @param z 1/2 the size of the bounding box along the z axis + * @return the bounding box + */ + public static BoundingBox of(Location center, double x, double y, double z) { + Validate.notNull(center, "Center is null!"); + return new BoundingBox(center.getX() - x, center.getY() - y, center.getZ() - z, center.getX() + x, center.getY() + y, center.getZ() + z); + } + + private double minX; + private double minY; + private double minZ; + private double maxX; + private double maxY; + private double maxZ; + + /** + * Creates a new (degenerate) bounding box with all corner coordinates at + * 0. + */ + public BoundingBox() { + this.resize(0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D); + } + + /** + * Creates a new bounding box from the given corner coordinates. + * + * @param x1 the first corner's x value + * @param y1 the first corner's y value + * @param z1 the first corner's z value + * @param x2 the second corner's x value + * @param y2 the second corner's y value + * @param z2 the second corner's z value + */ + public BoundingBox(double x1, double y1, double z1, double x2, double y2, double z2) { + this.resize(x1, y1, z1, x2, y2, z2); + } + + /** + * Resizes this bounding box. + * + * @param x1 the first corner's x value + * @param y1 the first corner's y value + * @param z1 the first corner's z value + * @param x2 the second corner's x value + * @param y2 the second corner's y value + * @param z2 the second corner's z value + * @return this bounding box (resized) + */ + public BoundingBox resize(double x1, double y1, double z1, double x2, double y2, double z2) { + NumberConversions.checkFinite(x1, "x1 not finite"); + NumberConversions.checkFinite(y1, "y1 not finite"); + NumberConversions.checkFinite(z1, "z1 not finite"); + NumberConversions.checkFinite(x2, "x2 not finite"); + NumberConversions.checkFinite(y2, "y2 not finite"); + NumberConversions.checkFinite(z2, "z2 not finite"); + + this.minX = Math.min(x1, x2); + this.minY = Math.min(y1, y2); + this.minZ = Math.min(z1, z2); + this.maxX = Math.max(x1, x2); + this.maxY = Math.max(y1, y2); + this.maxZ = Math.max(z1, z2); + return this; + } + + /** + * Gets the minimum x value. + * + * @return the minimum x value + */ + public double getMinX() { + return minX; + } + + /** + * Gets the minimum y value. + * + * @return the minimum y value + */ + public double getMinY() { + return minY; + } + + /** + * Gets the minimum z value. + * + * @return the minimum z value + */ + public double getMinZ() { + return minZ; + } + + /** + * Gets the minimum corner as vector. + * + * @return the minimum corner as vector + */ + public Vector getMin() { + return new Vector(minX, minY, minZ); + } + + /** + * Gets the maximum x value. + * + * @return the maximum x value + */ + public double getMaxX() { + return maxX; + } + + /** + * Gets the maximum y value. + * + * @return the maximum y value + */ + public double getMaxY() { + return maxY; + } + + /** + * Gets the maximum z value. + * + * @return the maximum z value + */ + public double getMaxZ() { + return maxZ; + } + + /** + * Gets the maximum corner as vector. + * + * @return the maximum corner vector + */ + public Vector getMax() { + return new Vector(maxX, maxY, maxZ); + } + + /** + * Gets the width of the bounding box in the x direction. + * + * @return the width in the x direction + */ + public double getWidthX() { + return (this.maxX - this.minX); + } + + /** + * Gets the width of the bounding box in the z direction. + * + * @return the width in the z direction + */ + public double getWidthZ() { + return (this.maxZ - this.minZ); + } + + /** + * Gets the height of the bounding box. + * + * @return the height + */ + public double getHeight() { + return (this.maxY - this.minY); + } + + /** + * Gets the volume of the bounding box. + * + * @return the volume + */ + public double getVolume() { + return (this.getHeight() * this.getWidthX() * this.getWidthZ()); + } + + /** + * Gets the x coordinate of the center of the bounding box. + * + * @return the center's x coordinate + */ + public double getCenterX() { + return (this.minX + this.getWidthX() * 0.5D); + } + + /** + * Gets the y coordinate of the center of the bounding box. + * + * @return the center's y coordinate + */ + public double getCenterY() { + return (this.minY + this.getHeight() * 0.5D); + } + + /** + * Gets the z coordinate of the center of the bounding box. + * + * @return the center's z coordinate + */ + public double getCenterZ() { + return (this.minZ + this.getWidthZ() * 0.5D); + } + + /** + * Gets the center of the bounding box. + * + * @return the center + */ + public Vector getCenter() { + return new Vector(this.getCenterX(), this.getCenterY(), this.getCenterZ()); + } + + /** + * Copies another bounding box. + * + * @param other the other bounding box + * @return this bounding box + */ + public BoundingBox copy(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + return this.resize(other.getMinX(), other.getMinY(), other.getMinZ(), other.getMaxX(), other.getMaxY(), other.getMaxZ()); + } + + /** + * Expands this bounding box by the given values in the corresponding + * directions. + *

+ * Negative values will shrink the bounding box in the corresponding + * direction. Shrinking will be limited to the point where the affected + * opposite faces would meet if the they shrank at uniform speeds. + * + * @param negativeX the amount of expansion in the negative x direction + * @param negativeY the amount of expansion in the negative y direction + * @param negativeZ the amount of expansion in the negative z direction + * @param positiveX the amount of expansion in the positive x direction + * @param positiveY the amount of expansion in the positive y direction + * @param positiveZ the amount of expansion in the positive z direction + * @return this bounding box (now expanded) + */ + public BoundingBox expand(double negativeX, double negativeY, double negativeZ, double positiveX, double positiveY, double positiveZ) { + if (negativeX == 0.0D && negativeY == 0.0D && negativeZ == 0.0D && positiveX == 0.0D && positiveY == 0.0D && positiveZ == 0.0D) { + return this; + } + double newMinX = this.minX - negativeX; + double newMinY = this.minY - negativeY; + double newMinZ = this.minZ - negativeZ; + double newMaxX = this.maxX + positiveX; + double newMaxY = this.maxY + positiveY; + double newMaxZ = this.maxZ + positiveZ; + + // limit shrinking: + if (newMinX > newMaxX) { + double centerX = this.getCenterX(); + if (newMaxX >= centerX) { + newMinX = newMaxX; + } else if (newMinX <= centerX) { + newMaxX = newMinX; + } else { + newMinX = centerX; + newMaxX = centerX; + } + } + if (newMinY > newMaxY) { + double centerY = this.getCenterY(); + if (newMaxY >= centerY) { + newMinY = newMaxY; + } else if (newMinY <= centerY) { + newMaxY = newMinY; + } else { + newMinY = centerY; + newMaxY = centerY; + } + } + if (newMinZ > newMaxZ) { + double centerZ = this.getCenterZ(); + if (newMaxZ >= centerZ) { + newMinZ = newMaxZ; + } else if (newMinZ <= centerZ) { + newMaxZ = newMinZ; + } else { + newMinZ = centerZ; + newMaxZ = centerZ; + } + } + return this.resize(newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ); + } + + /** + * Expands this bounding box uniformly by the given values in both positive + * and negative directions. + *

+ * Negative values will shrink the bounding box. Shrinking will be limited + * to the bounding box's current size. + * + * @param x the amount of expansion in both positive and negative x + * direction + * @param y the amount of expansion in both positive and negative y + * direction + * @param z the amount of expansion in both positive and negative z + * direction + * @return this bounding box (now expanded) + */ + public BoundingBox expand(double x, double y, double z) { + return this.expand(x, y, z, x, y, z); + } + + /** + * Expands this bounding box uniformly by the given values in both positive + * and negative directions. + *

+ * Negative values will shrink the bounding box. Shrinking will be limited + * to the bounding box's current size. + * + * @param expansion the expansion values + * @return this bounding box (now expanded) + */ + public BoundingBox expand(Vector expansion) { + Validate.notNull(expansion, "Expansion is null!"); + double x = expansion.getX(); + double y = expansion.getY(); + double z = expansion.getZ(); + return this.expand(x, y, z, x, y, z); + } + + /** + * Expands this bounding box uniformly by the given value in all directions. + *

+ * A negative value will shrink the bounding box. Shrinking will be limited + * to the bounding box's current size. + * + * @param expansion the amount of expansion + * @return this bounding box (now expanded) + */ + public BoundingBox expand(double expansion) { + return this.expand(expansion, expansion, expansion, expansion, expansion, expansion); + } + + /** + * Expands this bounding box in the specified direction. + *

+ * The magnitude of the direction will scale the expansion. A negative + * expansion value will shrink the bounding box in this direction. Shrinking + * will be limited to the bounding box's current size. + * + * @param dirX the x direction component + * @param dirY the y direction component + * @param dirZ the z direction component + * @param expansion the amount of expansion + * @return this bounding box (now expanded) + */ + public BoundingBox expand(double dirX, double dirY, double dirZ, double expansion) { + if (expansion == 0.0D) return this; + if (dirX == 0.0D && dirY == 0.0D && dirZ == 0.0D) return this; + + double negativeX = (dirX < 0.0D ? (-dirX * expansion) : 0.0D); + double negativeY = (dirY < 0.0D ? (-dirY * expansion) : 0.0D); + double negativeZ = (dirZ < 0.0D ? (-dirZ * expansion) : 0.0D); + double positiveX = (dirX > 0.0D ? (dirX * expansion) : 0.0D); + double positiveY = (dirY > 0.0D ? (dirY * expansion) : 0.0D); + double positiveZ = (dirZ > 0.0D ? (dirZ * expansion) : 0.0D); + return this.expand(negativeX, negativeY, negativeZ, positiveX, positiveY, positiveZ); + } + + /** + * Expands this bounding box in the specified direction. + *

+ * The magnitude of the direction will scale the expansion. A negative + * expansion value will shrink the bounding box in this direction. Shrinking + * will be limited to the bounding box's current size. + * + * @param direction the direction + * @param expansion the amount of expansion + * @return this bounding box (now expanded) + */ + public BoundingBox expand(Vector direction, double expansion) { + Validate.notNull(direction, "Direction is null!"); + return this.expand(direction.getX(), direction.getY(), direction.getZ(), expansion); + } + + /** + * Expands this bounding box in the direction specified by the given block + * face. + *

+ * A negative expansion value will shrink the bounding box in this + * direction. Shrinking will be limited to the bounding box's current size. + * + * @param blockFace the block face + * @param expansion the amount of expansion + * @return this bounding box (now expanded) + */ + public BoundingBox expand(BlockFace blockFace, double expansion) { + Validate.notNull(blockFace, "Block face is null!"); + if (blockFace == BlockFace.SELF) return this; + + return this.expand(blockFace.getDirection(), expansion); + } + + /** + * Expands this bounding box in the specified direction. + *

+ * Negative values will expand the bounding box in the negative direction, + * positive values will expand it in the positive direction. The magnitudes + * of the direction components determine the corresponding amounts of + * expansion. + * + * @param dirX the x direction component + * @param dirY the y direction component + * @param dirZ the z direction component + * @return this bounding box (now expanded) + */ + public BoundingBox expandDirectional(double dirX, double dirY, double dirZ) { + return this.expand(dirX, dirY, dirZ, 1.0D); + } + + /** + * Expands this bounding box in the specified direction. + *

+ * Negative values will expand the bounding box in the negative direction, + * positive values will expand it in the positive direction. The magnitude + * of the direction vector determines the amount of expansion. + * + * @param direction the direction and magnitude of the expansion + * @return this bounding box (now expanded) + */ + public BoundingBox expandDirectional(Vector direction) { + Validate.notNull(direction, "Expansion is null!"); + return this.expand(direction.getX(), direction.getY(), direction.getZ(), 1.0D); + } + + /** + * Expands this bounding box to contain (or border) the specified position. + * + * @param posX the x position value + * @param posY the y position value + * @param posZ the z position value + * @return this bounding box (now expanded) + * @see #contains(double, double, double) + */ + public BoundingBox union(double posX, double posY, double posZ) { + double newMinX = Math.min(this.minX, posX); + double newMinY = Math.min(this.minY, posY); + double newMinZ = Math.min(this.minZ, posZ); + double newMaxX = Math.max(this.maxX, posX); + double newMaxY = Math.max(this.maxY, posY); + double newMaxZ = Math.max(this.maxZ, posZ); + if (newMinX == this.minX && newMinY == this.minY && newMinZ == this.minZ && newMaxX == this.maxX && newMaxY == this.maxY && newMaxZ == this.maxZ) { + return this; + } + return this.resize(newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ); + } + + /** + * Expands this bounding box to contain (or border) the specified position. + * + * @param position the position + * @return this bounding box (now expanded) + * @see #contains(double, double, double) + */ + public BoundingBox union(Vector position) { + Validate.notNull(position, "Position is null!"); + return this.union(position.getX(), position.getY(), position.getZ()); + } + + /** + * Expands this bounding box to contain (or border) the specified position. + * + * @param position the position + * @return this bounding box (now expanded) + * @see #contains(double, double, double) + */ + public BoundingBox union(Location position) { + Validate.notNull(position, "Position is null!"); + return this.union(position.getX(), position.getY(), position.getZ()); + } + + /** + * Expands this bounding box to contain both this and the given bounding + * box. + * + * @param other the other bounding box + * @return this bounding box (now expanded) + */ + public BoundingBox union(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + if (this.contains(other)) return this; + double newMinX = Math.min(this.minX, other.minX); + double newMinY = Math.min(this.minY, other.minY); + double newMinZ = Math.min(this.minZ, other.minZ); + double newMaxX = Math.max(this.maxX, other.maxX); + double newMaxY = Math.max(this.maxY, other.maxY); + double newMaxZ = Math.max(this.maxZ, other.maxZ); + return this.resize(newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ); + } + + /** + * Resizes this bounding box to represent the intersection of this and the + * given bounding box. + * + * @param other the other bounding box + * @return this bounding box (now representing the intersection) + * @throws IllegalArgumentException if the bounding boxes don't overlap + */ + public BoundingBox intersection(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + Validate.isTrue(this.overlaps(other), "The bounding boxes do not overlap!"); + double newMinX = Math.max(this.minX, other.minX); + double newMinY = Math.max(this.minY, other.minY); + double newMinZ = Math.max(this.minZ, other.minZ); + double newMaxX = Math.min(this.maxX, other.maxX); + double newMaxY = Math.min(this.maxY, other.maxY); + double newMaxZ = Math.min(this.maxZ, other.maxZ); + return this.resize(newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ); + } + + /** + * Shifts this bounding box by the given amounts. + * + * @param shiftX the shift in x direction + * @param shiftY the shift in y direction + * @param shiftZ the shift in z direction + * @return this bounding box (now shifted) + */ + public BoundingBox shift(double shiftX, double shiftY, double shiftZ) { + if (shiftX == 0.0D && shiftY == 0.0D && shiftZ == 0.0D) return this; + return this.resize(this.minX + shiftX, this.minY + shiftY, this.minZ + shiftZ, + this.maxX + shiftX, this.maxY + shiftY, this.maxZ + shiftZ); + } + + /** + * Shifts this bounding box by the given amounts. + * + * @param shift the shift + * @return this bounding box (now shifted) + */ + public BoundingBox shift(Vector shift) { + Validate.notNull(shift, "Shift is null!"); + return this.shift(shift.getX(), shift.getY(), shift.getZ()); + } + + /** + * Shifts this bounding box by the given amounts. + * + * @param shift the shift + * @return this bounding box (now shifted) + */ + public BoundingBox shift(Location shift) { + Validate.notNull(shift, "Shift is null!"); + return this.shift(shift.getX(), shift.getY(), shift.getZ()); + } + + private boolean overlaps(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return this.minX < maxX && this.maxX > minX + && this.minY < maxY && this.maxY > minY + && this.minZ < maxZ && this.maxZ > minZ; + } + + /** + * Checks if this bounding box overlaps with the given bounding box. + *

+ * Bounding boxes that are only intersecting at the borders are not + * considered overlapping. + * + * @param other the other bounding box + * @return true if overlapping + */ + public boolean overlaps(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + return this.overlaps(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ); + } + + /** + * Checks if this bounding box overlaps with the bounding box that is + * defined by the given corners. + *

+ * Bounding boxes that are only intersecting at the borders are not + * considered overlapping. + * + * @param min the first corner + * @param max the second corner + * @return true if overlapping + */ + public boolean overlaps(Vector min, Vector max) { + Validate.notNull(min, "Min is null!"); + Validate.notNull(max, "Max is null!"); + double x1 = min.getX(); + double y1 = min.getY(); + double z1 = min.getZ(); + double x2 = max.getX(); + double y2 = max.getY(); + double z2 = max.getZ(); + return this.overlaps(Math.min(x1, x2), Math.min(y1, y2), Math.min(z1, z2), + Math.max(x1, x2), Math.max(y1, y2), Math.max(z1, z2)); + } + + /** + * Checks if this bounding box contains the specified position. + *

+ * Positions exactly on the minimum borders of the bounding box are + * considered to be inside the bounding box, while positions exactly on the + * maximum borders are considered to be outside. This allows bounding boxes + * to reside directly next to each other with positions always only residing + * in exactly one of them. + * + * @param x the position's x coordinates + * @param y the position's y coordinates + * @param z the position's z coordinates + * @return true if the bounding box contains the position + */ + public boolean contains(double x, double y, double z) { + return x >= this.minX && x < this.maxX + && y >= this.minY && y < this.maxY + && z >= this.minZ && z < this.maxZ; + } + + /** + * Checks if this bounding box contains the specified position. + *

+ * Positions exactly on the minimum borders of the bounding box are + * considered to be inside the bounding box, while positions exactly on the + * maximum borders are considered to be outside. This allows bounding boxes + * to reside directly next to each other with positions always only residing + * in exactly one of them. + * + * @param position the position + * @return true if the bounding box contains the position + */ + public boolean contains(Vector position) { + Validate.notNull(position, "Position is null!"); + return this.contains(position.getX(), position.getY(), position.getZ()); + } + + private boolean contains(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return this.minX <= minX && this.maxX >= maxX + && this.minY <= minY && this.maxY >= maxY + && this.minZ <= minZ && this.maxZ >= maxZ; + } + + /** + * Checks if this bounding box fully contains the given bounding box. + * + * @param other the other bounding box + * @return true if the bounding box contains the given bounding + * box + */ + public boolean contains(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + return this.contains(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ); + } + + /** + * Checks if this bounding box fully contains the bounding box that is + * defined by the given corners. + * + * @param min the first corner + * @param max the second corner + * @return true if the bounding box contains the specified + * bounding box + */ + public boolean contains(Vector min, Vector max) { + Validate.notNull(min, "Min is null!"); + Validate.notNull(max, "Max is null!"); + double x1 = min.getX(); + double y1 = min.getY(); + double z1 = min.getZ(); + double x2 = max.getX(); + double y2 = max.getY(); + double z2 = max.getZ(); + return this.contains(Math.min(x1, x2), Math.min(y1, y2), Math.min(z1, z2), + Math.max(x1, x2), Math.max(y1, y2), Math.max(z1, z2)); + } + + /** + * Calculates the intersection of this bounding box with the specified line + * segment. + *

+ * Intersections at edges and corners yield one of the affected block faces + * as hit result, but it is not defined which of them. + * + * @param start the start position + * @param direction the ray direction + * @param maxDistance the maximum distance + * @return the ray trace hit result, or null if there is no hit + */ + public RayTraceResult rayTrace(Vector start, Vector direction, double maxDistance) { + Validate.notNull(start, "Start is null!"); + start.checkFinite(); + Validate.notNull(direction, "Direction is null!"); + direction.checkFinite(); + Validate.isTrue(direction.lengthSquared() > 0, "Direction's magnitude is 0!"); + if (maxDistance < 0.0D) return null; + + // ray start: + double startX = start.getX(); + double startY = start.getY(); + double startZ = start.getZ(); + + // ray direction: + Vector dir = direction.clone().normalize(); + double dirX = dir.getX(); + double dirY = dir.getY(); + double dirZ = dir.getZ(); + + // saving a few divisions below: + double divX = 1.0D / dirX; + double divY = 1.0D / dirY; + double divZ = 1.0D / dirZ; + + double tMin; + double tMax; + BlockFace hitBlockFaceMin; + BlockFace hitBlockFaceMax; + + // intersections with x planes: + if (dirX >= 0.0D) { + tMin = (this.minX - startX) * divX; + tMax = (this.maxX - startX) * divX; + hitBlockFaceMin = BlockFace.WEST; + hitBlockFaceMax = BlockFace.EAST; + } else { + tMin = (this.maxX - startX) * divX; + tMax = (this.minX - startX) * divX; + hitBlockFaceMin = BlockFace.EAST; + hitBlockFaceMax = BlockFace.WEST; + } + + // intersections with y planes: + double tyMin; + double tyMax; + BlockFace hitBlockFaceYMin; + BlockFace hitBlockFaceYMax; + if (dirY >= 0.0D) { + tyMin = (this.minY - startY) * divY; + tyMax = (this.maxY - startY) * divY; + hitBlockFaceYMin = BlockFace.DOWN; + hitBlockFaceYMax = BlockFace.UP; + } else { + tyMin = (this.maxY - startY) * divY; + tyMax = (this.minY - startY) * divY; + hitBlockFaceYMin = BlockFace.UP; + hitBlockFaceYMax = BlockFace.DOWN; + } + if ((tMin > tyMax) || (tMax < tyMin)) { + return null; + } + if (tyMin > tMin) { + tMin = tyMin; + hitBlockFaceMin = hitBlockFaceYMin; + } + if (tyMax < tMax) { + tMax = tyMax; + hitBlockFaceMax = hitBlockFaceYMax; + } + + // intersections with z planes: + double tzMin; + double tzMax; + BlockFace hitBlockFaceZMin; + BlockFace hitBlockFaceZMax; + if (dirZ >= 0.0D) { + tzMin = (this.minZ - startZ) * divZ; + tzMax = (this.maxZ - startZ) * divZ; + hitBlockFaceZMin = BlockFace.NORTH; + hitBlockFaceZMax = BlockFace.SOUTH; + } else { + tzMin = (this.maxZ - startZ) * divZ; + tzMax = (this.minZ - startZ) * divZ; + hitBlockFaceZMin = BlockFace.SOUTH; + hitBlockFaceZMax = BlockFace.NORTH; + } + if ((tMin > tzMax) || (tMax < tzMin)) { + return null; + } + if (tzMin > tMin) { + tMin = tzMin; + hitBlockFaceMin = hitBlockFaceZMin; + } + if (tzMax < tMax) { + tMax = tzMax; + hitBlockFaceMax = hitBlockFaceZMax; + } + + // intersections are behind the start: + if (tMax < 0.0D) return null; + // intersections are to far away: + if (tMin > maxDistance) { + return null; + } + + // find the closest intersection: + double t; + BlockFace hitBlockFace; + if (tMin < 0.0D) { + t = tMax; + hitBlockFace = hitBlockFaceMax; + } else { + t = tMin; + hitBlockFace = hitBlockFaceMin; + } + // reusing the newly created direction vector for the hit position: + Vector hitPosition = dir.multiply(t).add(start); + return new RayTraceResult(hitPosition, hitBlockFace); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(maxX); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(maxY); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(maxZ); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(minX); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(minY); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(minZ); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof BoundingBox)) return false; + BoundingBox other = (BoundingBox) obj; + if (Double.doubleToLongBits(maxX) != Double.doubleToLongBits(other.maxX)) return false; + if (Double.doubleToLongBits(maxY) != Double.doubleToLongBits(other.maxY)) return false; + if (Double.doubleToLongBits(maxZ) != Double.doubleToLongBits(other.maxZ)) return false; + if (Double.doubleToLongBits(minX) != Double.doubleToLongBits(other.minX)) return false; + if (Double.doubleToLongBits(minY) != Double.doubleToLongBits(other.minY)) return false; + if (Double.doubleToLongBits(minZ) != Double.doubleToLongBits(other.minZ)) return false; + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("BoundingBox [minX="); + builder.append(minX); + builder.append(", minY="); + builder.append(minY); + builder.append(", minZ="); + builder.append(minZ); + builder.append(", maxX="); + builder.append(maxX); + builder.append(", maxY="); + builder.append(maxY); + builder.append(", maxZ="); + builder.append(maxZ); + builder.append("]"); + return builder.toString(); + } + + /** + * Creates a copy of this bounding box. + * + * @return the cloned bounding box + */ + @Override + public BoundingBox clone() { + try { + return (BoundingBox) super.clone(); + } catch (CloneNotSupportedException e) { + throw new Error(e); + } + } + + @Override + public Map serialize() { + Map result = new LinkedHashMap(); + result.put("minX", minX); + result.put("minY", minY); + result.put("minZ", minZ); + result.put("maxX", maxX); + result.put("maxY", maxY); + result.put("maxZ", maxZ); + return result; + } + + public static BoundingBox deserialize(Map args) { + double minX = 0.0D; + double minY = 0.0D; + double minZ = 0.0D; + double maxX = 0.0D; + double maxY = 0.0D; + double maxZ = 0.0D; + + if (args.containsKey("minX")) { + minX = ((Number) args.get("minX")).doubleValue(); + } + if (args.containsKey("minY")) { + minY = ((Number) args.get("minY")).doubleValue(); + } + if (args.containsKey("minZ")) { + minZ = ((Number) args.get("minZ")).doubleValue(); + } + if (args.containsKey("maxX")) { + maxX = ((Number) args.get("maxX")).doubleValue(); + } + if (args.containsKey("maxY")) { + maxY = ((Number) args.get("maxY")).doubleValue(); + } + if (args.containsKey("maxZ")) { + maxZ = ((Number) args.get("maxZ")).doubleValue(); + } + + return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + } +} diff --git a/paper-api/src/main/java/org/bukkit/util/RayTraceResult.java b/paper-api/src/main/java/org/bukkit/util/RayTraceResult.java new file mode 100644 index 0000000000..5a3a05efcb --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/util/RayTraceResult.java @@ -0,0 +1,157 @@ +package org.bukkit.util; + +import java.util.Objects; + +import org.apache.commons.lang.Validate; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; + +/** + * The hit result of a ray trace. + *

+ * Only the hit position is guaranteed to always be available. The availability + * of the other attributes depends on what got hit and on the context in which + * the ray trace was performed. + */ +public class RayTraceResult { + + private final Vector hitPosition; + + private final Block hitBlock; + private final BlockFace hitBlockFace; + private final Entity hitEntity; + + private RayTraceResult(Vector hitPosition, Block hitBlock, BlockFace hitBlockFace, Entity hitEntity) { + Validate.notNull(hitPosition, "Hit position is null!"); + this.hitPosition = hitPosition.clone(); + this.hitBlock = hitBlock; + this.hitBlockFace = hitBlockFace; + this.hitEntity = hitEntity; + } + + /** + * Creates a RayTraceResult. + * + * @param hitPosition the hit position + */ + public RayTraceResult(Vector hitPosition) { + this(hitPosition, null, null, null); + } + + /** + * Creates a RayTraceResult. + * + * @param hitPosition the hit position + * @param hitBlockFace the hit block face + */ + public RayTraceResult(Vector hitPosition, BlockFace hitBlockFace) { + this(hitPosition, null, hitBlockFace, null); + } + + /** + * Creates a RayTraceResult. + * + * @param hitPosition the hit position + * @param hitBlock the hit block + * @param hitBlockFace the hit block face + */ + public RayTraceResult(Vector hitPosition, Block hitBlock, BlockFace hitBlockFace) { + this(hitPosition, hitBlock, hitBlockFace, null); + } + + /** + * Creates a RayTraceResult. + * + * @param hitPosition the hit position + * @param hitEntity the hit entity + */ + public RayTraceResult(Vector hitPosition, Entity hitEntity) { + this(hitPosition, null, null, hitEntity); + } + + /** + * Creates a RayTraceResult. + * + * @param hitPosition the hit position + * @param hitEntity the hit entity + * @param hitBlockFace the hit block face + */ + public RayTraceResult(Vector hitPosition, Entity hitEntity, BlockFace hitBlockFace) { + this(hitPosition, null, hitBlockFace, hitEntity); + } + + /** + * Gets the exact position of the hit. + * + * @return a copy of the exact hit position + */ + public Vector getHitPosition() { + return hitPosition.clone(); + } + + /** + * Gets the hit block. + * + * @return the hit block, or null if not available + */ + public Block getHitBlock() { + return hitBlock; + } + + /** + * Gets the hit block face. + * + * @return the hit block face, or null if not available + */ + public BlockFace getHitBlockFace() { + return hitBlockFace; + } + + /** + * Gets the hit entity. + * + * @return the hit entity, or null if not available + */ + public Entity getHitEntity() { + return hitEntity; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + hitPosition.hashCode(); + result = prime * result + ((hitBlock == null) ? 0 : hitBlock.hashCode()); + result = prime * result + ((hitBlockFace == null) ? 0 : hitBlockFace.hashCode()); + result = prime * result + ((hitEntity == null) ? 0 : hitEntity.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof RayTraceResult)) return false; + RayTraceResult other = (RayTraceResult) obj; + if (!hitPosition.equals(other.hitPosition)) return false; + if (!Objects.equals(hitBlock, other.hitBlock)) return false; + if (!Objects.equals(hitBlockFace, other.hitBlockFace)) return false; + if (!Objects.equals(hitEntity, other.hitEntity)) return false; + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RayTraceResult [hitPosition="); + builder.append(hitPosition); + builder.append(", hitBlock="); + builder.append(hitBlock); + builder.append(", hitBlockFace="); + builder.append(hitBlockFace); + builder.append(", hitEntity="); + builder.append(hitEntity); + builder.append("]"); + return builder.toString(); + } +} diff --git a/paper-api/src/test/java/org/bukkit/LocationTest.java b/paper-api/src/test/java/org/bukkit/LocationTest.java index fa24776335..48d20761bb 100644 --- a/paper-api/src/test/java/org/bukkit/LocationTest.java +++ b/paper-api/src/test/java/org/bukkit/LocationTest.java @@ -17,7 +17,7 @@ import com.google.common.collect.ImmutableList; @RunWith(Parameterized.class) public class LocationTest { - private static final double δ = 1.0 / 1000000; + private static final double delta = 1.0 / 1000000; /** *

      * a² + b² = c², a = b
@@ -166,17 +166,17 @@ public class LocationTest {
     public void testExpectedPitchYaw() {
         Location location = getEmptyLocation().setDirection(getVector());
 
-        assertThat((double) location.getYaw(), is(closeTo(yaw, δ)));
-        assertThat((double) location.getPitch(), is(closeTo(pitch, δ)));
+        assertThat((double) location.getYaw(), is(closeTo(yaw, delta)));
+        assertThat((double) location.getPitch(), is(closeTo(pitch, delta)));
     }
 
     @Test
     public void testExpectedXYZ() {
         Vector vector = getLocation().getDirection();
 
-        assertThat(vector.getX(), is(closeTo(x, δ)));
-        assertThat(vector.getY(), is(closeTo(y, δ)));
-        assertThat(vector.getZ(), is(closeTo(z, δ)));
+        assertThat(vector.getX(), is(closeTo(x, delta)));
+        assertThat(vector.getY(), is(closeTo(y, delta)));
+        assertThat(vector.getZ(), is(closeTo(z, delta)));
     }
 
     private Vector getVector() {
diff --git a/paper-api/src/test/java/org/bukkit/util/BoundingBoxTest.java b/paper-api/src/test/java/org/bukkit/util/BoundingBoxTest.java
new file mode 100644
index 0000000000..1332aa2652
--- /dev/null
+++ b/paper-api/src/test/java/org/bukkit/util/BoundingBoxTest.java
@@ -0,0 +1,206 @@
+package org.bukkit.util;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.util.Map;
+
+import org.bukkit.Location;
+import org.bukkit.block.BlockFace;
+import org.junit.Test;
+
+public class BoundingBoxTest {
+
+    private static final double delta = 1.0 / 1000000;
+
+    @Test
+    public void testConstruction() {
+        BoundingBox expected = new BoundingBox(-1, -1, -1, 1, 2, 3);
+        assertThat(expected.getMin(), is(new Vector(-1, -1, -1)));
+        assertThat(expected.getMax(), is(new Vector(1, 2, 3)));
+        assertThat(expected.getCenter(), is(new Vector(0.0D, 0.5D, 1.0D)));
+        assertThat(expected.getWidthX(), is(2.0D));
+        assertThat(expected.getHeight(), is(3.0D));
+        assertThat(expected.getWidthZ(), is(4.0D));
+        assertThat(expected.getVolume(), is(24.0D));
+
+        assertThat(BoundingBox.of(new Vector(-1, -1, -1), new Vector(1, 2, 3)), is(expected));
+        assertThat(BoundingBox.of(new Vector(1, 2, 3), new Vector(-1, -1, -1)), is(expected));
+        assertThat(BoundingBox.of(new Location(null, -1, -1, -1), new Location(null, 1, 2, 3)), is(expected));
+        assertThat(BoundingBox.of(new Vector(0.0D, 0.5D, 1.0D), 1.0D, 1.5D, 2.0D), is(expected));
+        assertThat(BoundingBox.of(new Location(null, 0.0D, 0.5D, 1.0D), 1.0D, 1.5D, 2.0D), is(expected));
+    }
+
+    @Test
+    public void testContains() {
+        BoundingBox aabb = new BoundingBox(-1, -1, -1, 1, 2, 3);
+        assertThat(aabb.contains(-0.5D, 0.0D, 0.5D), is(true));
+        assertThat(aabb.contains(-1.0D, -1.0D, -1.0D), is(true));
+        assertThat(aabb.contains(1.0D, 2.0D, 3.0D), is(false));
+        assertThat(aabb.contains(-1.0D, 1.0D, 4.0D), is(false));
+        assertThat(aabb.contains(new Vector(-0.5D, 0.0D, 0.5D)), is(true));
+
+        assertThat(aabb.contains(new BoundingBox(-0.5D, -0.5D, -0.5D, 0.5D, 1.0D, 2.0D)), is(true));
+        assertThat(aabb.contains(aabb), is(true));
+        assertThat(aabb.contains(new BoundingBox(-1, -1, -1, 1, 1, 3)), is(true));
+        assertThat(aabb.contains(new BoundingBox(-2, -1, -1, 1, 2, 3)), is(false));
+        assertThat(aabb.contains(new Vector(-0.5D, -0.5D, -0.5D), new Vector(0.5D, 1.0D, 2.0D)), is(true));
+    }
+
+    @Test
+    public void testOverlaps() {
+        BoundingBox aabb = new BoundingBox(-1, -1, -1, 1, 2, 3);
+        assertThat(aabb.contains(aabb), is(true));
+        assertThat(aabb.overlaps(new BoundingBox(-2, -2, -2, 0, 0, 0)), is(true));
+        assertThat(aabb.overlaps(new BoundingBox(0.5D, 1.5D, 2.5D, 1, 2, 3)), is(true));
+        assertThat(aabb.overlaps(new BoundingBox(0.5D, 1.5D, 2.5D, 2, 3, 4)), is(true));
+        assertThat(aabb.overlaps(new BoundingBox(-2, -2, -2, -1, -1, -1)), is(false));
+        assertThat(aabb.overlaps(new BoundingBox(1, 2, 3, 2, 3, 4)), is(false));
+        assertThat(aabb.overlaps(new Vector(0.5D, 1.5D, 2.5D), new Vector(1, 2, 3)), is(true));
+    }
+
+    @Test
+    public void testDegenerate() {
+        BoundingBox aabb = new BoundingBox(0, 0, 0, 0, 0, 0);
+        assertThat(aabb.getWidthX(), is(0.0D));
+        assertThat(aabb.getHeight(), is(0.0D));
+        assertThat(aabb.getWidthZ(), is(0.0D));
+        assertThat(aabb.getVolume(), is(0.0D));
+    }
+
+    @Test
+    public void testShift() {
+        BoundingBox aabb = new BoundingBox(0, 0, 0, 1, 1, 1);
+        assertThat(aabb.clone().shift(1, 2, 3), is(new BoundingBox(1, 2, 3, 2, 3, 4)));
+        assertThat(aabb.clone().shift(-1, -2, -3), is(new BoundingBox(-1, -2, -3, 0, -1, -2)));
+        assertThat(aabb.clone().shift(new Vector(1, 2, 3)), is(new BoundingBox(1, 2, 3, 2, 3, 4)));
+        assertThat(aabb.clone().shift(new Location(null, 1, 2, 3)), is(new BoundingBox(1, 2, 3, 2, 3, 4)));
+    }
+
+    @Test
+    public void testUnion() {
+        BoundingBox aabb1 = new BoundingBox(0, 0, 0, 1, 1, 1);
+        assertThat(aabb1.clone().union(new BoundingBox(-2, -2, -2, -1, -1, -1)), is(new BoundingBox(-2, -2, -2, 1, 1, 1)));
+        assertThat(aabb1.clone().union(1, 2, 3), is(new BoundingBox(0, 0, 0, 1, 2, 3)));
+        assertThat(aabb1.clone().union(new Vector(1, 2, 3)), is(new BoundingBox(0, 0, 0, 1, 2, 3)));
+        assertThat(aabb1.clone().union(new Location(null, 1, 2, 3)), is(new BoundingBox(0, 0, 0, 1, 2, 3)));
+    }
+
+    @Test
+    public void testIntersection() {
+        BoundingBox aabb = new BoundingBox(-1, -1, -1, 1, 2, 3);
+        assertThat(aabb.clone().intersection(new BoundingBox(-2, -2, -2, 4, 4, 4)), is(aabb));
+        assertThat(aabb.clone().intersection(new BoundingBox(-2, -2, -2, 1, 1, 1)), is(new BoundingBox(-1, -1, -1, 1, 1, 1)));
+    }
+
+    @Test
+    public void testExpansion() {
+        BoundingBox aabb = new BoundingBox(0, 0, 0, 2, 2, 2);
+        assertThat(aabb.clone().expand(1, 2, 3, 1, 2, 3), is(new BoundingBox(-1, -2, -3, 3, 4, 5)));
+        assertThat(aabb.clone().expand(-1, -2, -3, 1, 2, 3), is(new BoundingBox(1, 2, 3, 3, 4, 5)));
+        assertThat(aabb.clone().expand(1, 2, 3, -1, -2, -3), is(new BoundingBox(-1, -2, -3, 1, 0, -1)));
+        assertThat(aabb.clone().expand(-1, -2, -3, -0.5D, -0.5, -3), is(new BoundingBox(1, 1.5D, 1, 1.5D, 1.5D, 1)));
+
+        assertThat(aabb.clone().expand(1, 2, 3), is(new BoundingBox(-1, -2, -3, 3, 4, 5)));
+        assertThat(aabb.clone().expand(-0.1, -0.5, -2), is(new BoundingBox(0.1D, 0.5D, 1, 1.9D, 1.5D, 1)));
+        assertThat(aabb.clone().expand(new Vector(1, 2, 3)), is(new BoundingBox(-1, -2, -3, 3, 4, 5)));
+
+        assertThat(aabb.clone().expand(1), is(new BoundingBox(-1, -1, -1, 3, 3, 3)));
+        assertThat(aabb.clone().expand(-0.5D), is(new BoundingBox(0.5D, 0.5D, 0.5D, 1.5D, 1.5D, 1.5D)));
+
+        assertThat(aabb.clone().expand(1, 0, 0, 0.5D), is(new BoundingBox(0, 0, 0, 2.5D, 2, 2)));
+        assertThat(aabb.clone().expand(1, 0, 0, -0.5D), is(new BoundingBox(0, 0, 0, 1.5D, 2, 2)));
+        assertThat(aabb.clone().expand(-1, 0, 0, 0.5D), is(new BoundingBox(-0.5D, 0, 0, 2, 2, 2)));
+        assertThat(aabb.clone().expand(-1, 0, 0, -0.5D), is(new BoundingBox(0.5D, 0, 0, 2, 2, 2)));
+
+        assertThat(aabb.clone().expand(0, 1, 0, 0.5D), is(new BoundingBox(0, 0, 0, 2, 2.5D, 2)));
+        assertThat(aabb.clone().expand(0, 1, 0, -0.5D), is(new BoundingBox(0, 0, 0, 2, 1.5D, 2)));
+        assertThat(aabb.clone().expand(0, -1, 0, 0.5D), is(new BoundingBox(0, -0.5D, 0, 2, 2, 2)));
+        assertThat(aabb.clone().expand(0, -1, 0, -0.5D), is(new BoundingBox(0, 0.5D, 0, 2, 2, 2)));
+
+        assertThat(aabb.clone().expand(0, 0, 1, 0.5D), is(new BoundingBox(0, 0, 0, 2, 2, 2.5D)));
+        assertThat(aabb.clone().expand(0, 0, 1, -0.5D), is(new BoundingBox(0, 0, 0, 2, 2, 1.5D)));
+        assertThat(aabb.clone().expand(0, 0, -1, 0.5D), is(new BoundingBox(0, 0, -0.5D, 2, 2, 2)));
+        assertThat(aabb.clone().expand(0, 0, -1, -0.5D), is(new BoundingBox(0, 0, 0.5D, 2, 2, 2)));
+
+        assertThat(aabb.clone().expand(new Vector(1, 0, 0), 0.5D), is(new BoundingBox(0, 0, 0, 2.5D, 2, 2)));
+        assertThat(aabb.clone().expand(BlockFace.EAST, 0.5D), is(new BoundingBox(0, 0, 0, 2.5D, 2, 2)));
+        assertThat(aabb.clone().expand(BlockFace.NORTH_NORTH_WEST, 1.0D), is(aabb.clone().expand(BlockFace.NORTH_NORTH_WEST.getDirection(), 1.0D)));
+        assertThat(aabb.clone().expand(BlockFace.SELF, 1.0D), is(aabb));
+
+        BoundingBox expanded = aabb.clone().expand(BlockFace.NORTH_WEST, 1.0D);
+        assertThat(expanded.getWidthX(), is(closeTo(aabb.getWidthX() + Math.sqrt(0.5D), delta)));
+        assertThat(expanded.getWidthZ(), is(closeTo(aabb.getWidthZ() + Math.sqrt(0.5D), delta)));
+        assertThat(expanded.getHeight(), is(aabb.getHeight()));
+
+        assertThat(aabb.clone().expandDirectional(1, 2, 3), is(new BoundingBox(0, 0, 0, 3, 4, 5)));
+        assertThat(aabb.clone().expandDirectional(-1, -2, -3), is(new BoundingBox(-1, -2, -3, 2, 2, 2)));
+        assertThat(aabb.clone().expandDirectional(new Vector(1, 2, 3)), is(new BoundingBox(0, 0, 0, 3, 4, 5)));
+    }
+
+    @Test
+    public void testRayTrace() {
+        BoundingBox aabb = new BoundingBox(-1, -1, -1, 1, 1, 1);
+
+        assertThat(aabb.rayTrace(new Vector(-2, 0, 0), new Vector(1, 0, 0), 10),
+                is(new RayTraceResult(new Vector(-1, 0, 0), BlockFace.WEST)));
+        assertThat(aabb.rayTrace(new Vector(2, 0, 0), new Vector(-1, 0, 0), 10),
+                is(new RayTraceResult(new Vector(1, 0, 0), BlockFace.EAST)));
+
+        assertThat(aabb.rayTrace(new Vector(0, -2, 0), new Vector(0, 1, 0), 10),
+                is(new RayTraceResult(new Vector(0, -1, 0), BlockFace.DOWN)));
+        assertThat(aabb.rayTrace(new Vector(0, 2, 0), new Vector(0, -1, 0), 10),
+                is(new RayTraceResult(new Vector(0, 1, 0), BlockFace.UP)));
+
+        assertThat(aabb.rayTrace(new Vector(0, 0, -2), new Vector(0, 0, 1), 10),
+                is(new RayTraceResult(new Vector(0, 0, -1), BlockFace.NORTH)));
+        assertThat(aabb.rayTrace(new Vector(0, 0, 2), new Vector(0, 0, -1), 10),
+                is(new RayTraceResult(new Vector(0, 0, 1), BlockFace.SOUTH)));
+
+        assertThat(aabb.rayTrace(new Vector(0, 0, 0), new Vector(1, 0, 0), 10),
+                is(new RayTraceResult(new Vector(1, 0, 0), BlockFace.EAST)));
+        assertThat(aabb.rayTrace(new Vector(0, 0, 0), new Vector(-1, 0, 0), 10),
+                is(new RayTraceResult(new Vector(-1, 0, 0), BlockFace.WEST)));
+
+        assertThat(aabb.rayTrace(new Vector(0, 0, 0), new Vector(0, 1, 0), 10),
+                is(new RayTraceResult(new Vector(0, 1, 0), BlockFace.UP)));
+        assertThat(aabb.rayTrace(new Vector(0, 0, 0), new Vector(0, -1, 0), 10),
+                is(new RayTraceResult(new Vector(0, -1, 0), BlockFace.DOWN)));
+
+        assertThat(aabb.rayTrace(new Vector(0, 0, 0), new Vector(0, 0, 1), 10),
+                is(new RayTraceResult(new Vector(0, 0, 1), BlockFace.SOUTH)));
+        assertThat(aabb.rayTrace(new Vector(0, 0, 0), new Vector(0, 0, -1), 10),
+                is(new RayTraceResult(new Vector(0, 0, -1), BlockFace.NORTH)));
+
+        assertThat(aabb.rayTrace(new Vector(-2, -2, -2), new Vector(1, 0, 0), 10), is(nullValue()));
+        assertThat(aabb.rayTrace(new Vector(-2, -2, -2), new Vector(0, 1, 0), 10), is(nullValue()));
+        assertThat(aabb.rayTrace(new Vector(-2, -2, -2), new Vector(0, 0, 1), 10), is(nullValue()));
+
+        assertThat(aabb.rayTrace(new Vector(0, 0, -3), new Vector(1, 0, 1), 10), is(nullValue()));
+        assertThat(aabb.rayTrace(new Vector(0, 0, -2), new Vector(1, 0, 2), 10),
+                is(new RayTraceResult(new Vector(0.5D, 0, -1), BlockFace.NORTH)));
+
+        // corner/edge hits yield unspecified block face:
+        assertThat(aabb.rayTrace(new Vector(2, 2, 2), new Vector(-1, -1, -1), 10),
+                anyOf(is(new RayTraceResult(new Vector(1, 1, 1), BlockFace.EAST)),
+                        is(new RayTraceResult(new Vector(1, 1, 1), BlockFace.UP)),
+                        is(new RayTraceResult(new Vector(1, 1, 1), BlockFace.SOUTH))));
+
+        assertThat(aabb.rayTrace(new Vector(-2, -2, -2), new Vector(1, 1, 1), 10),
+                anyOf(is(new RayTraceResult(new Vector(-1, -1, -1), BlockFace.WEST)),
+                        is(new RayTraceResult(new Vector(-1, -1, -1), BlockFace.DOWN)),
+                        is(new RayTraceResult(new Vector(-1, -1, -1), BlockFace.NORTH))));
+
+        assertThat(aabb.rayTrace(new Vector(0, 0, -2), new Vector(1, 0, 1), 10),
+                anyOf(is(new RayTraceResult(new Vector(1, 0, -1), BlockFace.NORTH)),
+                        is(new RayTraceResult(new Vector(1, 0, -1), BlockFace.EAST))));
+    }
+
+    @Test
+    public void testSerialization() {
+        BoundingBox aabb = new BoundingBox(-1, -1, -1, 1, 1, 1);
+        Map serialized = aabb.serialize();
+        BoundingBox deserialized = BoundingBox.deserialize(serialized);
+        assertThat(deserialized, is(aabb));
+    }
+}