diff --git a/src/main/java/com/dfsek/terra/Range.java b/src/main/java/com/dfsek/terra/Range.java index 455debd83..6c648b4f7 100644 --- a/src/main/java/com/dfsek/terra/Range.java +++ b/src/main/java/com/dfsek/terra/Range.java @@ -26,6 +26,30 @@ public class Range implements Iterable { return min; } + public Range setMax(int max) { + this.max = max; + return this; + } + + public int getRange() { + return max-min; + } + + public Range multiply(int mult) { + min*=mult; + max*=mult; + return this; + } + + public Range reflect(int pt) { + return new Range(2*pt-this.getMax(), 2*pt-this.getMin()); + } + + public Range setMin(int min) { + this.min = min; + return this; + } + public int get(Random r) { return r.nextInt((max-min)+1)+min; } diff --git a/src/main/java/com/dfsek/terra/TerraCommand.java b/src/main/java/com/dfsek/terra/TerraCommand.java index 80b3fa25d..78a61bdc6 100644 --- a/src/main/java/com/dfsek/terra/TerraCommand.java +++ b/src/main/java/com/dfsek/terra/TerraCommand.java @@ -179,9 +179,10 @@ public class TerraCommand implements CommandExecutor, TabExecutor { return true; } else if("load".equals(args[1])) { try { + GaeaStructure.Rotation r = GaeaStructure.Rotation.fromDegrees(Integer.parseInt(args[3])); GaeaStructure struc = GaeaStructure.load(new File(Terra.getInstance().getDataFolder() + File.separator + "export" + File.separator + "structures", args[2] + ".tstructure")); - if("true".equals(args[3])) struc.paste(pl.getLocation()); - else struc.paste(pl.getLocation(), pl.getLocation().getChunk()); + if("true".equals(args[4])) struc.paste(pl.getLocation(), r, Collections.emptyList()); + else struc.paste(pl.getLocation(), pl.getLocation().getChunk(), r, Collections.emptyList()); } catch(IOException e) { e.printStackTrace(); sender.sendMessage("Structure not found."); diff --git a/src/main/java/com/dfsek/terra/population/StructurePopulator.java b/src/main/java/com/dfsek/terra/population/StructurePopulator.java index 49160d71b..c28cadf06 100644 --- a/src/main/java/com/dfsek/terra/population/StructurePopulator.java +++ b/src/main/java/com/dfsek/terra/population/StructurePopulator.java @@ -15,13 +15,15 @@ import org.polydev.gaea.profiler.ProfileFuture; import java.io.File; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Random; import java.util.Set; public class StructurePopulator extends BlockPopulator { - StructureSpawn spawnTest = new StructureSpawn(250, 250); - GaeaStructure struc = GaeaStructure.load(new File(Terra.getInstance().getDataFolder() + File.separator + "export" + File.separator + "structures", "demo2.tstructure")); + StructureSpawn spawnTest = new StructureSpawn(75, 25); + GaeaStructure struc = GaeaStructure.load(new File(Terra.getInstance().getDataFolder() + File.separator + "export" + File.separator + "structures", "desert.tstructure")); double horizontal = struc.getStructureInfo().getMaxHorizontal(); public StructurePopulator() throws IOException { @@ -36,9 +38,8 @@ public class StructurePopulator extends BlockPopulator { spawn.setY(72); if(Math.abs((cx+8)-spawn.getBlockX()) <= horizontal && Math.abs((cz+8)-spawn.getBlockZ()) <= horizontal) { try(ProfileFuture ignore = TerraProfiler.fromWorld(world).measure("StructurePasteTime")) { - struc.paste(spawn, chunk); + struc.paste(spawn, chunk, GaeaStructure.Rotation.fromDegrees(random.nextInt(4)*90), Collections.emptyList()); } - Bukkit.getLogger().info("Pasted at " + spawn); } } } diff --git a/src/main/java/com/dfsek/terra/structure/GaeaStructure.java b/src/main/java/com/dfsek/terra/structure/GaeaStructure.java index 17c10f49e..701fa96af 100644 --- a/src/main/java/com/dfsek/terra/structure/GaeaStructure.java +++ b/src/main/java/com/dfsek/terra/structure/GaeaStructure.java @@ -6,9 +6,14 @@ import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.block.Sign; import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.Rotatable; +import org.bukkit.block.data.type.Chest; +import org.bukkit.block.data.type.Stairs; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -19,6 +24,8 @@ import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.function.Consumer; @@ -44,10 +51,10 @@ public class GaeaStructure implements Serializable { this.id = id; this.uuid = UUID.randomUUID(); if(l1.getX() > l2.getX() || l1.getY() > l2.getY() || l1.getZ() > l2.getZ()) throw new IllegalArgumentException("Invalid locations provided!"); - structure = new StructureContainedBlock[l2.getBlockX()-l1.getBlockX()+1][l2.getBlockY()-l1.getBlockY()+1][l2.getBlockZ()-l1.getBlockZ()+1]; + structure = new StructureContainedBlock[l2.getBlockX()-l1.getBlockX()+1][l2.getBlockZ()-l1.getBlockZ()+1][l2.getBlockY()-l1.getBlockY()+1]; for(int x = 0; x <= l2.getBlockX()-l1.getBlockX(); x++) { - for(int y = 0; y <= l2.getBlockY()-l1.getBlockY(); y++) { - for(int z = 0; z <= l2.getBlockZ()-l1.getBlockZ(); z++) { + for(int z = 0; z <= l2.getBlockZ()-l1.getBlockZ(); z++) { + for(int y = 0; y <= l2.getBlockY()-l1.getBlockY(); y++) { Block b = Objects.requireNonNull(l1.getWorld()).getBlockAt(l1.clone().add(x, y, z)); BlockState state = b.getState(); BlockData d = b.getBlockData(); @@ -67,7 +74,7 @@ public class GaeaStructure implements Serializable { } } } - structure[x][y][z] = new StructureContainedBlock(x, y, z, useState ? state : null, d); + structure[x][z][y] = new StructureContainedBlock(x, y, z, useState ? state : null, d); } } } @@ -80,46 +87,229 @@ public class GaeaStructure implements Serializable { return structureInfo; } - public void paste(@NotNull Location origin) { - this.executeForBlocksInRange(getRange(Axis.X), getRange(Axis.Y), getRange(Axis.Z), block -> pasteBlock(block, origin)); + public void paste(@NotNull Location origin, Rotation r, List m) { + this.executeForBlocksInRange(getRange(Axis.X), getRange(Axis.Y), getRange(Axis.Z), block -> pasteBlock(block, origin, r, m), r, m); } - public void paste(Location origin, Chunk chunk) { - int xOr = (chunk.getX() << 4); - int zOr = (chunk.getZ() << 4); - Range intersectX = new Range(xOr, xOr+16).sub(origin.getBlockX() - structureInfo.getCenterX()); - Range intersectZ = new Range(zOr, zOr+16).sub(origin.getBlockZ() - structureInfo.getCenterZ()); - if(intersectX == null || intersectZ == null) return; - executeForBlocksInRange(intersectX, getRange(Axis.Y), intersectZ, block -> pasteBlock(block, origin)); + private int[] getRotatedCoords(int[] arr, Rotation r, List m) { + int[] cp = Arrays.copyOf(arr, 2); + switch(r) { + case CW_90: + arr[0] = cp[1]; + arr[1] = - cp[0]; + break; + case CCW_90: + arr[0] = - cp[1]; + arr[1] = cp[0]; + break; + case CW_180: + arr[0] = - cp[0]; + arr[1] = - cp[1]; + break; + } + if(m.contains(Mirror.X)) arr[0] = - arr[0]; + if(m.contains(Mirror.Z)) arr[1] = - arr[1]; + return arr; } - private void pasteBlock(StructureContainedBlock block, Location origin) { - BlockData data = block.getBlockData(); - Block worldBlock = origin.clone().add(block.getX()-structureInfo.getCenterX(), block.getY(), block.getZ()-structureInfo.getCenterZ()).getBlock(); - if(!data.getMaterial().equals(Material.STRUCTURE_VOID)) worldBlock.setBlockData(data, false); - if(block.getState() != null) { - block.getState().getState(worldBlock.getState()).update(); + /** + * Inverse of {@link GaeaStructure#getRotatedCoords(int[], Rotation, List)}, gets the coordinates before rotation + * @param arr 2-integer array holding X and Z coordinates + * @param r Rotation + * @param m Mirror + * @return New coordinates + */ + private int[] getOriginalCoords(int[] arr, Rotation r, List m) { + int[] cp = Arrays.copyOf(arr, 2); + switch(r) { + case CW_90: + arr[1] = cp[0]; + arr[0] = - cp[1]; + break; + case CCW_90: + arr[1] = - cp[0]; + arr[0] = cp[1]; + break; + case CW_180: + arr[1] = - cp[1]; + arr[0] = - cp[0]; + break; + } + if(m.contains(Mirror.X)) arr[0] = - arr[0]; + if(m.contains(Mirror.Z)) arr[1] = - arr[1]; + return arr; + } + + /** + * Get the BlockFace with rotation and mirrors applied to it + * @param f BlockFace to apply rotation to + * @param r Rotation + * @param m Mirror + * @return Rotated BlockFace + */ + private BlockFace getRotatedFace(BlockFace f, Rotation r, List m) { + BlockFace n = f; + int rotateNum = r.getDegrees()/90; + int rn = faceRotation(f); + if(rn >= 0) { + n = fromRotation(faceRotation(n) - 4*rotateNum); + } + if(f.getModX()!=0 && m.contains(Mirror.X)) n = n.getOppositeFace(); + if(f.getModZ()!=0 && m.contains(Mirror.Z)) n = n.getOppositeFace(); + return n; + } + + /** + * Get an integer representation of a BlockFace, to perform math on. + * @param f BlockFace to get integer for + * @return integer representation of BlockFace + */ + private static int faceRotation(BlockFace f) { + switch(f) { + case NORTH: return 0; + case NORTH_NORTH_EAST: return 1; + case NORTH_EAST: return 2; + case EAST_NORTH_EAST: return 3; + case EAST: return 4; + case EAST_SOUTH_EAST: return 5; + case SOUTH_EAST: return 6; + case SOUTH_SOUTH_EAST: return 7; + case SOUTH: return 8; + case SOUTH_SOUTH_WEST: return 9; + case SOUTH_WEST: return 10; + case WEST_SOUTH_WEST: return 11; + case WEST: return 12; + case WEST_NORTH_WEST: return 13; + case NORTH_WEST: return 14; + case NORTH_NORTH_WEST: return 15; + default: return -1; } } - private void executeForBlocksInRange(Range xM, Range yM, Range zM, Consumer exec) { + /** + * Convert integer to BlockFace representation + * @param r integer to get BlockFace for + * @return BlockFace represented by integer. + */ + private static BlockFace fromRotation(int r) { + switch(Math.floorMod(r, 16)) { + case 0: return BlockFace.NORTH; + case 1: return BlockFace.NORTH_NORTH_EAST; + case 2: return BlockFace.NORTH_EAST; + case 3: return BlockFace.EAST_NORTH_EAST; + case 4: return BlockFace.EAST; + case 5: return BlockFace.EAST_SOUTH_EAST; + case 6: return BlockFace.SOUTH_EAST; + case 7: return BlockFace.SOUTH_SOUTH_EAST; + case 8: return BlockFace.SOUTH; + case 9: return BlockFace.SOUTH_SOUTH_WEST; + case 10: return BlockFace.SOUTH_WEST; + case 11: return BlockFace.WEST_SOUTH_WEST; + case 12: return BlockFace.WEST; + case 13: return BlockFace.WEST_NORTH_WEST; + case 14: return BlockFace.NORTH_WEST; + case 15: return BlockFace.NORTH_NORTH_WEST; + default: throw new IllegalArgumentException(); + } + } + + + /** + * Paste structure at an origin location, confined to a single chunk. + * @param origin Origin location + * @param chunk Chunk to confine pasting to + * @param r Rotation + * @param m Mirror + */ + public void paste(Location origin, Chunk chunk, Rotation r, List m) { + int xOr = (chunk.getX() << 4); + int zOr = (chunk.getZ() << 4); + Range intersectX; + Range intersectZ; + intersectX = new Range(xOr, xOr+16).sub(origin.getBlockX() - structureInfo.getCenterX()); + intersectZ = new Range(zOr, zOr+16).sub(origin.getBlockZ() - structureInfo.getCenterZ()); + if(intersectX == null || intersectZ == null) return; + executeForBlocksInRange(intersectX, getRange(Axis.Y), intersectZ, block -> pasteBlock(block, origin, r, m), r, m); + Bukkit.getLogger().info(intersectX.toString() + " : " + intersectZ.toString()); + } + + /** + * Paste a single StructureDefinedBlock at an origin location, offset by its coordinates. + * @param block The block to paste + * @param origin The origin location + * @param r The rotation of the structure + * @param m The mirror type of the structure + */ + private void pasteBlock(StructureContainedBlock block, Location origin, Rotation r, List m) { + BlockData data = block.getBlockData().clone(); + Block worldBlock = origin.clone().add(block.getX(), block.getY(), block.getZ()).getBlock(); + if(!data.getMaterial().equals(Material.STRUCTURE_VOID)) { + if(data instanceof Rotatable) { + BlockFace rt = getRotatedFace(((Rotatable) data).getRotation(), r, m); + Bukkit.getLogger().info("Previous: " + ((Rotatable) data).getRotation() + ", New: " + rt); + ((Rotatable) data).setRotation(rt); + } else if(data instanceof Directional) { + BlockFace rt = getRotatedFace(((Directional) data).getFacing(), r, m); + Bukkit.getLogger().info("Previous: " + ((Directional) data).getFacing() + ", New: " + rt); + ((Directional) data).setFacing(rt); + } + worldBlock.setBlockData(data, false); + if(block.getState() != null) { + block.getState().getState(worldBlock.getState()).update(); + } + } + } + + /** + * Execute a Consumer for all blocks in a cuboid region defined by 3 Ranges, accounting for rotation. + * @param xM X Range + * @param yM Y Range + * @param zM Z Range + * @param exec Consumer to execute for each block. + * @param r Rotation + * @param m Mirror + */ + private void executeForBlocksInRange(Range xM, Range yM, Range zM, Consumer exec, Rotation r, List m) { for(int x : xM) { for(int y : yM) { for(int z : zM) { - if(isInStructure(x, y, z)) exec.accept(structure[x][y][z]); + int[] c = getOriginalCoords(new int[] {structure[x][z][y].getX()-structureInfo.getCenterX(), structure[x][z][y].getZ()-structureInfo.getCenterZ()}, r, m); + if(isInStructure(c[0], y, c[1])) { + StructureContainedBlock b = structure[c[0]][c[1]][y]; + exec.accept(new StructureContainedBlock(x, y, z, b.getState(), b.getBlockData())); + } } } } } + /** + * Test whether a set of coordinates is within the current structure + * @param x X coordinate + * @param y Y coordinate + * @param z Z coordinate + * @return True if coordinate set is in structure, false if it is not. + */ private boolean isInStructure(int x, int y, int z) { return x < structureInfo.getSizeX() && y < structureInfo.getSizeY() && z < structureInfo.getSizeZ() && x >= 0 && y >= 0 && z >= 0; } + /** + * Save the structure to a file + * @param f File to save to + * @throws IOException If file access error occurs + */ public void save(@NotNull File f) throws IOException { toFile(this, f); } + /** + * Load a structure from a file. + * @param f File to load from + * @return The structure loaded + * @throws IOException If file access error occurs + * @throws ClassNotFoundException If structure data is invalid. + */ @NotNull private static GaeaStructure fromFile(@NotNull File f) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f)); @@ -164,7 +354,30 @@ public class GaeaStructure implements Serializable { } } - enum Axis { + public enum Axis { X, Y, Z } + public enum Rotation { + CW_90(90), CW_180(180), CCW_90(270), NONE(0); + private final int degrees; + Rotation(int degrees) { + this.degrees = degrees; + } + + public int getDegrees() { + return degrees; + } + public static Rotation fromDegrees(int deg) { + switch(Math.floorMod(deg, 360)) { + case 0: return Rotation.NONE; + case 90: return Rotation.CW_90; + case 180: return Rotation.CW_180; + case 270: return Rotation.CCW_90; + default: throw new IllegalArgumentException(); + } + } + } + public enum Mirror { + NONE, X, Z + } } diff --git a/src/main/java/com/dfsek/terra/structure/StructureContainedBlock.java b/src/main/java/com/dfsek/terra/structure/StructureContainedBlock.java index 4cda779d5..35d31e69c 100644 --- a/src/main/java/com/dfsek/terra/structure/StructureContainedBlock.java +++ b/src/main/java/com/dfsek/terra/structure/StructureContainedBlock.java @@ -27,6 +27,15 @@ public class StructureContainedBlock implements Serializable { this.z = z; this.bl = new SerializableBlockData(d); } + public StructureContainedBlock(int x, int y, int z, SerializableBlockState state, BlockData d) { + if(state instanceof SerializableSign) { + this.state = state; + } else this.state = null; + this.x = x; + this.y = y; + this.z = z; + this.bl = new SerializableBlockData(d); + } public int getX() { return x; diff --git a/src/main/resources/terra.commodore b/src/main/resources/terra.commodore index 06752a308..6482f7d60 100644 --- a/src/main/resources/terra.commodore +++ b/src/main/resources/terra.commodore @@ -30,7 +30,9 @@ terra { } load { structure brigadier:string single_word { - respect_chunk brigadier:bool; + rotation brigadier:integer { + respect_chunk brigadier:bool; + } } } getspawn; diff --git a/src/test/java/RangeTest.java b/src/test/java/RangeTest.java index 36955cfeb..7ebf6064f 100644 --- a/src/test/java/RangeTest.java +++ b/src/test/java/RangeTest.java @@ -26,4 +26,11 @@ public class RangeTest { one = new Range(25, 50); assertNull(one.intersects(two)); } + @Test + public void reflect() { + Range t = new Range(3, 10); + Range other = t.reflect(5); + assertEquals(7, other.getMax()); + assertEquals(0, other.getMin()); + } }