blob: 649a40d3ecdc75c17d9dc98e79715475d21146ce [file] [log] [blame] [raw]
package net.querz.nbt.mca;
import net.querz.nbt.CompoundTag;
import net.querz.nbt.ListTag;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Section {
private CompoundTag data;
private Map<String, List<PaletteIndex>> valueIndexedPalette = new HashMap<>();
private ListTag<CompoundTag> palette;
private byte[] blockLight;
private long[] blockStates;
private byte[] skyLight;
/**
* Creates a new Section based on raw section data.
* @param sectionRoot The raw section data
*/
public Section(CompoundTag sectionRoot) {
ListTag<?> rawPalette = sectionRoot.getListTag("Palette");
if (rawPalette == null) {
return;
}
palette = rawPalette.asCompoundTagList();
for (int i = 0; i < palette.size(); i++) {
CompoundTag data = palette.get(i);
putValueIndexedPalette(data, i);
}
blockLight = sectionRoot.getByteArray("BlockLight");
blockStates = sectionRoot.getLongArray("BlockStates");
skyLight = sectionRoot.getByteArray("SkyLight");
data = sectionRoot;
}
Section() {}
private void putValueIndexedPalette(CompoundTag data, int index) {
PaletteIndex leaf = new PaletteIndex(data, index);
String name = data.getString("Name");
List<PaletteIndex> leaves = valueIndexedPalette.get(name);
if (leaves == null) {
leaves = new ArrayList<>(1);
leaves.add(leaf);
valueIndexedPalette.put(name, leaves);
} else {
for (PaletteIndex pal : leaves) {
if (pal.data.equals(data)) {
return;
}
}
leaves.add(leaf);
}
}
private PaletteIndex getValueIndexedPalette(CompoundTag data) {
List<PaletteIndex> leaves = valueIndexedPalette.get(data.getString("Name"));
if (leaves == null) {
return null;
}
for (PaletteIndex leaf : leaves) {
if (leaf.data.equals(data)) {
return leaf;
}
}
return null;
}
private class PaletteIndex {
CompoundTag data;
int index;
PaletteIndex(CompoundTag data, int index) {
this.data = data;
this.index = index;
}
}
/**
* Checks whether the data of this Section is empty.
* @return true if empty
*/
public boolean isEmpty() {
return data == null;
}
/**
* Fetches a block state based on a block location from this section.
* The coordinates represent the location of the block inside of this Section.
* @param blockX The x-coordinate of the block in this Section
* @param blockY The y-coordinate of the block in this Section
* @param blockZ The z-coordinate of the block in this Section
* @return The block state data of this block.
*/
public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) {
int index = getBlockIndex(blockX, blockY, blockZ);
int paletteIndex = getPaletteIndex(index);
return palette.get(paletteIndex);
}
/**
* Attempts to add a block state for a specific block location in this Section.
* @param blockX The x-coordinate of the block in this Section
* @param blockY The y-coordinate of the block in this Section
* @param blockZ The z-coordinate of the block in this Section
* @param state The block state to be set
* @param cleanup When <code>true</code>, it will cleanup the palette of this section.
* 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 Section to file.
*/
public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) {
int paletteSizeBefore = palette.size();
int paletteIndex = addToPalette(state);
//power of 2 --> bits must increase, but only if the palette size changed
//otherwise we would attempt to update all blockstates and the entire palette
//every time an existing blockstate was added while having 2^x blockstates in the palette
if (paletteSizeBefore != palette.size() && (paletteIndex & (paletteIndex - 1)) == 0) {
adjustBlockStateBits(null, blockStates);
cleanup = true;
}
setPaletteIndex(getBlockIndex(blockX, blockY, blockZ), paletteIndex, blockStates);
if (cleanup) {
cleanupPaletteAndBlockStates();
}
}
/**
* Returns the index of the block data in the palette.
* @param blockStateIndex The index of the block in this section, ranging from 0-4095.
* @return The index of the block data in the palette.
* */
public int getPaletteIndex(int blockStateIndex) {
int bits = blockStates.length >> 6;
double blockStatesIndex = blockStateIndex / (4096D / blockStates.length);
int longIndex = (int) blockStatesIndex;
int startBit = (int) ((blockStatesIndex - Math.floor(blockStatesIndex)) * 64D);
if (startBit + bits > 64) {
long prev = bitRange(blockStates[longIndex], startBit, 64);
long next = bitRange(blockStates[longIndex + 1], 0, startBit + bits - 64);
return (int) ((next << 64 - startBit) + prev);
} else {
return (int) bitRange(blockStates[longIndex], startBit, startBit + bits);
}
}
/**
* Sets the index of the block data in the BlockStates. Does not adjust the size of the BlockStates array.
* @param blockIndex The index of the block in this section, ranging from 0-4095.
* @param paletteIndex The block state to be set (index of block data in the palette).
* @param blockStates The block states to be updated.
* */
public void setPaletteIndex(int blockIndex, int paletteIndex, long[] blockStates) {
int bits = blockStates.length / 64;
double blockStatesIndex = blockIndex / (4096D / blockStates.length);
int longIndex = (int) blockStatesIndex;
int startBit = (int) ((blockStatesIndex - Math.floor(longIndex)) * 64D);
if (startBit + bits > 64) {
blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, 64);
blockStates[longIndex + 1] = updateBits(blockStates[longIndex + 1], paletteIndex, startBit - 64, startBit + bits - 64);
} else {
blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, startBit + bits);
}
}
/**
* Fetches the palette of this Section.
* @return The palette of this Section.
*/
public ListTag<CompoundTag> getPalette() {
return palette;
}
int addToPalette(CompoundTag data) {
PaletteIndex index;
if ((index = getValueIndexedPalette(data)) != null) {
return index.index;
}
palette.add(data);
putValueIndexedPalette(data, palette.size() - 1);
return palette.size() - 1;
}
private int getBlockIndex(int blockX, int blockY, int blockZ) {
return (blockY & 0xF) * 256 + (blockZ & 0xF) * 16 + (blockX & 0xF);
}
private static long updateBits(long n, long m, int i, int j) {
//replace i to j in n with j - i bits of m
long mShifted = i > 0 ? (m & ((1L << j - i) - 1)) << i : (m & ((1L << j - i) - 1)) >>> -i;
return ((n & ((j > 63 ? 0 : (~0L << j)) | (i < 0 ? 0 : ((1L << i) - 1L)))) | mShifted);
}
private static long bitRange(long value, int from, int to) {
int waste = 64 - to;
return (value << waste) >>> (waste + from);
}
/**
* This method recalculates the palette and its indices.
* This 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 Section to file.
*/
public void cleanupPaletteAndBlockStates() {
Map<Integer, Integer> oldToNewMapping = cleanupPalette();
adjustBlockStateBits(oldToNewMapping, blockStates);
}
private Map<Integer, Integer> cleanupPalette() {
//create index - palette mapping
Map<Integer, Integer> allIndices = new HashMap<>();
for (int i = 0; i < 4096; i++) {
int paletteIndex = getPaletteIndex(i);
allIndices.put(paletteIndex, paletteIndex);
}
//delete unused blocks from palette
//start at index 1 because we need to keep minecraft:air
int index = 1;
valueIndexedPalette = new HashMap<>(valueIndexedPalette.size());
putValueIndexedPalette(palette.get(0), 0);
for (int i = 1; i < palette.size(); i++) {
if (!allIndices.containsKey(index)) {
palette.remove(i);
i--;
} else {
putValueIndexedPalette(palette.get(i), i);
allIndices.put(index, i);
}
index++;
}
return allIndices;
}
private void adjustBlockStateBits(Map<Integer, Integer> oldToNewMapping, long[] blockStates) {
//increases or decreases the amount of bits used per BlockState
//based on the size of the palette. oldToNewMapping can be used to update indices
//if the palette had been cleaned up before using MCAFile#cleanupPalette().
int newBits = 32 - Integer.numberOfLeadingZeros(palette.size() - 1);
newBits = newBits < 4 ? 4 : newBits;
long[] newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newBits * 64];
if (oldToNewMapping != null) {
for (int i = 0; i < 4096; i++) {
setPaletteIndex(i, oldToNewMapping.get(getPaletteIndex(i)), newBlockStates);
}
} else {
for (int i = 0; i < 4096; i++) {
setPaletteIndex(i, getPaletteIndex(i), newBlockStates);
}
}
this.blockStates = newBlockStates;
}
/**
* @return The block light array of this Section
*/
public byte[] getBlockLight() {
return blockLight;
}
/**
* Sets the block light array for this section.
* @param blockLight The block light array
* @throws IllegalArgumentException When the length of the array is not 2048
*/
public void setBlockLight(byte[] blockLight) {
if (blockLight != null && blockLight.length != 2048) {
throw new IllegalArgumentException("BlockLight array must have a length of 2048");
}
this.blockLight = blockLight;
}
/**
* @return The indices of the block states of this Section.
*/
public long[] getBlockStates() {
return blockStates;
}
/**
* Sets the block state indices to a custom value.
* @param blockStates The block state indices.
* @throws NullPointerException If <code>blockStates</code> is <code>null</code>
* @throws IllegalArgumentException When <code>blockStates</code>' length is &lt; 256 or &gt; 4096 and is not a multiple of 64
*/
public void setBlockStates(long[] blockStates) {
if (blockStates == null) {
throw new NullPointerException("BlockStates cannot be null");
} else if (blockStates.length % 64 != 0 || blockStates.length < 256 || blockStates.length > 4096) {
throw new IllegalArgumentException("BlockStates must have a length > 255 and < 4097 and must be divisible by 64");
}
this.blockStates = blockStates;
}
/**
* @return The sky light values of this Section
*/
public byte[] getSkyLight() {
return skyLight;
}
/**
* Sets the sky light values of this section.
* @param skyLight The custom sky light values
* @throws IllegalArgumentException If the length of the array is not 2048
*/
public void setSkyLight(byte[] skyLight) {
if (skyLight != null && skyLight.length != 2048) {
throw new IllegalArgumentException("SkyLight array must have a length of 2048");
}
this.skyLight = skyLight;
}
/**
* Creates an empty Section with base values.
* @return An empty Section
*/
public static Section newSection() {
Section s = new Section();
s.blockStates = new long[256];
s.palette = new ListTag<>(CompoundTag.class);
CompoundTag air = new CompoundTag();
air.putString("Name", "minecraft:air");
s.palette.add(air);
s.data = new CompoundTag();
return s;
}
/**
* Updates the raw CompoundTag that this Section is based on.
* This must be called before saving a Section to disk if the Section was manually created
* to set the Y of this Section.
* @param y The Y-value of this Section
* @return A reference to the raw CompoundTag this Section is based on
*/
public CompoundTag updateHandle(int y) {
data.putByte("Y", (byte) y);
data.put("Palette", palette);
if (blockLight != null) data.putByteArray("BlockLight", blockLight);
data.putLongArray("BlockStates", blockStates);
if (skyLight != null) data.putByteArray("SkyLight", skyLight);
return data;
}
}