Skip to content

Instantly share code, notes, and snippets.

@ursinn
Last active April 29, 2024 16:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ursinn/871525236408e33d4cbee607f7eff8ae to your computer and use it in GitHub Desktop.
Save ursinn/871525236408e33d4cbee607f7eff8ae to your computer and use it in GitHub Desktop.
Spigot Plugin | Cuboid Helper
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
/**
* This class is a region/cuboid from one location to another. It can be used for blocks protection and things like WorldEdit.
* @author desht (Original code), KingFaris10 (Editor of code)
*/
public class Cuboid implements Iterable<Block>, Cloneable, ConfigurationSerializable {
protected final String worldName;
protected final int x1, y1, z1;
protected final int x2, y2, z2;
/**
* Construct a Cuboid given two Location objects which represent any two corners of the Cuboid.
* Note: The 2 locations must be on the same world.
*
* @param l1 - One of the corners
* @param l2 - The other corner
*/
public Cuboid(Location l1, Location l2) {
if (!l1.getWorld().equals(l2.getWorld())) throw new IllegalArgumentException("Locations must be on the same world");
this.worldName = l1.getWorld().getName();
this.x1 = Math.min(l1.getBlockX(), l2.getBlockX());
this.y1 = Math.min(l1.getBlockY(), l2.getBlockY());
this.z1 = Math.min(l1.getBlockZ(), l2.getBlockZ());
this.x2 = Math.max(l1.getBlockX(), l2.getBlockX());
this.y2 = Math.max(l1.getBlockY(), l2.getBlockY());
this.z2 = Math.max(l1.getBlockZ(), l2.getBlockZ());
}
/**
* Construct a one-block Cuboid at the given Location of the Cuboid.
*
* @param l1 location of the Cuboid
*/
public Cuboid(Location l1) {
this(l1, l1);
}
/**
* Copy constructor.
*
* @param other - The Cuboid to copy
*/
public Cuboid(Cuboid other) {
this(other.getWorld().getName(), other.x1, other.y1, other.z1, other.x2, other.y2, other.z2);
}
/**
* Construct a Cuboid in the given World and xyz co-ordinates
*
* @param world - The Cuboid's world
* @param x1 - X co-ordinate of corner 1
* @param y1 - Y co-ordinate of corner 1
* @param z1 - Z co-ordinate of corner 1
* @param x2 - X co-ordinate of corner 2
* @param y2 - Y co-ordinate of corner 2
* @param z2 - Z co-ordinate of corner 2
*/
public Cuboid(World world, int x1, int y1, int z1, int x2, int y2, int z2) {
this.worldName = world.getName();
this.x1 = Math.min(x1, x2);
this.x2 = Math.max(x1, x2);
this.y1 = Math.min(y1, y2);
this.y2 = Math.max(y1, y2);
this.z1 = Math.min(z1, z2);
this.z2 = Math.max(z1, z2);
}
/**
* Construct a Cuboid in the given world name and xyz co-ordinates.
*
* @param worldName - The Cuboid's world name
* @param x1 - X co-ordinate of corner 1
* @param y1 - Y co-ordinate of corner 1
* @param z1 - Z co-ordinate of corner 1
* @param x2 - X co-ordinate of corner 2
* @param y2 - Y co-ordinate of corner 2
* @param z2 - Z co-ordinate of corner 2
*/
private Cuboid(String worldName, int x1, int y1, int z1, int x2, int y2, int z2) {
this.worldName = worldName;
this.x1 = Math.min(x1, x2);
this.x2 = Math.max(x1, x2);
this.y1 = Math.min(y1, y2);
this.y2 = Math.max(y1, y2);
this.z1 = Math.min(z1, z2);
this.z2 = Math.max(z1, z2);
}
/**
* Construct a Cuboid using a map with the following keys: worldName, x1, x2, y1, y2, z1, z2
* @param map - The map of keys.
*/
public Cuboid(Map<String, Object> map) {
this.worldName = (String) map.get("worldName");
this.x1 = (Integer) map.get("x1");
this.x2 = (Integer) map.get("x2");
this.y1 = (Integer) map.get("y1");
this.y2 = (Integer) map.get("y2");
this.z1 = (Integer) map.get("z1");
this.z2 = (Integer) map.get("z2");
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("worldName", this.worldName);
map.put("x1", this.x1);
map.put("y1", this.y1);
map.put("z1", this.z1);
map.put("x2", this.x2);
map.put("y2", this.y2);
map.put("z2", this.z2);
return map;
}
/**
* Get the Location of the lower northeast corner of the Cuboid (minimum XYZ co-ordinates).
*
* @return Location of the lower northeast corner
*/
public Location getLowerNE() {
return new Location(this.getWorld(), this.x1, this.y1, this.z1);
}
/**
* Get the Location of the upper southwest corner of the Cuboid (maximum XYZ co-ordinates).
*
* @return Location of the upper southwest corner
*/
public Location getUpperSW() {
return new Location(this.getWorld(), this.x2, this.y2, this.z2);
}
/**
* Get the blocks in the Cuboid.
*
* @return The blocks in the Cuboid
*/
public ArrayList<Block> getBlocks() {
Iterator<Block> blockI = this.iterator();
ArrayList<Block> copy = new ArrayList<Block>();
while (blockI.hasNext())
copy.add(blockI.next());
return copy;
}
/**
* Get the the centre of the Cuboid.
*
* @return Location at the centre of the Cuboid
*/
public Location getCenter() {
int x1 = this.getUpperX() + 1;
int y1 = this.getUpperY() + 1;
int z1 = this.getUpperZ() + 1;
return new Location(this.getWorld(), this.getLowerX() + (x1 - this.getLowerX()) / 2.0, this.getLowerY() + (y1 - this.getLowerY()) / 2.0, this.getLowerZ() + (z1 - this.getLowerZ()) / 2.0);
}
/**
* Get the Cuboid's world.
*
* @return The World object representing this Cuboid's world
* @throws IllegalStateException if the world is not loaded
*/
public World getWorld() {
World world = Bukkit.getWorld(this.worldName);
if (world == null) throw new IllegalStateException("World '" + this.worldName + "' is not loaded");
return world;
}
/**
* Get the size of this Cuboid along the X axis
*
* @return Size of Cuboid along the X axis
*/
public int getSizeX() {
return (this.x2 - this.x1) + 1;
}
/**
* Get the size of this Cuboid along the Y axis
*
* @return Size of Cuboid along the Y axis
*/
public int getSizeY() {
return (this.y2 - this.y1) + 1;
}
/**
* Get the size of this Cuboid along the Z axis
*
* @return Size of Cuboid along the Z axis
*/
public int getSizeZ() {
return (this.z2 - this.z1) + 1;
}
/**
* Get the minimum X co-ordinate of this Cuboid
*
* @return the minimum X co-ordinate
*/
public int getLowerX() {
return this.x1;
}
/**
* Get the minimum Y co-ordinate of this Cuboid
*
* @return the minimum Y co-ordinate
*/
public int getLowerY() {
return this.y1;
}
/**
* Get the minimum Z co-ordinate of this Cuboid
*
* @return the minimum Z co-ordinate
*/
public int getLowerZ() {
return this.z1;
}
/**
* Get the maximum X co-ordinate of this Cuboid
*
* @return the maximum X co-ordinate
*/
public int getUpperX() {
return this.x2;
}
/**
* Get the maximum Y co-ordinate of this Cuboid
*
* @return the maximum Y co-ordinate
*/
public int getUpperY() {
return this.y2;
}
/**
* Get the maximum Z co-ordinate of this Cuboid
*
* @return the maximum Z co-ordinate
*/
public int getUpperZ() {
return this.z2;
}
/**
* Get the Blocks at the eight corners of the Cuboid.
*
* @return array of Block objects representing the Cuboid corners
*/
public Block[] corners() {
Block[] res = new Block[8];
World w = this.getWorld();
res[0] = w.getBlockAt(this.x1, this.y1, this.z1);
res[1] = w.getBlockAt(this.x1, this.y1, this.z2);
res[2] = w.getBlockAt(this.x1, this.y2, this.z1);
res[3] = w.getBlockAt(this.x1, this.y2, this.z2);
res[4] = w.getBlockAt(this.x2, this.y1, this.z1);
res[5] = w.getBlockAt(this.x2, this.y1, this.z2);
res[6] = w.getBlockAt(this.x2, this.y2, this.z1);
res[7] = w.getBlockAt(this.x2, this.y2, this.z2);
return res;
}
/**
* Expand the Cuboid in the given direction by the given amount. Negative amounts will shrink the Cuboid in the given direction. Shrinking a cuboid's face past the opposite face is not an error and will return a valid Cuboid.
*
* @param dir - The direction in which to expand
* @param amount - The number of blocks by which to expand
* @return A new Cuboid expanded by the given direction and amount
*/
public Cuboid expand(CuboidDirection dir, int amount) {
switch (dir) {
case North:
return new Cuboid(this.worldName, this.x1 - amount, this.y1, this.z1, this.x2, this.y2, this.z2);
case South:
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, this.x2 + amount, this.y2, this.z2);
case East:
return new Cuboid(this.worldName, this.x1, this.y1, this.z1 - amount, this.x2, this.y2, this.z2);
case West:
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, this.x2, this.y2, this.z2 + amount);
case Down:
return new Cuboid(this.worldName, this.x1, this.y1 - amount, this.z1, this.x2, this.y2, this.z2);
case Up:
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, this.x2, this.y2 + amount, this.z2);
default:
throw new IllegalArgumentException("Invalid direction " + dir);
}
}
/**
* Shift the Cuboid in the given direction by the given amount.
*
* @param dir - The direction in which to shift
* @param amount - The number of blocks by which to shift
* @return A new Cuboid shifted by the given direction and amount
*/
public Cuboid shift(CuboidDirection dir, int amount) {
return expand(dir, amount).expand(dir.opposite(), -amount);
}
/**
* Outset (grow) the Cuboid in the given direction by the given amount.
*
* @param dir - The direction in which to outset (must be Horizontal, Vertical, or Both)
* @param amount - The number of blocks by which to outset
* @return A new Cuboid outset by the given direction and amount
*/
public Cuboid outset(CuboidDirection dir, int amount) {
Cuboid c;
switch (dir) {
case Horizontal:
c = expand(CuboidDirection.North, amount).expand(CuboidDirection.South, amount).expand(CuboidDirection.East, amount).expand(CuboidDirection.West, amount);
break;
case Vertical:
c = expand(CuboidDirection.Down, amount).expand(CuboidDirection.Up, amount);
break;
case Both:
c = outset(CuboidDirection.Horizontal, amount).outset(CuboidDirection.Vertical, amount);
break;
default:
throw new IllegalArgumentException("Invalid direction " + dir);
}
return c;
}
/**
* Inset (shrink) the Cuboid in the given direction by the given amount. Equivalent
* to calling outset() with a negative amount.
*
* @param dir - The direction in which to inset (must be Horizontal, Vertical, or Both)
* @param amount - The number of blocks by which to inset
* @return A new Cuboid inset by the given direction and amount
*/
public Cuboid inset(CuboidDirection dir, int amount) {
return this.outset(dir, -amount);
}
/**
* Return true if the point at (x,y,z) is contained within this Cuboid.
*
* @param x - The X co-ordinate
* @param y - The Y co-ordinate
* @param z - The Z co-ordinate
* @return true if the given point is within this Cuboid, false otherwise
*/
public boolean contains(int x, int y, int z) {
return x >= this.x1 && x <= this.x2 && y >= this.y1 && y <= this.y2 && z >= this.z1 && z <= this.z2;
}
/**
* Check if the given Block is contained within this Cuboid.
*
* @param b - The Block to check for
* @return true if the Block is within this Cuboid, false otherwise
*/
public boolean contains(Block b) {
return this.contains(b.getLocation());
}
/**
* Check if the given Location is contained within this Cuboid.
*
* @param l - The Location to check for
* @return true if the Location is within this Cuboid, false otherwise
*/
public boolean contains(Location l) {
if (!this.worldName.equals(l.getWorld().getName())) return false;
return this.contains(l.getBlockX(), l.getBlockY(), l.getBlockZ());
}
/**
* Get the volume of this Cuboid.
*
* @return The Cuboid volume, in blocks
*/
public int getVolume() {
return this.getSizeX() * this.getSizeY() * this.getSizeZ();
}
/**
* Get the average light level of all empty (air) blocks in the Cuboid. Returns 0 if there are no empty blocks.
*
* @return The average light level of this Cuboid
*/
public byte getAverageLightLevel() {
long total = 0;
int n = 0;
for (Block b : this) {
if (b.isEmpty()) {
total += b.getLightLevel();
++n;
}
}
return n > 0 ? (byte) (total / n) : 0;
}
/**
* Contract the Cuboid, returning a Cuboid with any air around the edges removed, just large enough to include all non-air blocks.
*
* @return A new Cuboid with no external air blocks
*/
public Cuboid contract() {
return this.contract(CuboidDirection.Down).contract(CuboidDirection.South).contract(CuboidDirection.East).contract(CuboidDirection.Up).contract(CuboidDirection.North).contract(CuboidDirection.West);
}
/**
* Contract the Cuboid in the given direction, returning a new Cuboid which has no exterior empty space.
* E.g. A direction of Down will push the top face downwards as much as possible.
*
* @param dir - The direction in which to contract
* @return A new Cuboid contracted in the given direction
*/
public Cuboid contract(CuboidDirection dir) {
Cuboid face = getFace(dir.opposite());
switch (dir) {
case Down:
while (face.containsOnly(0) && face.getLowerY() > this.getLowerY()) {
face = face.shift(CuboidDirection.Down, 1);
}
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, this.x2, face.getUpperY(), this.z2);
case Up:
while (face.containsOnly(0) && face.getUpperY() < this.getUpperY()) {
face = face.shift(CuboidDirection.Up, 1);
}
return new Cuboid(this.worldName, this.x1, face.getLowerY(), this.z1, this.x2, this.y2, this.z2);
case North:
while (face.containsOnly(0) && face.getLowerX() > this.getLowerX()) {
face = face.shift(CuboidDirection.North, 1);
}
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, face.getUpperX(), this.y2, this.z2);
case South:
while (face.containsOnly(0) && face.getUpperX() < this.getUpperX()) {
face = face.shift(CuboidDirection.South, 1);
}
return new Cuboid(this.worldName, face.getLowerX(), this.y1, this.z1, this.x2, this.y2, this.z2);
case East:
while (face.containsOnly(0) && face.getLowerZ() > this.getLowerZ()) {
face = face.shift(CuboidDirection.East, 1);
}
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, this.x2, this.y2, face.getUpperZ());
case West:
while (face.containsOnly(0) && face.getUpperZ() < this.getUpperZ()) {
face = face.shift(CuboidDirection.West, 1);
}
return new Cuboid(this.worldName, this.x1, this.y1, face.getLowerZ(), this.x2, this.y2, this.z2);
default:
throw new IllegalArgumentException("Invalid direction " + dir);
}
}
/**
* Get the Cuboid representing the face of this Cuboid. The resulting Cuboid will be one block thick in the axis perpendicular to the requested face.
*
* @param dir - which face of the Cuboid to get
* @return The Cuboid representing this Cuboid's requested face
*/
public Cuboid getFace(CuboidDirection dir) {
switch (dir) {
case Down:
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, this.x2, this.y1, this.z2);
case Up:
return new Cuboid(this.worldName, this.x1, this.y2, this.z1, this.x2, this.y2, this.z2);
case North:
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, this.x1, this.y2, this.z2);
case South:
return new Cuboid(this.worldName, this.x2, this.y1, this.z1, this.x2, this.y2, this.z2);
case East:
return new Cuboid(this.worldName, this.x1, this.y1, this.z1, this.x2, this.y2, this.z1);
case West:
return new Cuboid(this.worldName, this.x1, this.y1, this.z2, this.x2, this.y2, this.z2);
default:
throw new IllegalArgumentException("Invalid direction " + dir);
}
}
/**
* Check if the Cuboid contains only blocks of the given type
*
* @param blockId - The block ID to check for
* @return true if this Cuboid contains only blocks of the given type
*/
@SuppressWarnings("deprecation")
public boolean containsOnly(int blockId) {
for (Block b : this) {
if (b.getTypeId() != blockId) return false;
}
return true;
}
/**
* Get the Cuboid big enough to hold both this Cuboid and the given one.
*
* @param other - The other cuboid.
* @return A new Cuboid large enough to hold this Cuboid and the given Cuboid
*/
public Cuboid getBoundingCuboid(Cuboid other) {
if (other == null) return this;
int xMin = Math.min(this.getLowerX(), other.getLowerX());
int yMin = Math.min(this.getLowerY(), other.getLowerY());
int zMin = Math.min(this.getLowerZ(), other.getLowerZ());
int xMax = Math.max(this.getUpperX(), other.getUpperX());
int yMax = Math.max(this.getUpperY(), other.getUpperY());
int zMax = Math.max(this.getUpperZ(), other.getUpperZ());
return new Cuboid(this.worldName, xMin, yMin, zMin, xMax, yMax, zMax);
}
/**
* Get a block relative to the lower NE point of the Cuboid.
*
* @param x - The X co-ordinate
* @param y - The Y co-ordinate
* @param z - The Z co-ordinate
* @return The block at the given position
*/
public Block getRelativeBlock(int x, int y, int z) {
return this.getWorld().getBlockAt(this.x1 + x, this.y1 + y, this.z1 + z);
}
/**
* Get a block relative to the lower NE point of the Cuboid in the given World. This
* version of getRelativeBlock() should be used if being called many times, to avoid
* excessive calls to getWorld().
*
* @param w - The world
* @param x - The X co-ordinate
* @param y - The Y co-ordinate
* @param z - The Z co-ordinate
* @return The block at the given position
*/
public Block getRelativeBlock(World w, int x, int y, int z) {
return w.getBlockAt(this.x1 + x, y1 + y, this.z1 + z);
}
/**
* Get a list of the chunks which are fully or partially contained in this cuboid.
*
* @return A list of Chunk objects
*/
public List<Chunk> getChunks() {
List<Chunk> res = new ArrayList<Chunk>();
World w = this.getWorld();
int x1 = this.getLowerX() & ~0xf;
int x2 = this.getUpperX() & ~0xf;
int z1 = this.getLowerZ() & ~0xf;
int z2 = this.getUpperZ() & ~0xf;
for (int x = x1; x <= x2; x += 16) {
for (int z = z1; z <= z2; z += 16) {
res.add(w.getChunkAt(x >> 4, z >> 4));
}
}
return res;
}
public Iterator<Block> iterator() {
return new CuboidIterator(this.getWorld(), this.x1, this.y1, this.z1, this.x2, this.y2, this.z2);
}
@Override
public Cuboid clone() {
return new Cuboid(this);
}
@Override
public String toString() {
return new String("Cuboid: " + this.worldName + "," + this.x1 + "," + this.y1 + "," + this.z1 + "=>" + this.x2 + "," + this.y2 + "," + this.z2);
}
public class CuboidIterator implements Iterator<Block> {
private World w;
private int baseX, baseY, baseZ;
private int x, y, z;
private int sizeX, sizeY, sizeZ;
public CuboidIterator(World w, int x1, int y1, int z1, int x2, int y2, int z2) {
this.w = w;
this.baseX = x1;
this.baseY = y1;
this.baseZ = z1;
this.sizeX = Math.abs(x2 - x1) + 1;
this.sizeY = Math.abs(y2 - y1) + 1;
this.sizeZ = Math.abs(z2 - z1) + 1;
this.x = this.y = this.z = 0;
}
public boolean hasNext() {
return this.x < this.sizeX && this.y < this.sizeY && this.z < this.sizeZ;
}
public Block next() {
Block b = this.w.getBlockAt(this.baseX + this.x, this.baseY + this.y, this.baseZ + this.z);
if (++x >= this.sizeX) {
this.x = 0;
if (++this.y >= this.sizeY) {
this.y = 0;
++this.z;
}
}
return b;
}
public void remove() {
}
}
public enum CuboidDirection {
North, East, South, West, Up, Down, Horizontal, Vertical, Both, Unknown;
public CuboidDirection opposite() {
switch (this) {
case North:
return South;
case East:
return West;
case South:
return North;
case West:
return East;
case Horizontal:
return Vertical;
case Vertical:
return Horizontal;
case Up:
return Down;
case Down:
return Up;
case Both:
return Both;
default:
return Unknown;
}
}
}
}
Copy link

ghost commented Oct 14, 2022

Excellent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment