Overhauled Schematic Importing and Exporting #56

Merged
SenseiKiwi merged 8 commits from master into master 2013-07-30 22:29:38 +00:00
5 changed files with 400 additions and 149 deletions
Showing only changes of commit 210c791af4 - Show all commits

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}