From d5c5e4983e81a36d40d7726e1fe1414aa091ad9e Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 9 Dec 2020 03:28:22 -0500 Subject: [PATCH] Refactor boundary checks into unified bounding box (#1126) --- pom.xml | 12 - .../GriefPrevention/BlockEventHandler.java | 65 +- .../ryanhamshire/GriefPrevention/Claim.java | 28 +- .../GriefPrevention/util/BoundingBox.java | 732 ++++++++++++++++++ .../GriefPrevention/ClaimTest.java | 72 -- .../GriefPrevention/util/BoundingBoxTest.java | 182 +++++ 6 files changed, 943 insertions(+), 148 deletions(-) create mode 100644 src/main/java/me/ryanhamshire/GriefPrevention/util/BoundingBox.java delete mode 100644 src/test/java/me/ryanhamshire/GriefPrevention/ClaimTest.java create mode 100644 src/test/java/me/ryanhamshire/GriefPrevention/util/BoundingBoxTest.java diff --git a/pom.xml b/pom.xml index 6569427..c589c23 100644 --- a/pom.xml +++ b/pom.xml @@ -127,18 +127,6 @@ 5.6.2 test - - org.mockito - mockito-core - 3.4.6 - test - - - org.mockito - mockito-junit-jupiter - 3.4.6 - test - diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/BlockEventHandler.java b/src/main/java/me/ryanhamshire/GriefPrevention/BlockEventHandler.java index 5e0fc81..905c2f1 100644 --- a/src/main/java/me/ryanhamshire/GriefPrevention/BlockEventHandler.java +++ b/src/main/java/me/ryanhamshire/GriefPrevention/BlockEventHandler.java @@ -18,6 +18,7 @@ package me.ryanhamshire.GriefPrevention; +import me.ryanhamshire.GriefPrevention.util.BoundingBox; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.GameMode; @@ -540,36 +541,10 @@ public class BlockEventHandler implements Listener return; } - // Initialize bounding box for moved blocks with first in list. - int minX, maxX, minY, maxY, minZ, maxZ; - Block movedBlock = blocks.get(0); - minX = maxX = movedBlock.getX(); - minY = maxY = movedBlock.getY(); - minZ = maxZ = movedBlock.getZ(); - - // Fill in rest of bounding box with remaining blocks. - for (int count = 1; count < blocks.size(); ++count) - { - movedBlock = blocks.get(count); - minX = Math.min(minX, movedBlock.getX()); - minY = Math.min(minY, movedBlock.getY()); - minZ = Math.min(minZ, movedBlock.getZ()); - maxX = Math.max(maxX, movedBlock.getX()); - maxY = Math.max(maxY, movedBlock.getY()); - maxZ = Math.max(maxZ, movedBlock.getZ()); - } - - // Add direction to include invaded zone. - if (direction.getModX() > 0) - maxX += direction.getModX(); - else - minX += direction.getModX(); - if (direction.getModY() > 0) - maxY += direction.getModY(); - if (direction.getModZ() > 0) - maxZ += direction.getModZ(); - else - minZ += direction.getModZ(); + // Create bounding box for moved blocks. + BoundingBox movedBlocks = BoundingBox.ofBlocks(blocks); + // Expand to include invaded zone. + movedBlocks.resize(direction, 1); /* * Claims-only mode. All moved blocks must be inside of the owning claim. @@ -579,10 +554,7 @@ public class BlockEventHandler implements Listener */ if (pistonMode == PistonMode.CLAIMS_ONLY) { - Location minLoc = pistonClaim.getLesserBoundaryCorner(); - Location maxLoc = pistonClaim.getGreaterBoundaryCorner(); - - if (minY < minLoc.getY() || minX < minLoc.getBlockX() || maxX > maxLoc.getBlockX() || minZ < minLoc.getBlockZ() || maxZ > maxLoc.getBlockZ()) + if (!new BoundingBox(pistonClaim).contains(movedBlocks)) event.setCancelled(true); return; @@ -591,19 +563,26 @@ public class BlockEventHandler implements Listener // Ensure we have top level claim - piston ownership is only checked based on claim owner in everywhere mode. while (pistonClaim != null && pistonClaim.parent != null) pistonClaim = pistonClaim.parent; - // Pushing down or pulling up is safe if all blocks are in line with the piston. - if (minX == maxX && minZ == maxZ && direction == (isRetract ? BlockFace.UP : BlockFace.DOWN)) return; + // Check if blocks are in line vertically. + if (movedBlocks.getLength() == 1 && movedBlocks.getWidth() == 1) + { + // Pulling up is always safe. The claim may not contain the area pulled from, but claims cannot stack. + if (isRetract && direction == BlockFace.UP) return; + + // Pushing down is always safe. The claim may not contain the area pushed into, but claims cannot stack. + if (!isRetract && direction == BlockFace.DOWN) return; + } // Fast mode: Use the intersection of a cuboid containing all blocks instead of individual locations. if (pistonMode == PistonMode.EVERYWHERE_SIMPLE) { ArrayList intersectable = new ArrayList<>(); - int chunkXMax = maxX >> 4; - int chunkZMax = maxZ >> 4; + int chunkXMax = movedBlocks.getMaxX() >> 4; + int chunkZMax = movedBlocks.getMaxZ() >> 4; - for (int chunkX = minX >> 4; chunkX <= chunkXMax; ++chunkX) + for (int chunkX = movedBlocks.getMinX() >> 4; chunkX <= chunkXMax; ++chunkX) { - for (int chunkZ = minZ >> 4; chunkZ <= chunkZMax; ++chunkZ) + for (int chunkZ = movedBlocks.getMinZ() >> 4; chunkZ <= chunkZMax; ++chunkZ) { ArrayList chunkClaims = dataStore.chunksToClaimsMap.get(DataStore.getChunkHash(chunkX, chunkZ)); if (chunkClaims == null) continue; @@ -620,12 +599,8 @@ public class BlockEventHandler implements Listener { if (claim == pistonClaim) continue; - Location minLoc = claim.getLesserBoundaryCorner(); - Location maxLoc = claim.getGreaterBoundaryCorner(); - // Ensure claim intersects with bounding box. - if (maxY < minLoc.getBlockY() || minX > maxLoc.getBlockX() || maxX < minLoc.getBlockX() || minZ > maxLoc.getBlockZ() || maxZ < minLoc.getBlockZ()) - continue; + if (!new BoundingBox(claim).intersects(movedBlocks)) continue; // If owners are different, cancel. if (pistonClaim == null || !Objects.equals(pistonClaim.getOwnerID(), claim.getOwnerID())) diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/Claim.java b/src/main/java/me/ryanhamshire/GriefPrevention/Claim.java index 4cf9cf2..80dde29 100644 --- a/src/main/java/me/ryanhamshire/GriefPrevention/Claim.java +++ b/src/main/java/me/ryanhamshire/GriefPrevention/Claim.java @@ -18,6 +18,7 @@ package me.ryanhamshire.GriefPrevention; +import me.ryanhamshire.GriefPrevention.util.BoundingBox; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Material; @@ -35,6 +36,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -726,20 +728,14 @@ public class Claim public boolean contains(Location location, boolean ignoreHeight, boolean excludeSubdivisions) { //not in the same world implies false - if (!location.getWorld().equals(this.lesserBoundaryCorner.getWorld())) return false; + if (!Objects.equals(location.getWorld(), this.lesserBoundaryCorner.getWorld())) return false; - double x = location.getX(); - double y = location.getY(); - double z = location.getZ(); + int x = (int) location.getX(); + int y = (int) (ignoreHeight ? getLesserBoundaryCorner().getY() : location.getY()); + int z = (int) location.getZ(); //main check - boolean inClaim = (ignoreHeight || y >= this.lesserBoundaryCorner.getY()) && - x >= this.lesserBoundaryCorner.getX() && - x < this.greaterBoundaryCorner.getX() + 1 && - z >= this.lesserBoundaryCorner.getZ() && - z < this.greaterBoundaryCorner.getZ() + 1; - - if (!inClaim) return false; + if (!new BoundingBox(this).contains(x, y, z)) return false; //additional check for subdivisions //you're only in a subdivision when you're also in its parent claim @@ -772,15 +768,9 @@ public class Claim //used internally to prevent overlaps when creating claims boolean overlaps(Claim otherClaim) { - // For help visualizing test cases, try https://silentmatt.com/rectangle-intersection/ - - if (!this.lesserBoundaryCorner.getWorld().equals(otherClaim.getLesserBoundaryCorner().getWorld())) return false; - - return !(this.getGreaterBoundaryCorner().getX() < otherClaim.getLesserBoundaryCorner().getX() || - this.getLesserBoundaryCorner().getX() > otherClaim.getGreaterBoundaryCorner().getX() || - this.getGreaterBoundaryCorner().getZ() < otherClaim.getLesserBoundaryCorner().getZ() || - this.getLesserBoundaryCorner().getZ() > otherClaim.getGreaterBoundaryCorner().getZ()); + if (!Objects.equals(this.lesserBoundaryCorner.getWorld(), otherClaim.getLesserBoundaryCorner().getWorld())) return false; + return new BoundingBox(this).intersects(new BoundingBox(otherClaim)); } //whether more entities may be added to a claim diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/util/BoundingBox.java b/src/main/java/me/ryanhamshire/GriefPrevention/util/BoundingBox.java new file mode 100644 index 0000000..d9f038e --- /dev/null +++ b/src/main/java/me/ryanhamshire/GriefPrevention/util/BoundingBox.java @@ -0,0 +1,732 @@ +package me.ryanhamshire.GriefPrevention.util; + +import me.ryanhamshire.GriefPrevention.Claim; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.util.NumberConversions; +import org.bukkit.util.Vector; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; + +/** + * A mutable block-based axis-aligned bounding box. + * + *

This is a rectangular box defined by minimum and maximum corners + * that can be used to represent a collection of blocks. + * + *

While similar to Bukkit's {@link org.bukkit.util.BoundingBox BoundingBox}, + * this implementation is much more focused on performance and does not use as + * many input sanitization operations. + * + * @author Jikoo + */ +public class BoundingBox implements Cloneable +{ + + /** + * Construct a new bounding box containing all of the given blocks. + * + * @param blocks a collection of blocks to construct a bounding box around + * @return the bounding box + */ + public static BoundingBox ofBlocks(Collection blocks) + { + if (blocks.size() == 0) throw new IllegalArgumentException("Cannot create bounding box with no blocks!"); + + Iterator iterator = blocks.iterator(); + // Initialize bounding box with first block + BoundingBox box = new BoundingBox(iterator.next()); + + // Fill in rest of bounding box with remaining blocks. + while (iterator.hasNext()) + { + Block block = iterator.next(); + box.union(block.getX(), block.getY(), block.getZ()); + } + + return box; + } + + private int minX; + private int minY; + private int minZ; + private int maxX; + private int maxY; + private int maxZ; + + /** + * Construct a new bounding box with the given corners. + * + * @param x1 the X coordinate of the first corner + * @param y1 the Y coordinate of the first corner + * @param z1 the Z coordinate of the first corner + * @param x2 the X coordinate of the second corner + * @param y2 the Y coordinate of the second corner + * @param z2 the Z coordinate of the second corner + * @param verify whether or not to verify that the provided corners are in fact the minimum corners + */ + protected BoundingBox(int x1, int y1, int z1, int x2, int y2, int z2, boolean verify) { + if (verify) + { + verify(x1, y1, z1, x2, y2, z2); + } + else + { + this.minX = x1; + this.maxX = x2; + this.minY = y1; + this.maxY = y2; + this.minZ = z1; + this.maxZ = z2; + } + } + + /** + * Construct a new bounding box with the given corners. + * + * @param x1 the X coordinate of the first corner + * @param y1 the Y coordinate of the first corner + * @param z1 the Z coordinate of the first corner + * @param x2 the X coordinate of the second corner + * @param y2 the Y coordinate of the second corner + * @param z2 the Z coordinate of the second corner + */ + public BoundingBox(int x1, int y1, int z1, int x2, int y2, int z2) + { + this(x1, y1, z1, x2, y2, z2, true); + } + + /** + * Construct a new bounding box with the given corners. + * + * @param pos1 the position of the first corner + * @param pos2 the position of the second corner + * @param verify whether or not to verify that the provided corners are in fact the minimum corners + */ + private BoundingBox(Location pos1, Location pos2, boolean verify) + { + this(pos1.getBlockX(), pos1.getBlockY(), pos1.getBlockZ(), + pos2.getBlockX(), pos2.getBlockY(), pos2.getBlockZ(), + verify); + } + + /** + * Construct a new bounding box with the given corners. + * + * @param pos1 the position of the first corner + * @param pos2 the position of the second corner + */ + public BoundingBox(Location pos1, Location pos2) + { + this(pos1, pos2, true); + } + + /** + * Construct a new bounding box with the given corners. + * + * @param pos1 the position of the first corner + * @param pos2 the position of the second corner + */ + public BoundingBox(Vector pos1, Vector pos2) + { + this(pos1.getBlockX(), pos1.getBlockY(), pos1.getBlockZ(), + pos2.getBlockX(), pos2.getBlockY(), pos2.getBlockZ(), true); + } + + /** + * Construct a new bounding box representing the given claim. + * + * @param claim the claim + */ + public BoundingBox(Claim claim) + { + this(claim.getLesserBoundaryCorner(), claim.getGreaterBoundaryCorner(), false); + } + + /** + * Construct a new bounding box representing the given block. + * + * @param block the block + */ + public BoundingBox(Block block) + { + this(block.getX(), block.getY(), block.getZ(), block.getX(), block.getY(), block.getZ(), false); + } + + /** + * Construct a new bounding box representing the given Bukkit {@link org.bukkit.util.BoundingBox BoundingBox}. + * + * @param boundingBox the Bukkit bounding box + */ + public BoundingBox(org.bukkit.util.BoundingBox boundingBox) + { + this((int) boundingBox.getMinX(), + (int) boundingBox.getMinY(), + (int) boundingBox.getMinZ(), + // Since Bukkit bounding boxes are inclusive of upper bounds, subtract a small number. + // This ensures that a full block Bukkit bounding boxes yield correct equivalents. + // Uses Math.max to account for degenerate boxes. + (int) Math.max(boundingBox.getMinX(), boundingBox.getMaxX() - .0001), + (int) Math.max(boundingBox.getMinY(), boundingBox.getMaxY() - .0001), + (int) Math.max(boundingBox.getMinZ(), boundingBox.getMaxZ() - .0001), + false); + } + + /** + * Sets bounds of this bounding box to the specified values. + * Ensures that the minimum and maximum corners are set from the correct respective values. + * + * @param x1 the first X value + * @param y1 the first Y value + * @param z1 the first Z value + * @param x2 the second X value + * @param y2 the second Y value + * @param z2 the second Z value + */ + private void verify(int x1, int y1, int z1, int x2, int y2, int z2) { + if (x1 < x2) + { + this.minX = x1; + this.maxX = x2; + } + else + { + this.minX = x2; + this.maxX = x1; + } + if (y1 < y2) + { + this.minY = y1; + this.maxY = y2; + } + else + { + this.minY = y2; + this.maxY = y1; + } + if (z1 < z2) + { + this.minZ = z1; + this.maxZ = z2; + } + else + { + this.minZ = z2; + this.maxZ = z1; + } + } + + /** + * Gets the minimum X coordinate of the bounding box. + * + * @return the minimum X value + */ + public int getMinX() + { + return this.minX; + } + + /** + * Gets the minimum Y coordinate of the bounding box. + * + * @return the minimum Y value + */ + public int getMinY() + { + return this.minY; + } + + /** + * Gets the minimum Y coordinate of the bounding box. + * + * @return the minimum Y value + */ + public int getMinZ() + { + return this.minZ; + } + + /** + * Gets the minimum corner's coordinates as a vector. + * + * @return the minimum corner as a vector + */ + public Vector getMin() + { + return new Vector(this.minX, this.minY, this.minZ); + } + + /** + * Gets the maximum X coordinate of the bounding box. + * + * @return the maximum X value + */ + public int getMaxX() + { + return this.maxX; + } + + /** + * Gets the maximum Y coordinate of the bounding box. + * + * @return the maximum Y value + */ + public int getMaxY() + { + return this.maxY; + } + + /** + * Gets the maximum Z coordinate of the bounding box. + * + * @return the maximum Z value + */ + public int getMaxZ() + { + return this.maxZ; + } + + /** + * Gets the maximum corner's coordinates as a vector. + * + * @return the maximum corner as a vector + */ + public Vector getMax() + { + return new Vector(this.maxX, this.maxY, this.maxZ); + } + + /** + * Gets the length of the bounding box on the X axis. + * + * @return the length on the X axis + */ + public int getLength() + { + return (this.maxX - this.minX) + 1; + } + + /** + * Gets the length of the bounding box on the Y axis. + * + * @return the length on the Y axis + */ + public int getHeight() + { + return (this.maxY - this.minY) + 1; + } + + /** + * Gets the length of the bounding box on the Z axis. + * + * @return the length on the Z axis + */ + public int getWidth() + { + return (this.maxZ - this.minZ) + 1; + } + + /** + * Gets the center of the bounding box on the X axis. + * + *

Note that center coordinates are world coordinates + * while all of the other coordinates are block coordinates. + * + * @return the center of the X axis + */ + public double getCenterX() + { + return this.minX + (this.getLength() / 2D); + } + + /** + * Gets the center of the bounding box on the Y axis. + * + *

Note that center coordinates are world coordinates + * while all of the other coordinates are block coordinates. + * + * @return the center of the X axis + */ + public double getCenterY() + { + return this.minY + (this.getHeight() / 2D); + } + + /** + * Gets the center of the bounding box on the Z axis. + * + *

Note that center coordinates are world coordinates + * while all of the other coordinates are block coordinates. + * + * @return the center of the X axis + */ + public double getCenterZ() + { + return this.minZ + (this.getWidth() / 2D); + } + + /** + * Gets the center of the bounding box as a vector. + * + *

Note that center coordinates are world coordinates + * while all of the other coordinates are block coordinates. + * + * @return the center of the X axis + */ + public Vector getCenter() + { + return new Vector(this.getCenterX(), this.getCenterY(), this.getCenterZ()); + } + + /** + * Gets the area of the base of the bounding box. + * + *

The base is the lowest plane defined by the X and Z axis. + * + * @return the area of the base of the bounding box + */ + public int getArea() + { + return this.getLength() * this.getWidth(); + } + + /** + * Gets the volume of the bounding box. + * + * @return the volume of the bounding box + */ + public int getVolume() + { + return this.getArea() * getHeight(); + } + + /** + * Copies the dimensions and location of another bounding box. + * + * @param other the bounding box to copy + */ + public void copy(BoundingBox other) + { + this.minX = other.minX; + this.minY = other.minY; + this.minZ = other.minZ; + this.maxX = other.maxX; + this.maxY = other.maxY; + this.maxZ = other.maxZ; + } + + /** + * Changes the size the bounding box in the direction specified by the Minecraft blockface. + * + *

If the specified directional magnitude is negative, the box is contracted instead. + * + *

When contracting, the box does not care if the contraction would cause a negative side length. + * In these cases, the lowest point is redefined by the new location of the maximum corner instead. + * + * @param direction the direction to change size in + * @param magnitude the magnitude of the resizing + */ + public void resize(BlockFace direction, int magnitude) + { + if (magnitude == 0 || direction == BlockFace.SELF) return; + + Vector vector = direction.getDirection().multiply(magnitude); + + // Force normalized rounding - prevents issues with non-cardinal directions. + int modX = NumberConversions.round(vector.getX()); + int modY = NumberConversions.round(vector.getY()); + int modZ = NumberConversions.round(vector.getZ()); + + if (modX == 0 && modY == 0 && modZ == 0) return; + + // Modify correct point. + if (direction.getModX() > 0) + this.maxX += modX; + else + this.minX += modX; + if (direction.getModY() > 0) + this.maxY += modY; + else + this.minY += modY; + if (direction.getModZ() > 0) + this.maxZ += modZ; + else + this.minZ += modZ; + + // If box is contracting, re-verify points in case corners have swapped. + if (magnitude < 0) + verify(this.minX, this.minY, this.minZ, this.maxX, this.maxY, this.maxZ); + } + + /** + * Moves the bounding box in the direction specified by the Minecraft BlockFace and magnitude. + * + *

Note that a negative direction will move in the opposite direction + * to the extent that the following example returns true: + *

+     * public boolean testBoxMove(BoundingBox box, BlockFace face, int magnitude)
+     * {
+     *     BoundingBox box2 = box.clone();
+     *     box.move(face, magnitude);
+     *     box2.move(face.getOpposite(), -magnitude);
+     *     return box.equals(box2);
+     * }
+     * 
+ * + * @param direction the direction to move in + * @param magnitude the magnitude of the move + */ + public void move(BlockFace direction, int magnitude) + { + if (magnitude == 0 || direction == BlockFace.SELF) return; + + Vector vector = direction.getDirection().multiply(magnitude); + + int blockX = NumberConversions.round(vector.getX()); + this.minX += blockX; + this.maxX += blockX; + int blockY = NumberConversions.round(vector.getY()); + this.minY += blockY; + this.maxY += blockY; + int blockZ = NumberConversions.round(vector.getZ()); + this.minZ += blockZ; + this.maxZ += blockZ; + } + + /** + * Expands the bounding box to contain the position specified. + * + * @param x the X coordinate to include + * @param y the Y coordinate to include + * @param z the Z coordinate to include + */ + public void union(int x, int y, int z) + { + this.minX = Math.min(x, this.minX); + this.maxX = Math.max(x, this.maxX); + this.minY = Math.min(y, this.minY); + this.maxY = Math.max(y, this.maxY); + this.minZ = Math.min(z, this.minZ); + this.maxZ = Math.max(z, this.maxZ); + } + + /** + * Expands the bounding box to contain the position specified. + * + * @param position the position to include + */ + public void union(Block position) + { + this.union(position.getX(), position.getY(), position.getZ()); + } + + /** + * Expands the bounding box to contain the position specified. + * + * @param position the position to include + */ + public void union(Vector position) + { + this.union(position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } + + /** + * Expands the bounding box to contain the position specified. + * + * @param position the position to include + */ + public void union(Location position) + { + this.union(position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } + + /** + * Expands the bounding box to contain the bounding box specified. + * + * @param other the bounding box to include + */ + public void union(BoundingBox other) + { + this.minX = Math.min(this.minX, other.minX); + this.maxX = Math.max(this.maxX, other.maxX); + this.minY = Math.min(this.minY, other.minY); + this.maxY = Math.max(this.maxY, other.maxY); + this.minZ = Math.min(this.minZ, other.minZ); + this.maxZ = Math.max(this.maxZ, other.maxZ); + } + + /** + * Internal containment check. + * + * @param minX the minimum X value to check for containment + * @param minY the minimum Y value to check for containment + * @param minZ the minimum Z value to check for containment + * @param maxX the maximum X value to check for containment + * @param maxY the maximum X value to check for containment + * @param maxZ the maximum X value to check for containment + * @return true if the specified values are inside the bounding box + */ + private boolean containsInternal(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) + { + return minX >= this.minX && maxX <= this.maxX + && minY >= this.minY && maxY <= this.maxY + && minZ >= this.minZ && maxZ <= this.maxZ; + } + + /** + * Checks if the bounding box contains the position specified. + * + * @param x the X coordinate of the position + * @param y the Y coordinate of the position + * @param z the Z coordinate of the position + * @return true if the specified position is inside the bounding box + */ + public boolean contains(int x, int y, int z) + { + return containsInternal(x, y, z, x, y, z); + } + + /** + * Checks if the bounding box contains the position specified. + * + * @param position the position + * @return true if the specified position is inside the bounding box + */ + public boolean contains(Vector position) + { + return contains(position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } + + /** + * Checks if the bounding box contains the position specified. + * + * @param position the position + * @return true if the specified position is inside the bounding box + */ + public boolean contains(Location position) + { + return contains(position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } + + /** + * Checks if the bounding box contains the position specified. + * + * @param position the position + * @return true if the specified position is inside the bounding box + */ + public boolean contains(Block position) + { + return contains(position.getX(), position.getY(), position.getZ()); + } + + /** + * Checks if the bounding box contains another bounding box consisting of the positions specified. + * + * @param x1 the X coordinate of the first position + * @param y1 the Y coordinate of the first position + * @param z1 the Z coordinate of the first position + * @param x2 the X coordinate of the second position + * @param y2 the Y coordinate of the second position + * @param z2 the Z coordinate of the second position + * @return true if the specified positions are inside the bounding box + */ + public boolean contains(int x1, int y1, int z1, int x2, int y2, int z2) + { + return contains(new BoundingBox(x1, y1, z1, x2, y2, z2)); + } + + /** + * Checks if the bounding box contains another bounding box. + * + * @param other the other bounding box + * @return true if the specified positions are inside the bounding box + */ + public boolean contains(BoundingBox other) + { + return containsInternal(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ); + } + + /** + * Checks if the bounding box intersects another bounding box. + * + * @param other the other bounding box + * @return true if the specified positions are inside the bounding box + */ + public boolean intersects(BoundingBox other) + { + // For help visualizing test cases, try https://silentmatt.com/rectangle-intersection/ + return this.minX <= other.maxX && this.maxX >= other.minX + && this.minY <= other.maxY && this.maxY >= other.minY + && this.minZ <= other.maxZ && this.maxZ >= other.minZ; + } + + /** + * Gets a bounding box containing the intersection of the bounding box with another. + * + * @param other the other bounding box + * @return the bounding box representing overlapping area or null if the boxes do not overlap. + */ + public BoundingBox intersection(BoundingBox other) + { + if (!intersects(other)) return null; + + return new BoundingBox( + Math.max(this.minX, other.minX), + Math.max(this.minY, other.minY), + Math.max(this.minZ, other.minZ), + Math.min(this.maxX, other.maxX), + Math.min(this.maxY, other.maxY), + Math.min(this.maxZ, other.maxZ), + false); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BoundingBox other = (BoundingBox) o; + return this.minX == other.minX + && this.minY == other.minY + && this.minZ == other.minZ + && this.maxX == other.maxX + && this.maxY == other.maxY + && this.maxZ == other.maxZ; + } + + @Override + public int hashCode() + { + return Objects.hash(this.minX, this.minY, this.minZ, this.maxX, this.maxY, this.maxZ); + } + + @Override + public String toString() + { + return "BoundingBox{" + + "minX=" + minX + + ", minY=" + minY + + ", minZ=" + minZ + + ", maxX=" + maxX + + ", maxY=" + maxY + + ", maxZ=" + maxZ + + '}'; + } + + @Override + public BoundingBox clone() + { + try + { + return (BoundingBox) super.clone(); + } + catch (CloneNotSupportedException e) + { + throw new Error(e); + } + } + +} diff --git a/src/test/java/me/ryanhamshire/GriefPrevention/ClaimTest.java b/src/test/java/me/ryanhamshire/GriefPrevention/ClaimTest.java deleted file mode 100644 index 5fa3437..0000000 --- a/src/test/java/me/ryanhamshire/GriefPrevention/ClaimTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package me.ryanhamshire.GriefPrevention; - -import org.bukkit.Location; -import org.bukkit.World; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ClaimTest { - - @Test - public void testClaimOverlap() - { - World world = Mockito.mock(World.class); - - // One corner inside - Claim claimA = new Claim(new Location(world, 0, 0, 0), new Location(world, 10, 0, 10), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - Claim claimB = new Claim(new Location(world, 5, 0, 5), new Location(world, 15, 0, 15), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - Claim claimC = new Claim(new Location(world, -5, 0, -5), new Location(world, 4, 0, 4), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - - assertTrue(claimA.overlaps(claimB)); - assertTrue(claimB.overlaps(claimA)); - assertTrue(claimA.overlaps(claimC)); - assertTrue(claimC.overlaps(claimA)); - assertFalse(claimB.overlaps(claimC)); - assertFalse(claimC.overlaps(claimB)); - - // Complete containment - claimA = new Claim(new Location(world, 0, 0, 0), new Location(world, 20, 0, 20), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - claimB = new Claim(new Location(world, 5, 0, 5), new Location(world, 15, 0, 15), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - - assertTrue(claimA.overlaps(claimB)); - assertTrue(claimB.overlaps(claimA)); - - // Central intersection - claimA = new Claim(new Location(world, 0, 0, 5), new Location(world, 10, 0, 15), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - claimB = new Claim(new Location(world, 5, 0, 0), new Location(world, 15, 0, 10), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - - assertTrue(claimA.overlaps(claimB)); - assertTrue(claimB.overlaps(claimA)); - - // Linear North-South - claimA = new Claim(new Location(world, 0, 0, 0), new Location(world, 10, 0, 10), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - claimB = new Claim(new Location(world, 0, 0, 15), new Location(world, 15, 0, 25), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - - assertFalse(claimA.overlaps(claimB)); - assertFalse(claimB.overlaps(claimA)); - - // Linear East-West - claimA = new Claim(new Location(world, 0, 0, 0), new Location(world, 10, 0, 10), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - claimB = new Claim(new Location(world, 15, 0, 0), new Location(world, 25, 0, 15), null, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L); - - assertFalse(claimA.overlaps(claimB)); - assertFalse(claimB.overlaps(claimA)); - } - -} diff --git a/src/test/java/me/ryanhamshire/GriefPrevention/util/BoundingBoxTest.java b/src/test/java/me/ryanhamshire/GriefPrevention/util/BoundingBoxTest.java new file mode 100644 index 0000000..008d82d --- /dev/null +++ b/src/test/java/me/ryanhamshire/GriefPrevention/util/BoundingBoxTest.java @@ -0,0 +1,182 @@ +package me.ryanhamshire.GriefPrevention.util; + +import org.bukkit.block.BlockFace; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BoundingBoxTest +{ + @Test + public void testVerify() + { + BoundingBox boxA = new BoundingBox(0, 0, 0, 10, 10, 10); + BoundingBox boxB = new BoundingBox(10, 0, 10, 0, 10, 0); + assertEquals(boxA, boxB); + } + + @Test + public void testMeasurements() + { + BoundingBox boxA = new BoundingBox(-1, 0, 1, 5, 4, 3); + assertEquals(7, boxA.getLength()); + assertEquals(5, boxA.getHeight()); + assertEquals(3, boxA.getWidth()); + assertEquals(7 * 3, boxA.getArea()); + assertEquals(7 * 3 * 5, boxA.getVolume()); + assertEquals(2.5, boxA.getCenterX()); + assertEquals(2.5, boxA.getCenterY()); + assertEquals(2.5, boxA.getCenterZ()); + } + + @Test + public void testCopy() + { + BoundingBox boxA = new BoundingBox(1, 2, 3, 4, 5, 6); + BoundingBox boxB = new BoundingBox(7, 8, 9, 10, 11, 12); + boxB.copy(boxA); + assertEquals(boxA.getMinX(), boxB.getMinX()); + assertEquals(boxA.getMinY(), boxB.getMinY()); + assertEquals(boxA.getMinZ(), boxB.getMinZ()); + assertEquals(boxA.getMaxX(), boxB.getMaxX()); + assertEquals(boxA.getMaxY(), boxB.getMaxY()); + assertEquals(boxA.getMaxZ(), boxB.getMaxZ()); + } + + @Test + public void testResize() + { + testBlockfaceFunction(BoundingBox::resize, + new BoundingBox(0, 0, -10, 10, 10, 10)); + } + + private void testBlockfaceFunction(TriConsumer function, BoundingBox boxB) + { + BoundingBox boxA = new BoundingBox(0, 0, 0, 10, 10, 10); + function.apply(boxA, BlockFace.NORTH, 10); + assertEquals(boxB, boxA); + + for (BlockFace face : BlockFace.values()) + { + if (face == BlockFace.SELF) + { + function.apply(boxA, face, 15); + assertEquals(boxB, boxA); + continue; + } + function.apply(boxA, face, 15); + assertNotEquals(boxB, boxA); + function.apply(boxA, face, -15); + assertEquals(boxB, boxA); + } + } + + private interface TriConsumer { + void apply(T t, U u, V v); + } + + @Test + public void testMove() + { + testBlockfaceFunction(BoundingBox::move, + new BoundingBox(0, 0, -10, 10, 10, 0)); + + BoundingBox boxA = new BoundingBox(0, 0, 0, 10, 10, 10); + BoundingBox boxB = boxA.clone(); + boxA.move(BlockFace.EAST, 15); + assertNotEquals(boxB, boxA); + BoundingBox boxC = boxA.clone(); + boxA.move(BlockFace.EAST, -15); + assertEquals(boxB, boxA); + boxC.move(BlockFace.EAST.getOppositeFace(), 15); + assertEquals(boxB, boxC); + } + + @Test + public void testUnion() + { + BoundingBox boxA = new BoundingBox(0, 0, 0, 10, 10, 10); + BoundingBox boxB = new BoundingBox(0, 0, 0, 10, 15, 20); + BoundingBox boxC = new BoundingBox(-10, 0, 0, 10, 15, 20); + boxA.union(0, 15, 20); + + assertEquals(boxB, boxA); + boxA.union(-10, 7, 10); + assertEquals(boxC, boxA); + } + + @Test + public void testIntersectCorner() + { + // One corner inside + BoundingBox boxA = new BoundingBox(0, 0, 0, 10, 0, 10); + BoundingBox boxB = new BoundingBox(5, 0, 5, 15, 0, 15); + BoundingBox boxC = new BoundingBox(-5, 0, -5, 4, 0, 4); + + assertTrue(boxA.intersects(boxB)); + assertTrue(boxB.intersects(boxA)); + assertTrue(boxA.intersects(boxC)); + assertTrue(boxC.intersects(boxA)); + assertFalse(boxB.intersects(boxC)); + assertFalse(boxC.intersects(boxB)); + } + + @Test + public void testIntersectCenter() + { + // Central intersection + BoundingBox boxA = new BoundingBox(0, 0, 5, 10, 0, 15); + BoundingBox boxB = new BoundingBox(5, 0, 0, 15, 0, 10); + + assertTrue(boxA.intersects(boxB)); + assertTrue(boxB.intersects(boxA)); + } + + @Test + public void testIntersectLinearAdjacent() + { + // Linear North-South + BoundingBox boxA = new BoundingBox(0, 0, 0, 10, 0, 10); + BoundingBox boxB = new BoundingBox(0, 0, 11, 10, 0, 21); + BoundingBox boxC = new BoundingBox(0, 0, 10, 10, 0, 20); + + // Adjacent + assertFalse(boxA.intersects(boxB)); + assertFalse(boxB.intersects(boxA)); + // Overlapping on edge + assertTrue(boxA.intersects(boxC)); + assertTrue(boxC.intersects(boxA)); + + // Linear East-West + boxA = new BoundingBox(0, 0, 0, 10, 0, 10); + boxB = new BoundingBox(11, 0, 0, 21, 0, 10); + boxC = new BoundingBox(10, 0, 0, 20, 0, 10); + + // Adjacent + assertFalse(boxA.intersects(boxB)); + assertFalse(boxB.intersects(boxA)); + // Overlapping on edge + assertTrue(boxA.intersects(boxC)); + assertTrue(boxC.intersects(boxA)); + } + + @Test + public void testContainment() + { + // Complete containment + BoundingBox boxA = new BoundingBox(0, 0, 0, 20, 0, 20); + BoundingBox boxB = new BoundingBox(5, 0, 5, 15, 0, 15); + BoundingBox boxC = new BoundingBox(-5, 0, -5, 4, 0, 4); + BoundingBox boxD = boxA.clone(); + + assertTrue(boxA.contains(boxB)); + assertTrue(boxB.intersects(boxA)); + assertFalse(boxB.contains(boxA)); + assertFalse(boxA.contains(boxC)); + assertTrue(boxA.contains(boxD)); + assertTrue(boxD.contains(boxA)); + } +}