From 210c791af475104b11da9486b867b5a9f0ecc48b Mon Sep 17 00:00:00 2001 From: SenseiKiwi Date: Sat, 27 Jul 2013 06:52:30 -0400 Subject: [PATCH] Created Schematic Class Separated part of the dungeon-exporting logic into a new class: Schematic. Also created a few classes to implement important operations. Some parts of the original exporting logic are missing now, such as mapping certain blocks to standard IDs. That will be reimplemented in the future. Importing schematics is not supported yet. I need to test what's done so far. --- .../mod_pocketDim/helpers/DungeonHelper.java | 154 +-------------- .../schematic/CompactBoundsOperation.java | 73 +++++++ .../mod_pocketDim/schematic/Schematic.java | 183 ++++++++++++++++++ .../schematic/WorldCopyOperation.java | 75 +++++++ .../schematic/WorldOperation.java | 64 ++++++ 5 files changed, 400 insertions(+), 149 deletions(-) create mode 100644 StevenDimDoors/mod_pocketDim/schematic/CompactBoundsOperation.java create mode 100644 StevenDimDoors/mod_pocketDim/schematic/Schematic.java create mode 100644 StevenDimDoors/mod_pocketDim/schematic/WorldCopyOperation.java create mode 100644 StevenDimDoors/mod_pocketDim/schematic/WorldOperation.java diff --git a/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java b/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java index 268fbd2d..f9cfa8df 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 00000000..9b3ad881 --- /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 00000000..6e8b7806 --- /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 00000000..d0c53d20 --- /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 00000000..3d6f7fae --- /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; + } +}