diff --git a/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java b/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java index 268fbd2..f9cfa8d 100644 --- a/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java +++ b/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java @@ -1,7 +1,6 @@ package StevenDimDoors.mod_pocketDim.helpers; import java.io.File; -import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -11,10 +10,6 @@ import java.util.Random; import java.util.regex.Pattern; import net.minecraft.block.Block; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.tileentity.TileEntity; import net.minecraft.util.WeightedRandom; import net.minecraft.world.World; import StevenDimDoors.mod_pocketDim.DDProperties; @@ -23,6 +18,7 @@ import StevenDimDoors.mod_pocketDim.DungeonGenerator; import StevenDimDoors.mod_pocketDim.LinkData; import StevenDimDoors.mod_pocketDim.mod_pocketDim; import StevenDimDoors.mod_pocketDim.items.itemDimDoor; +import StevenDimDoors.mod_pocketDim.schematic.Schematic; import StevenDimDoors.mod_pocketDim.util.WeightedContainer; public class DungeonHelper @@ -383,153 +379,13 @@ public class DungeonHelper public boolean exportDungeon(World world, int centerX, int centerY, int centerZ, String exportPath) { - int xMin, yMin, zMin; - int xMax, yMax, zMax; - int xStart, yStart, zStart; - int xEnd, yEnd, zEnd; - - //Find the smallest bounding box that contains all non-air blocks within a max radius around the player. - xMax = yMax = zMax = Integer.MIN_VALUE; - xMin = yMin = zMin = Integer.MAX_VALUE; - - xStart = centerX - MAX_EXPORT_RADIUS; - zStart = centerZ - MAX_EXPORT_RADIUS; - yStart = Math.max(centerY - MAX_EXPORT_RADIUS, 0); - - xEnd = centerX + MAX_EXPORT_RADIUS; - zEnd = centerZ + MAX_EXPORT_RADIUS; - yEnd = Math.min(centerY + MAX_EXPORT_RADIUS, world.getHeight()); - - //This could be done more efficiently, but honestly, this is the simplest approach and it - //makes it easy for us to verify that the code is correct. - for (int y = yStart; y <= yEnd; y++) - { - for (int z = zStart; z <= zEnd; z++) - { - for (int x = xStart; x <= xEnd; x++) - { - if (!world.isAirBlock(x, y, z)) - { - xMax = x > xMax ? x : xMax; - zMax = z > zMax ? z : zMax; - yMax = y > yMax ? y : yMax; - - xMin = x < xMin ? x : xMin; - zMin = z < zMin ? z : zMin; - yMin = y < yMin ? y : yMin; - } - } - } - } - - //Export all the blocks within our selected bounding box - short width = (short) (xMax - xMin + 1); - short height = (short) (yMax - yMin + 1); - short length = (short) (zMax - zMin + 1); - - byte[] blocks = new byte[width * height * length]; - byte[] addBlocks = null; - byte[] blockData = new byte[width * height * length]; - NBTTagList tileEntities = new NBTTagList(); - - for (int y = 0; y < height; y++) - { - for (int z = 0; z < length; z++) - { - for (int x = 0; x < width; x++) - { - int index = y * width * length + z * width + x; - int blockID = world.getBlockId(x + xMin, y + yMin, z + zMin); - int metadata = world.getBlockMetadata(x + xMin, y + yMin, z + zMin); - boolean changed = false; - - if (blockID == properties.DimensionalDoorID) - { - blockID = Block.doorIron.blockID; - changed = true; - } - if (blockID == properties.WarpDoorID) - { - blockID = Block.doorWood.blockID; - changed = true; - } - //Map fabric of reality and permafabric blocks to standard export IDs - if (blockID == properties.FabricBlockID) - { - blockID = FABRIC_OF_REALITY_EXPORT_ID; - changed = true; - } - if (blockID == properties.PermaFabricBlockID) - { - blockID = PERMAFABRIC_EXPORT_ID; - changed = true; - } - - // Save 4096 IDs in an AddBlocks section - if (blockID > 255) - { - if (addBlocks == null) - { - //Lazily create section - addBlocks = new byte[(blocks.length >> 1) + 1]; - } - - addBlocks[index >> 1] = (byte) (((index & 1) == 0) ? - addBlocks[index >> 1] & 0xF0 | (blockID >> 8) & 0xF - : addBlocks[index >> 1] & 0xF | ((blockID >> 8) & 0xF) << 4); - } - - blocks[index] = (byte) blockID; - blockData[index] = (byte) metadata; - - //Obtain and export the tile entity of the current block, if any. - //Do not obtain a tile entity if the block was changed from its original ID. - //I'm not sure if this approach is the most efficient but it works. ~SenseiKiwi - TileEntity tileEntity = !changed ? world.getBlockTileEntity(x + xMin, y + yMin, z + zMin) : null; - - if (tileEntity != null) - { - //Get the tile entity's description as a compound NBT tag - NBTTagCompound entityData = new NBTTagCompound(); - tileEntity.writeToNBT(entityData); - //Change the tile entity's location to the schematic coordinate system - entityData.setInteger("x", x); - entityData.setInteger("y", y); - entityData.setInteger("z", z); - - tileEntities.appendTag(entityData); - } - } - } - } - - //Write NBT tags for schematic file - NBTTagCompound schematicTag = new NBTTagCompound("Schematic"); - - schematicTag.setShort("Width", width); - schematicTag.setShort("Length", length); - schematicTag.setShort("Height", height); - - schematicTag.setByteArray("Blocks", blocks); - schematicTag.setByteArray("Data", blockData); - - schematicTag.setTag("Entities", new NBTTagList()); - schematicTag.setTag("TileEntities", tileEntities); - schematicTag.setString("Materials", "Alpha"); - - if (addBlocks != null) - { - schematicTag.setByteArray("AddBlocks", addBlocks); - } - //Write schematic data to a file try { - FileOutputStream outputStream = new FileOutputStream(new File(exportPath)); - CompressedStreamTools.writeCompressed(schematicTag, outputStream); - //writeCompressed() probably closes the stream on its own - call close again just in case. - //Closing twice will not throw an exception. - outputStream.close(); + short size = (short) 2 * MAX_EXPORT_RADIUS + 1; + Schematic schematic = Schematic.copyFromWorld(world, + centerX - MAX_EXPORT_RADIUS, centerY - MAX_EXPORT_RADIUS, centerZ - MAX_EXPORT_RADIUS, size, size, size, true); + schematic.writeToFile(exportPath); return true; } catch(Exception e) diff --git a/StevenDimDoors/mod_pocketDim/schematic/CompactBoundsOperation.java b/StevenDimDoors/mod_pocketDim/schematic/CompactBoundsOperation.java new file mode 100644 index 0000000..9b3ad88 --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/schematic/CompactBoundsOperation.java @@ -0,0 +1,73 @@ +package StevenDimDoors.mod_pocketDim.schematic; + +import net.minecraft.world.World; +import StevenDimDoors.mod_pocketDim.Point3D; + +public class CompactBoundsOperation extends WorldOperation +{ + private int minX; + private int minY; + private int minZ; + private int maxX; + private int maxY; + private int maxZ; + + public CompactBoundsOperation() + { + super("CompactBoundsOperation"); + } + + @Override + protected boolean start(World world, int x, int y, int z, int width, int height, int length) + { + minX = Integer.MAX_VALUE; + minY = Integer.MAX_VALUE; + minZ = Integer.MAX_VALUE; + maxX = x; + maxY = y; + maxZ = z; + return true; + } + + @Override + protected boolean applyToBlock(World world, int x, int y, int z) + { + //This could be done more efficiently, but honestly, this is the simplest approach and it + //makes it easy for us to verify that the code is correct. + if (!world.isAirBlock(x, y, z)) + { + maxX = x > maxX ? x : maxX; + maxZ = z > maxZ ? z : maxZ; + maxY = y > maxY ? y : maxY; + + minX = x < minX ? x : minX; + minZ = z < minZ ? z : minZ; + minY = y < minY ? y : minY; + } + return true; + } + + protected boolean finish() + { + if (minX == Integer.MAX_VALUE) + { + //The whole search space was empty! + //Compact the space to a single block. + minX = maxX; + minY = maxY; + minZ = maxZ; + return false; + } + return true; + } + + public Point3D getMaxCorner() + { + return new Point3D(maxX, maxY, maxZ); + } + + public Point3D getMinCorner() + { + return new Point3D(minX, minY, minZ); + } +} diff --git a/StevenDimDoors/mod_pocketDim/schematic/Schematic.java b/StevenDimDoors/mod_pocketDim/schematic/Schematic.java new file mode 100644 index 0000000..6e8b780 --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/schematic/Schematic.java @@ -0,0 +1,183 @@ +package StevenDimDoors.mod_pocketDim.schematic; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.world.World; +import StevenDimDoors.mod_pocketDim.Point3D; + +public class Schematic { + + protected short width; + protected short height; + protected short length; + + protected short[] blocks; + protected byte[] metadata; + protected NBTTagList tileEntities = new NBTTagList(); + + protected Schematic(short width, short height, short length, short[] blocks, byte[] metadata, NBTTagList tileEntities) + { + this.width = width; + this.height = height; + this.length = length; + this.blocks = blocks; + this.metadata = metadata; + this.tileEntities = tileEntities; + } + + private int calculateIndex(int x, int y, int z) + { + if (x < 0 || x >= width) + throw new IndexOutOfBoundsException("x must be non-negative and less than width"); + if (y < 0 || y >= height) + throw new IndexOutOfBoundsException("y must be non-negative and less than height"); + if (z < 0 || z >= length) + throw new IndexOutOfBoundsException("z must be non-negative and less than length"); + + return (y * width * length + z * width + x); + } + + public short getBlockID(int x, int y, int z) + { + return blocks[calculateIndex(x, y, z)]; + } + + public byte getBlockMetadata(int x, int y, int z) + { + return metadata[calculateIndex(x, y, z)]; + } + + public NBTTagList getTileEntities() + { + return (NBTTagList) tileEntities.copy(); + } + + public static Schematic readFromFile() + { + throw new UnsupportedOperationException(); + } + + public static Schematic copyFromWorld(World world, int x, int y, int z, short width, short height, short length, boolean doCompactBounds) + { + if (doCompactBounds) + { + //Adjust the vertical bounds to reasonable values if necessary + int worldHeight = world.getHeight(); + int fixedY = (y < 0) ? 0 : y; + int fixedHeight = height + y - fixedY; + + if (fixedHeight + fixedY >= worldHeight) + { + fixedHeight = worldHeight - fixedY; + } + + //Compact the area to be copied to remove empty borders + CompactBoundsOperation compactor = new CompactBoundsOperation(); + compactor.apply(world, x, fixedY, z, width, fixedHeight, length); + Point3D minCorner = compactor.getMinCorner(); + Point3D maxCorner = compactor.getMaxCorner(); + + short compactWidth = (short) (maxCorner.getX() - minCorner.getX() + 1); + short compactHeight = (short) (maxCorner.getY() - minCorner.getY() + 1); + short compactLength = (short) (maxCorner.getZ() - minCorner.getZ() + 1); + + return copyFromWorld(world, minCorner.getX(), minCorner.getY(), minCorner.getZ(), + compactWidth, compactHeight, compactLength); + } + else + { + return copyFromWorld(world, x, y, z, width, height, length); + } + } + + private static Schematic copyFromWorld(World world, int x, int y, int z, short width, short height, short length) + { + //Short and sweet ^_^ + WorldCopyOperation copier = new WorldCopyOperation(); + copier.apply(world, x, y, z, width, height, length); + return new Schematic(width, height, length, copier.getBlockIDs(), copier.getMetadata(), copier.getTileEntities()); + } + + private static boolean encodeBlockIDs(short[] blocks, byte[] lowBits, byte[] highBits) + { + int index; + int length = blocks.length - (blocks.length & 1); + boolean hasHighBits = false; + for (index = 0; index < length; index += 2) + { + highBits[index >> 1] = (byte) (((blocks[index] >> 8) & 0x0F) + ((blocks[index + 1] >> 4) & 0xF0)); + hasHighBits |= (highBits[index >> 1] != 0); + } + if (index < blocks.length) + { + highBits[index >> 1] = (byte) ((blocks[index] >> 8) & 0x0F); + hasHighBits |= (highBits[index >> 1] != 0); + } + return hasHighBits; + } + + public NBTTagCompound writeToNBT() + { + return writeToNBT(true); + } + + private NBTTagCompound writeToNBT(boolean copyTileEntities) + { + //This is the main storage function. Schematics are really compressed NBT tags, so if we can generate + //the tags, most of the work is done. All the other storage functions will rely on this one. + + NBTTagCompound schematicTag = new NBTTagCompound("Schematic"); + + schematicTag.setShort("Width", width); + schematicTag.setShort("Length", length); + schematicTag.setShort("Height", height); + + schematicTag.setTag("Entities", new NBTTagList()); + schematicTag.setString("Materials", "Alpha"); + + byte[] lowBytes = new byte[blocks.length]; + byte[] highBytes = new byte[(blocks.length >> 1) + 1]; + boolean hasExtendedIDs = encodeBlockIDs(blocks, lowBytes, highBytes); + + schematicTag.setByteArray("Blocks", lowBytes); + schematicTag.setByteArray("Data", metadata); + + if (hasExtendedIDs) + { + schematicTag.setByteArray("AddBlocks", highBytes); + } + + if (copyTileEntities) + { + //Used when the result of this function will be passed outside this class. + //Avoids exposing the private field to external modifications. + schematicTag.setTag("TileEntities", (NBTTagList) tileEntities.copy()); + } + else + { + //Used when the result of this function is for internal use. + //It's more efficient not to copy the tags unless it's needed. + schematicTag.setTag("TileEntities", tileEntities); + } + return schematicTag; + } + + public void writeToFile(String schematicPath) throws IOException + { + writeToFile(new File(schematicPath)); + } + + public void writeToFile(File schematicFile) throws IOException + { + FileOutputStream outputStream = new FileOutputStream(schematicFile); + CompressedStreamTools.writeCompressed(writeToNBT(false), outputStream); + //writeCompressed() probably closes the stream on its own - call close again just in case. + //Closing twice will not throw an exception. + outputStream.close(); + } +} diff --git a/StevenDimDoors/mod_pocketDim/schematic/WorldCopyOperation.java b/StevenDimDoors/mod_pocketDim/schematic/WorldCopyOperation.java new file mode 100644 index 0000000..d0c53d2 --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/schematic/WorldCopyOperation.java @@ -0,0 +1,75 @@ +package StevenDimDoors.mod_pocketDim.schematic; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; + +public class WorldCopyOperation extends WorldOperation +{ + private int originX; + private int originY; + private int originZ; + private int index; + private short[] blockIDs; + private byte[] metadata; + private NBTTagList tileEntities; + + public WorldCopyOperation() + { + super("WorldCopyOperation"); + blockIDs = null; + metadata = null; + tileEntities = new NBTTagList(); + } + + @Override + protected boolean start(World world, int x, int y, int z, int width, int height, int length) + { + index = 0; + originX = x; + originY = y; + originZ = z; + blockIDs = new short[width * height * length]; + metadata = new byte[width * height * length]; + return true; + } + + @Override + protected boolean applyToBlock(World world, int x, int y, int z) + { + blockIDs[index] = (short) world.getBlockId(x, y, z); + metadata[index] = (byte) world.getBlockMetadata(x, y, z); + + TileEntity tileEntity = world.getBlockTileEntity(x, y, z); + if (tileEntity != null) + { + //Extract tile entity data + NBTTagCompound tileTag = new NBTTagCompound(); + tileEntity.writeToNBT(tileTag); + //Translate the tile entity's position from the world's coordinate system + //to the schematic's coordinate system. + tileTag.setInteger("x", x - originX); + tileTag.setInteger("y", y - originY); + tileTag.setInteger("z", z - originZ); + tileEntities.appendTag(tileTag); + } + index++; //This works assuming the loops in WorldOperation are done in YZX order + return true; + } + + public short[] getBlockIDs() + { + return blockIDs; + } + + public byte[] getMetadata() + { + return metadata; + } + + public NBTTagList getTileEntities() + { + return tileEntities; + } +} diff --git a/StevenDimDoors/mod_pocketDim/schematic/WorldOperation.java b/StevenDimDoors/mod_pocketDim/schematic/WorldOperation.java new file mode 100644 index 0000000..3d6f7fa --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/schematic/WorldOperation.java @@ -0,0 +1,64 @@ +package StevenDimDoors.mod_pocketDim.schematic; + +import net.minecraft.world.World; + +public abstract class WorldOperation { + + private String name; + + public WorldOperation(String name) + { + this.name = name; + } + + protected boolean start(World world, int x, int y, int z, int width, int height, int length) + { + return true; + } + + protected abstract boolean applyToBlock(World world, int x, int y, int z); + + protected boolean finish() + { + return true; + } + + public boolean apply(World world, int x, int y, int z, int width, int height, int length) + { + if (!start(world, x, y, z, width, height, length)) + return false; + + int cx, cy, cz; + int limitX = x + width; + int limitY = y + height; + int limitZ = z + length; + + //The order of these loops is important. Don't change it! It's used to avoid calculating + //indeces in some schematic operations. The proper order is YZX. + for (cy = y; cy < limitY; cy++) + { + for (cz = z; cz < limitZ; cz++) + { + for (cx = x; cx < limitX; cx++) + { + if (!applyToBlock(world, cx, cy, cz)) + return false; + } + } + } + + return finish(); + } + + + public String getName() + { + return name; + } + + @Override + public String toString() + { + return name; + } +}