blob: c0691ab993b28718984c2bbe432803130abee6f3 [file] [log] [blame] [raw]
package net.querz.mca;
import net.querz.nbt.tag.CompoundTag;
import net.querz.nbt.tag.ListTag;
import net.querz.nbt.io.NamedTag;
import net.querz.nbt.io.NBTDeserializer;
import net.querz.nbt.io.NBTSerializer;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import static net.querz.mca.LoadFlags.*;
public class Chunk {
public static final int DEFAULT_DATA_VERSION = 1628;
private int lastMCAUpdate;
private CompoundTag data;
private int dataVersion;
private long lastUpdate;
private long inhabitedTime;
private int[] biomes;
private CompoundTag heightMaps;
private CompoundTag carvingMasks;
private Section[] sections = new Section[16]; //always initialized with size = 16 for fast access
private ListTag<CompoundTag> entities;
private ListTag<CompoundTag> tileEntities;
private ListTag<CompoundTag> tileTicks;
private ListTag<CompoundTag> liquidTicks;
private ListTag<ListTag<?>> lights;
private ListTag<ListTag<?>> liquidsToBeTicked;
private ListTag<ListTag<?>> toBeTicked;
private ListTag<ListTag<?>> postProcessing;
private String status;
private CompoundTag structures;
Chunk(int lastMCAUpdate) {
this.lastMCAUpdate = lastMCAUpdate;
}
/**
* Create a new chunk based on raw base data from a region file.
* @param data The raw base data to be used.
*/
public Chunk(CompoundTag data) {
this.data = data;
initReferences(-1);
}
private void initReferences(long loadFlags) {
if (data == null) {
throw new NullPointerException("data cannot be null");
}
CompoundTag level;
if ((level = data.getCompoundTag("Level")) == null) {
throw new IllegalArgumentException("data does not contain \"Level\" tag");
}
this.dataVersion = data.getInt("DataVersion");
this.inhabitedTime = level.getLong("InhabitedTime");
this.lastUpdate = level.getLong("LastUpdate");
if((loadFlags | BIOMES) != 0) {
this.biomes = level.getIntArray("Biomes");
}
if((loadFlags | HEIGHTMAPS) != 0) {
this.heightMaps = level.getCompoundTag("Heightmaps");
}
if((loadFlags | CARVING_MARKS) != 0) {
this.carvingMasks = level.getCompoundTag("CarvingMasks");
}
if((loadFlags | ENTITIES) != 0) {
this.entities = level.containsKey("Entities") ? level.getListTag("Entities").asCompoundTagList() : null;
}
if((loadFlags | TILE_ENTITIES) != 0) {
this.tileEntities = level.containsKey("TileEntities") ? level.getListTag("TileEntities").asCompoundTagList() : null;
}
if((loadFlags | TILE_TICKS) != 0) {
this.tileTicks = level.containsKey("TileTicks") ? level.getListTag("TileTicks").asCompoundTagList() : null;
}
if((loadFlags | LIQUID_TILE_TICKS) != 0) {
this.liquidTicks = level.containsKey("LiquidTicks") ? level.getListTag("LiquidTicks").asCompoundTagList() : null;
}
if((loadFlags | LIGHTS) != 0) {
this.lights = level.containsKey("Lights") ? level.getListTag("Lights").asListTagList() : null;
}
if((loadFlags | LIQUIDS_TO_BE_TICKED) != 0) {
this.liquidsToBeTicked = level.containsKey("LiquidsToBeTicked") ? level.getListTag("LiquidsToBeTicked").asListTagList() : null;
}
if((loadFlags | TO_BE_TICKED) != 0) {
this.toBeTicked = level.containsKey("ToBeTicked") ? level.getListTag("ToBeTicked").asListTagList() : null;
}
if((loadFlags | POST_PROCESSING) != 0) {
this.postProcessing = level.containsKey("PostProcessing") ? level.getListTag("PostProcessing").asListTagList() : null;
}
this.status = level.getString("Status");
this.structures = level.getCompoundTag("Structures");
if (level.containsKey("Sections")) {
for (CompoundTag section : level.getListTag("Sections").asCompoundTagList()) {
int sectionIndex = section.getByte("Y");
if (sectionIndex > 15 || sectionIndex < 0) {
continue;
}
Section newSection = new Section(section, dataVersion);
if (newSection.isEmpty()) {
continue;
}
this.sections[sectionIndex] = newSection;
}
}
// If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it.
if(loadFlags != ALL_DATA) {
this.data = null;
}
}
/**
* Serializes this chunk to a <code>RandomAccessFile</code>.
* @param raf The RandomAccessFile to be written to.
* @param xPos The x-coordinate of the chunk.
* @param zPos The z-coodrinate of the chunk.
* @return The amount of bytes written to the RandomAccessFile.
* @throws IOException When something went wrong during writing.
*/
public int serialize(RandomAccessFile raf, int xPos, int zPos) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
try (BufferedOutputStream nbtOut = new BufferedOutputStream(CompressionType.ZLIB.compress(baos))) {
new NBTSerializer(false).toStream(new NamedTag(null, updateHandle(xPos, zPos)), nbtOut);
}
byte[] rawData = baos.toByteArray();
raf.writeInt(rawData.length + 1); // including the byte to store the compression type
raf.writeByte(CompressionType.ZLIB.getID());
raf.write(rawData);
return rawData.length + 5;
}
/**
* Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position.
* @param raf The RandomAccessFile to read the chunk data from.
* @throws IOException When something went wrong during reading.
*/
public void deserialize(RandomAccessFile raf) throws IOException {
deserialize(raf, ALL_DATA);
}
/**
* Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position.
* @param raf The RandomAccessFile to read the chunk data from.
* @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded
* @throws IOException When something went wrong during reading.
*/
public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException {
byte compressionTypeByte = raf.readByte();
CompressionType compressionType = CompressionType.getFromID(compressionTypeByte);
if (compressionType == null) {
throw new IOException("invalid compression type " + compressionTypeByte);
}
BufferedInputStream dis = new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD())));
NamedTag tag = new NBTDeserializer(false).fromStream(dis);
if (tag != null && tag.getTag() instanceof CompoundTag) {
data = (CompoundTag) tag.getTag();
initReferences(loadFlags);
} else {
throw new IOException("invalid data tag: " + (tag == null ? "null" : tag.getClass().getName()));
}
}
/**
* @deprecated Use {@link #getBiomeAt(int, int, int)} instead
*/
@Deprecated
public int getBiomeAt(int blockX, int blockZ) {
if (dataVersion < 2202) {
if (biomes == null || biomes.length != 256) {
return -1;
}
return biomes[getBlockIndex(blockX, blockZ)];
} else {
throw new IllegalStateException("cannot get biome using Chunk#getBiomeAt(int,int) from biome data with DataVersion of 2202 or higher, use Chunk#getBiomeAt(int,int,int) instead");
}
}
/**
* Fetches a biome id at a specific block in this chunk.
* The coordinates can be absolute coordinates or relative to the region or chunk.
* @param blockX The x-coordinate of the block.
* @param blockY The y-coordinate of the block.
* @param blockZ The z-coordinate of the block.
* @return The biome id or -1 if the biomes are not correctly initialized.
*/
public int getBiomeAt(int blockX, int blockY, int blockZ) {
if (dataVersion < 2202) {
if (biomes == null || biomes.length != 256) {
return -1;
}
return biomes[getBlockIndex(blockX, blockZ)];
} else {
if (biomes == null || biomes.length != 1024) {
return -1;
}
int biomeX = (blockX & 0xF) >> 2;
int biomeY = (blockY & 0xF) >> 2;
int biomeZ = (blockZ & 0xF) >> 2;
return biomes[getBiomeIndex(biomeX, biomeY, biomeZ)];
}
}
@Deprecated
public void setBiomeAt(int blockX, int blockZ, int biomeID) {
if (dataVersion < 2202) {
if (biomes == null || biomes.length != 256) {
biomes = new int[256];
Arrays.fill(biomes, -1);
}
biomes[getBlockIndex(blockX, blockZ)] = biomeID;
} else {
if (biomes == null || biomes.length != 1024) {
biomes = new int[1024];
Arrays.fill(biomes, -1);
}
int biomeX = (blockX & 0xF) >> 2;
int biomeZ = (blockZ & 0xF) >> 2;
for (int y = 0; y < 64; y++) {
biomes[getBiomeIndex(biomeX, y, biomeZ)] = biomeID;
}
}
}
/**
* Sets a biome id at a specific block column.
* The coordinates can be absolute coordinates or relative to the region or chunk.
* @param blockX The x-coordinate of the block column.
* @param blockZ The z-coordinate of the block column.
* @param biomeID The biome id to be set.
* When set to a negative number, Minecraft will replace it with the block column's default biome.
*/
public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) {
if (dataVersion < 2202) {
if (biomes == null || biomes.length != 256) {
biomes = new int[256];
Arrays.fill(biomes, -1);
}
biomes[getBlockIndex(blockX, blockZ)] = biomeID;
} else {
if (biomes == null || biomes.length != 1024) {
biomes = new int[1024];
Arrays.fill(biomes, -1);
}
int biomeX = (blockX & 0xF) >> 2;
int biomeZ = (blockZ & 0xF) >> 2;
biomes[getBiomeIndex(biomeX, blockY, biomeZ)] = biomeID;
}
}
int getBiomeIndex(int biomeX, int biomeY, int biomeZ) {
return biomeY * 64 + biomeZ * 4 + biomeX;
}
public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) {
Section section = sections[MCAUtil.blockToChunk(blockY)];
if (section == null) {
return null;
}
return section.getBlockStateAt(blockX, blockY, blockZ);
}
/**
* Sets a block state at a specific location.
* The block coordinates can be absolute or relative to the region or chunk.
* @param blockX The x-coordinate of the block.
* @param blockY The y-coordinate of the block.
* @param blockZ The z-coordinate of the block.
* @param state The block state to be set.
* @param cleanup When <code>true</code>, it will cleanup all palettes of this chunk.
* This option should only be used moderately to avoid unnecessary recalculation of the palette indices.
* Recalculating the Palette should only be executed once right before saving the Chunk to file.
*/
public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) {
int sectionIndex = MCAUtil.blockToChunk(blockY);
Section section = sections[sectionIndex];
if (section == null) {
section = sections[sectionIndex] = Section.newSection();
}
section.setBlockStateAt(blockX, blockY, blockZ, state, cleanup);
}
/**
* @return The DataVersion of this chunk.
*/
public int getDataVersion() {
return dataVersion;
}
/**
* Sets the DataVersion of this chunk. This does not check if the data of this chunk conforms
* to that DataVersion, that is the responsibility of the developer.
* @param dataVersion The DataVersion to be set.
*/
public void setDataVersion(int dataVersion) {
this.dataVersion = dataVersion;
}
/**
* @return The timestamp when this region file was last updated in seconds since 1970-01-01.
*/
public int getLastMCAUpdate() {
return lastMCAUpdate;
}
/**
* Sets the timestamp when this region file was last updated in seconds since 1970-01-01.
* @param lastMCAUpdate The time in seconds since 1970-01-01.
*/
public void setLastMCAUpdate(int lastMCAUpdate) {
this.lastMCAUpdate = lastMCAUpdate;
}
/**
* @return The generation station of this chunk.
*/
public String getStatus() {
return status;
}
/**
* Sets the generation status of this chunk.
* @param status The generation status of this chunk.
*/
public void setStatus(String status) {
this.status = status;
}
/**
* Fetches the section at the given y-coordinate.
* @param sectionY The y-coordinate of the section in this chunk ranging from 0 to 15.
* @return The Section.
*/
public Section getSection(int sectionY) {
return sections[sectionY];
}
/**
* Sets a section at a givesn y-coordinate
* @param sectionY The y-coordinate of the section in this chunk ranging from 0 to 15.
* @param section The section to be set.
*/
public void setSection(int sectionY, Section section) {
sections[sectionY] = section;
}
/**
* @return The timestamp when this chunk was last updated as a UNIX timestamp.
*/
public long getLastUpdate() {
return lastUpdate;
}
/**
* Sets the time when this chunk was last updated as a UNIX timestamp.
* @param lastUpdate The UNIX timestamp.
*/
public void setLastUpdate(long lastUpdate) {
this.lastUpdate = lastUpdate;
}
/**
* @return The cumulative amount of time players have spent in this chunk in ticks.
*/
public long getInhabitedTime() {
return inhabitedTime;
}
/**
* Sets the cumulative amount of time players have spent in this chunk in ticks.
* @param inhabitedTime The time in ticks.
*/
public void setInhabitedTime(long inhabitedTime) {
this.inhabitedTime = inhabitedTime;
}
/**
* @return A matrix of biome IDs for all block columns in this chunk.
*/
public int[] getBiomes() {
return biomes;
}
/**
* Sets the biome IDs for this chunk.
* @param biomes The biome ID matrix of this chunk. Must have a length of <code>256</code>.
* @throws IllegalArgumentException When the biome matrix does not have a length of <code>256</code>
* or is <code>null</code>
*/
public void setBiomes(int[] biomes) {
if (biomes != null) {
if (dataVersion < 2202 && biomes.length != 256 || dataVersion >= 2202 && biomes.length != 1024) {
throw new IllegalArgumentException("biomes array must have a length of " + (dataVersion < 2202 ? "256" : "1024"));
}
}
this.biomes = biomes;
}
/**
* @return The height maps of this chunk.
*/
public CompoundTag getHeightMaps() {
return heightMaps;
}
/**
* Sets the height maps of this chunk.
* @param heightMaps The height maps.
*/
public void setHeightMaps(CompoundTag heightMaps) {
this.heightMaps = heightMaps;
}
/**
* @return The carving masks of this chunk.
*/
public CompoundTag getCarvingMasks() {
return carvingMasks;
}
/**
* Sets the carving masks of this chunk.
* @param carvingMasks The carving masks.
*/
public void setCarvingMasks(CompoundTag carvingMasks) {
this.carvingMasks = carvingMasks;
}
/**
* @return The entities of this chunk.
*/
public ListTag<CompoundTag> getEntities() {
return entities;
}
/**
* Sets the entities of this chunk.
* @param entities The entities.
*/
public void setEntities(ListTag<CompoundTag> entities) {
this.entities = entities;
}
/**
* @return The tile entities of this chunk.
*/
public ListTag<CompoundTag> getTileEntities() {
return tileEntities;
}
/**
* Sets the tile entities of this chunk.
* @param tileEntities The tile entities of this chunk.
*/
public void setTileEntities(ListTag<CompoundTag> tileEntities) {
this.tileEntities = tileEntities;
}
/**
* @return The tile ticks of this chunk.
*/
public ListTag<CompoundTag> getTileTicks() {
return tileTicks;
}
/**
* Sets the tile ticks of this chunk.
* @param tileTicks Thee tile ticks.
*/
public void setTileTicks(ListTag<CompoundTag> tileTicks) {
this.tileTicks = tileTicks;
}
/**
* @return The liquid ticks of this chunk.
*/
public ListTag<CompoundTag> getLiquidTicks() {
return liquidTicks;
}
/**
* Sets the liquid ticks of this chunk.
* @param liquidTicks The liquid ticks.
*/
public void setLiquidTicks(ListTag<CompoundTag> liquidTicks) {
this.liquidTicks = liquidTicks;
}
/**
* @return The light sources in this chunk.
*/
public ListTag<ListTag<?>> getLights() {
return lights;
}
/**
* Sets the light sources in this chunk.
* @param lights The light sources.
*/
public void setLights(ListTag<ListTag<?>> lights) {
this.lights = lights;
}
/**
* @return THe liquids to be ticked in this chunk.
*/
public ListTag<ListTag<?>> getLiquidsToBeTicked() {
return liquidsToBeTicked;
}
/**
* Sets the liquids to be ticked in this chunk.
* @param liquidsToBeTicked The liquids to be ticked.
*/
public void setLiquidsToBeTicked(ListTag<ListTag<?>> liquidsToBeTicked) {
this.liquidsToBeTicked = liquidsToBeTicked;
}
/**
* @return Stuff to be ticked in this chunk.
*/
public ListTag<ListTag<?>> getToBeTicked() {
return toBeTicked;
}
/**
* Sets stuff to be ticked in this chunk.
* @param toBeTicked The stuff to be ticked.
*/
public void setToBeTicked(ListTag<ListTag<?>> toBeTicked) {
this.toBeTicked = toBeTicked;
}
/**
* @return Things that are in post processing in this chunk.
*/
public ListTag<ListTag<?>> getPostProcessing() {
return postProcessing;
}
/**
* Sets things to be post processed in this chunk.
* @param postProcessing The things to be post processed.
*/
public void setPostProcessing(ListTag<ListTag<?>> postProcessing) {
this.postProcessing = postProcessing;
}
/**
* @return Data about structures in this chunk.
*/
public CompoundTag getStructures() {
return structures;
}
/**
* Sets data about structures in this chunk.
* @param structures The data about structures.
*/
public void setStructures(CompoundTag structures) {
this.structures = structures;
}
int getBlockIndex(int blockX, int blockZ) {
return (blockZ & 0xF) * 16 + (blockX & 0xF);
}
public void cleanupPalettesAndBlockStates() {
for (Section section : sections) {
if (section != null) {
section.cleanupPaletteAndBlockStates();
}
}
}
public static Chunk newChunk() {
Chunk c = new Chunk(0);
c.dataVersion = DEFAULT_DATA_VERSION;
c.data = new CompoundTag();
c.data.put("Level", new CompoundTag());
c.status = "mobs_spawned";
return c;
}
public CompoundTag updateHandle(int xPos, int zPos) {
data.putInt("DataVersion", dataVersion);
CompoundTag level = data.getCompoundTag("Level");
level.putInt("xPos", xPos);
level.putInt("zPos", zPos);
level.putLong("LastUpdate", lastUpdate);
level.putLong("InhabitedTime", inhabitedTime);
if (dataVersion < 2202) {
if (biomes != null && biomes.length == 256) level.putIntArray("Biomes", biomes);
} else {
if (biomes != null && biomes.length == 1024) level.putIntArray("Biomes", biomes);
}
if (heightMaps != null) level.put("HeightMaps", heightMaps);
if (carvingMasks != null) level.put("CarvingMasks", carvingMasks);
if (entities != null) level.put("Entities", entities);
if (tileEntities != null) level.put("TileEntities", tileEntities);
if (tileTicks != null) level.put("TileTicks", tileTicks);
if (liquidTicks != null) level.put("LiquidTicks", liquidTicks);
if (lights != null) level.put("Lights", lights);
if (liquidsToBeTicked != null) level.put("LiquidsToBeTicked", liquidsToBeTicked);
if (toBeTicked != null) level.put("ToBeTicked", toBeTicked);
if (postProcessing != null) level.put("PostProcessing", postProcessing);
level.putString("Status", status);
if (structures != null) level.put("Structures", structures);
ListTag<CompoundTag> sections = new ListTag<>(CompoundTag.class);
for (int i = 0; i < this.sections.length; i++) {
if (this.sections[i] != null) {
sections.add(this.sections[i].updateHandle(i));
}
}
level.put("Sections", sections);
return data;
}
}