mirror of
https://github.com/Minestom/Minestom.git
synced 2025-01-25 01:21:20 +01:00
Improve block placement performance, more abstraction for future features
This commit is contained in:
parent
933b2663eb
commit
71d0d06f90
@ -1,15 +1,14 @@
|
|||||||
package net.minestom.server.instance.palette;
|
package net.minestom.server.instance.palette;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.shorts.Short2ShortLinkedOpenHashMap;
|
|
||||||
import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
|
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
import net.minestom.server.instance.Chunk;
|
import net.minestom.server.instance.Chunk;
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
import net.minestom.server.utils.MathUtils;
|
import net.minestom.server.utils.MathUtils;
|
||||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||||
|
import net.minestom.server.utils.clone.CloneUtils;
|
||||||
import net.minestom.server.utils.clone.PublicCloneable;
|
import net.minestom.server.utils.clone.PublicCloneable;
|
||||||
import net.minestom.server.utils.validate.Check;
|
import net.minestom.server.utils.validate.Check;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_COUNT;
|
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_COUNT;
|
||||||
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_SIZE;
|
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_SIZE;
|
||||||
@ -22,102 +21,58 @@ import static net.minestom.server.instance.Chunk.CHUNK_SECTION_SIZE;
|
|||||||
*/
|
*/
|
||||||
public class PaletteStorage implements PublicCloneable<PaletteStorage> {
|
public class PaletteStorage implements PublicCloneable<PaletteStorage> {
|
||||||
|
|
||||||
/**
|
private Section[] sections = new Section[CHUNK_SECTION_COUNT];
|
||||||
* The maximum bits per entry value.
|
|
||||||
*/
|
|
||||||
private final static int MAXIMUM_BITS_PER_ENTRY = 15;
|
|
||||||
|
|
||||||
/**
|
private final int defaultBitsPerEntry;
|
||||||
* The minimum bits per entry value.
|
private final int defaultBitsIncrement;
|
||||||
*/
|
|
||||||
private final static int MINIMUM_BITS_PER_ENTRY = 4;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximum bits per entry value which allow for a data palette.
|
|
||||||
*/
|
|
||||||
private final static int PALETTE_MAXIMUM_BITS = 8;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of blocks that should be in one chunk section.
|
|
||||||
*/
|
|
||||||
private final static int BLOCK_COUNT = CHUNK_SECTION_SIZE * CHUNK_SECTION_SIZE * CHUNK_SECTION_SIZE;
|
|
||||||
|
|
||||||
private int bitsPerEntry;
|
|
||||||
private final int bitsIncrement;
|
|
||||||
|
|
||||||
private int valuesPerLong;
|
|
||||||
private boolean hasPalette;
|
|
||||||
|
|
||||||
private long[][] sectionBlocks;
|
|
||||||
|
|
||||||
// chunk section - palette index = block id
|
|
||||||
private Short2ShortLinkedOpenHashMap[] paletteBlockMaps;
|
|
||||||
// chunk section - block id = palette index
|
|
||||||
private Short2ShortOpenHashMap[] blockPaletteMaps;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new palette storage.
|
* Creates a new palette storage.
|
||||||
*
|
*
|
||||||
* @param bitsPerEntry the number of bits used for one entry (block)
|
* @param defaultBitsPerEntry the number of bits used for one entry (block)
|
||||||
* @param bitsIncrement the number of bits to add per-block once the palette array is filled
|
* @param defaultBitsIncrement the number of bits to add per-block once the palette array is filled
|
||||||
*/
|
*/
|
||||||
public PaletteStorage(int bitsPerEntry, int bitsIncrement) {
|
public PaletteStorage(int defaultBitsPerEntry, int defaultBitsIncrement) {
|
||||||
Check.argCondition(bitsPerEntry > MAXIMUM_BITS_PER_ENTRY, "The maximum bits per entry is 15");
|
Check.argCondition(defaultBitsPerEntry > Section.MAXIMUM_BITS_PER_ENTRY,
|
||||||
bitsPerEntry = fixBitsPerEntry(bitsPerEntry);
|
"The maximum bits per entry is 15");
|
||||||
|
this.defaultBitsPerEntry = defaultBitsPerEntry;
|
||||||
this.bitsPerEntry = bitsPerEntry;
|
this.defaultBitsIncrement = defaultBitsIncrement;
|
||||||
this.bitsIncrement = bitsIncrement;
|
|
||||||
|
|
||||||
this.valuesPerLong = Long.SIZE / bitsPerEntry;
|
|
||||||
this.hasPalette = bitsPerEntry <= PALETTE_MAXIMUM_BITS;
|
|
||||||
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
this.sectionBlocks = new long[CHUNK_SECTION_COUNT][0];
|
|
||||||
|
|
||||||
this.paletteBlockMaps = new Short2ShortLinkedOpenHashMap[CHUNK_SECTION_COUNT];
|
|
||||||
this.blockPaletteMaps = new Short2ShortOpenHashMap[CHUNK_SECTION_COUNT];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBlockAt(int x, int y, int z, short blockId) {
|
public void setBlockAt(int x, int y, int z, short blockId) {
|
||||||
PaletteStorage.setBlockAt(this, x, y, z, blockId);
|
if (!MathUtils.isBetween(y, 0, Chunk.CHUNK_SIZE_Y - 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int sectionIndex = ChunkUtils.getSectionAt(y);
|
||||||
|
x = toChunkCoordinate(x);
|
||||||
|
z = toChunkCoordinate(z);
|
||||||
|
|
||||||
|
Section section = sections[sectionIndex];
|
||||||
|
if (section == null) {
|
||||||
|
section = new Section(defaultBitsPerEntry, defaultBitsIncrement);
|
||||||
|
sections[sectionIndex] = section;
|
||||||
|
}
|
||||||
|
section.setBlockAt(x, y, z, blockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public short getBlockAt(int x, int y, int z) {
|
public short getBlockAt(int x, int y, int z) {
|
||||||
return PaletteStorage.getBlockAt(this, x, y, z);
|
if (y < 0 || y >= Chunk.CHUNK_SIZE_Y) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
final int sectionIndex = ChunkUtils.getSectionAt(y);
|
||||||
* Gets the number of bits that the palette currently take per block.
|
final Section section = sections[sectionIndex];
|
||||||
*
|
if (section == null) {
|
||||||
* @return the bits per entry
|
return Block.AIR.getBlockId();
|
||||||
*/
|
}
|
||||||
public int getBitsPerEntry() {
|
x = toChunkCoordinate(x);
|
||||||
return bitsPerEntry;
|
z = toChunkCoordinate(z);
|
||||||
|
|
||||||
|
return section.getBlockAt(x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Section[] getSections() {
|
||||||
* Gets the palette with the index and the block id as the value.
|
return sections;
|
||||||
*
|
|
||||||
* @param section the chunk section to get the palette from
|
|
||||||
* @return the palette
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public short[] getPalette(int section) {
|
|
||||||
Short2ShortLinkedOpenHashMap paletteBlockMap = paletteBlockMaps[section];
|
|
||||||
return paletteBlockMap != null ? paletteBlockMap.values().toShortArray() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the sections of this object,
|
|
||||||
* the first array representing the chunk section and the second the block position from {@link #getSectionIndex(int, int, int)}.
|
|
||||||
*
|
|
||||||
* @return the section blocks
|
|
||||||
*/
|
|
||||||
public long[][] getSectionBlocks() {
|
|
||||||
return sectionBlocks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,23 +82,8 @@ public class PaletteStorage implements PublicCloneable<PaletteStorage> {
|
|||||||
* is composed of almost-empty sections since the loop will not stop until a non-air block is discovered.
|
* is composed of almost-empty sections since the loop will not stop until a non-air block is discovered.
|
||||||
*/
|
*/
|
||||||
public synchronized void clean() {
|
public synchronized void clean() {
|
||||||
for (int i = 0; i < sectionBlocks.length; i++) {
|
for (Section section : sections) {
|
||||||
long[] section = sectionBlocks[i];
|
section.clean();
|
||||||
|
|
||||||
if (section.length != 0) {
|
|
||||||
boolean canClear = true;
|
|
||||||
for (long blockGroup : section) {
|
|
||||||
if (blockGroup != 0) {
|
|
||||||
canClear = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (canClear) {
|
|
||||||
sectionBlocks[i] = new long[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,16 +91,9 @@ public class PaletteStorage implements PublicCloneable<PaletteStorage> {
|
|||||||
* Clears all the data in the palette and data array.
|
* Clears all the data in the palette and data array.
|
||||||
*/
|
*/
|
||||||
public void clear() {
|
public void clear() {
|
||||||
init();
|
for (Section section : sections) {
|
||||||
|
section.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use {@link #clone()}
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
@NotNull
|
|
||||||
public PaletteStorage copy() {
|
|
||||||
return clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ -168,10 +101,7 @@ public class PaletteStorage implements PublicCloneable<PaletteStorage> {
|
|||||||
public PaletteStorage clone() {
|
public PaletteStorage clone() {
|
||||||
try {
|
try {
|
||||||
PaletteStorage paletteStorage = (PaletteStorage) super.clone();
|
PaletteStorage paletteStorage = (PaletteStorage) super.clone();
|
||||||
paletteStorage.sectionBlocks = sectionBlocks.clone();
|
paletteStorage.sections = CloneUtils.cloneArray(sections, Section[]::new);
|
||||||
|
|
||||||
paletteStorage.paletteBlockMaps = paletteBlockMaps.clone();
|
|
||||||
paletteStorage.blockPaletteMaps = blockPaletteMaps.clone();
|
|
||||||
return paletteStorage;
|
return paletteStorage;
|
||||||
} catch (CloneNotSupportedException e) {
|
} catch (CloneNotSupportedException e) {
|
||||||
MinecraftServer.getExceptionManager().handleException(e);
|
MinecraftServer.getExceptionManager().handleException(e);
|
||||||
@ -179,205 +109,6 @@ public class PaletteStorage implements PublicCloneable<PaletteStorage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the palette index for the specified block id.
|
|
||||||
* <p>
|
|
||||||
* Also responsible for resizing the palette when full.
|
|
||||||
*
|
|
||||||
* @param section the chunk section
|
|
||||||
* @param blockId the block id to convert
|
|
||||||
* @return the palette index of {@code blockId}
|
|
||||||
*/
|
|
||||||
private short getPaletteIndex(int section, short blockId) {
|
|
||||||
if (!hasPalette) {
|
|
||||||
return blockId;
|
|
||||||
}
|
|
||||||
|
|
||||||
Short2ShortOpenHashMap blockPaletteMap = blockPaletteMaps[section];
|
|
||||||
if (blockPaletteMap == null) {
|
|
||||||
blockPaletteMap = createBlockPaletteMap();
|
|
||||||
blockPaletteMaps[section] = blockPaletteMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!blockPaletteMap.containsKey(blockId)) {
|
|
||||||
Short2ShortLinkedOpenHashMap paletteBlockMap = paletteBlockMaps[section];
|
|
||||||
if (paletteBlockMap == null) {
|
|
||||||
paletteBlockMap = createPaletteBlockMap();
|
|
||||||
paletteBlockMaps[section] = paletteBlockMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize the palette if full
|
|
||||||
if (paletteBlockMap.size() >= getMaxPaletteSize()) {
|
|
||||||
resize(bitsPerEntry + bitsIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
final short paletteIndex = (short) (paletteBlockMap.lastShortKey() + 1);
|
|
||||||
paletteBlockMap.put(paletteIndex, blockId);
|
|
||||||
blockPaletteMap.put(blockId, paletteIndex);
|
|
||||||
return paletteIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return blockPaletteMap.get(blockId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes the array.
|
|
||||||
* <p>
|
|
||||||
* Will create a new palette storage to set all the current blocks, and the data will be transferred to 'this'.
|
|
||||||
*
|
|
||||||
* @param newBitsPerEntry the new bits per entry count
|
|
||||||
*/
|
|
||||||
private void resize(int newBitsPerEntry) {
|
|
||||||
newBitsPerEntry = fixBitsPerEntry(newBitsPerEntry);
|
|
||||||
|
|
||||||
PaletteStorage paletteStorageCache = new PaletteStorage(newBitsPerEntry, bitsIncrement);
|
|
||||||
paletteStorageCache.paletteBlockMaps = paletteBlockMaps;
|
|
||||||
paletteStorageCache.blockPaletteMaps = blockPaletteMaps;
|
|
||||||
|
|
||||||
for (int y = 0; y < Chunk.CHUNK_SIZE_Y; y++) {
|
|
||||||
for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
|
|
||||||
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
|
|
||||||
final short blockId = getBlockAt(x, y, z);
|
|
||||||
paletteStorageCache.setBlockAt(x, y, z, blockId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bitsPerEntry = paletteStorageCache.bitsPerEntry;
|
|
||||||
|
|
||||||
this.valuesPerLong = paletteStorageCache.valuesPerLong;
|
|
||||||
this.hasPalette = paletteStorageCache.hasPalette;
|
|
||||||
|
|
||||||
this.sectionBlocks = paletteStorageCache.sectionBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the maximum number of blocks that the current palette (could be the global one) can take.
|
|
||||||
*
|
|
||||||
* @return the number of blocks possible in the palette
|
|
||||||
*/
|
|
||||||
private int getMaxPaletteSize() {
|
|
||||||
return 1 << bitsPerEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Magic values generated with "Integer.MAX_VALUE >> (31 - bitsPerIndex)" for bitsPerIndex between 4 and 15
|
|
||||||
private static final int[] MAGIC_MASKS =
|
|
||||||
{0, 0, 0, 0,
|
|
||||||
15, 31, 63, 127, 255,
|
|
||||||
511, 1023, 2047, 4095,
|
|
||||||
8191, 16383, 32767};
|
|
||||||
|
|
||||||
private static void setBlockAt(@NotNull PaletteStorage paletteStorage, int x, int y, int z, short blockId) {
|
|
||||||
if (!MathUtils.isBetween(y, 0, Chunk.CHUNK_SIZE_Y - 1)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int section = ChunkUtils.getSectionAt(y);
|
|
||||||
|
|
||||||
int valuesPerLong = paletteStorage.valuesPerLong;
|
|
||||||
|
|
||||||
if (paletteStorage.sectionBlocks[section].length == 0) {
|
|
||||||
if (blockId == 0) {
|
|
||||||
// Section is empty and method is trying to place an air block, stop unnecessary computation
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the section
|
|
||||||
paletteStorage.sectionBlocks[section] = new long[getSize(valuesPerLong)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert world coordinates to chunk coordinates
|
|
||||||
x = toChunkCoordinate(x);
|
|
||||||
z = toChunkCoordinate(z);
|
|
||||||
|
|
||||||
// Change to palette value
|
|
||||||
blockId = paletteStorage.getPaletteIndex(section, blockId);
|
|
||||||
|
|
||||||
// The storage could have been resized
|
|
||||||
valuesPerLong = paletteStorage.valuesPerLong;
|
|
||||||
|
|
||||||
final int sectionIndex = getSectionIndex(x, y, z);
|
|
||||||
|
|
||||||
final int index = sectionIndex / valuesPerLong;
|
|
||||||
final int bitsPerEntry = paletteStorage.bitsPerEntry;
|
|
||||||
|
|
||||||
final int bitIndex = (sectionIndex % valuesPerLong) * bitsPerEntry;
|
|
||||||
|
|
||||||
final long[] sectionBlock = paletteStorage.sectionBlocks[section];
|
|
||||||
|
|
||||||
long block = sectionBlock[index];
|
|
||||||
{
|
|
||||||
final long clear = MAGIC_MASKS[bitsPerEntry];
|
|
||||||
|
|
||||||
block |= clear << bitIndex;
|
|
||||||
block ^= clear << bitIndex;
|
|
||||||
block |= (long) blockId << bitIndex;
|
|
||||||
|
|
||||||
sectionBlock[index] = block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static short getBlockAt(@NotNull PaletteStorage paletteStorage, int x, int y, int z) {
|
|
||||||
if (y < 0 || y >= Chunk.CHUNK_SIZE_Y) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int section = ChunkUtils.getSectionAt(y);
|
|
||||||
final long[] blocks;
|
|
||||||
|
|
||||||
// Retrieve the longs and check if the section is empty
|
|
||||||
{
|
|
||||||
blocks = paletteStorage.sectionBlocks[section];
|
|
||||||
|
|
||||||
if (blocks.length == 0) {
|
|
||||||
// Section is not loaded, can only be air
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
x = toChunkCoordinate(x);
|
|
||||||
z = toChunkCoordinate(z);
|
|
||||||
|
|
||||||
final int sectionIndex = getSectionIndex(x, y, z);
|
|
||||||
|
|
||||||
final int valuesPerLong = paletteStorage.valuesPerLong;
|
|
||||||
final int bitsPerEntry = paletteStorage.bitsPerEntry;
|
|
||||||
|
|
||||||
final int index = sectionIndex / valuesPerLong;
|
|
||||||
final int bitIndex = sectionIndex % valuesPerLong * bitsPerEntry;
|
|
||||||
|
|
||||||
final long value = blocks[index] >> bitIndex & MAGIC_MASKS[bitsPerEntry];
|
|
||||||
|
|
||||||
// Change to palette value and return
|
|
||||||
return paletteStorage.hasPalette ?
|
|
||||||
paletteStorage.paletteBlockMaps[section].get((short) value) :
|
|
||||||
(short) value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Short2ShortLinkedOpenHashMap createPaletteBlockMap() {
|
|
||||||
Short2ShortLinkedOpenHashMap map = new Short2ShortLinkedOpenHashMap(CHUNK_SECTION_SIZE);
|
|
||||||
map.put((short) 0, (short) 0);
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Short2ShortOpenHashMap createBlockPaletteMap() {
|
|
||||||
Short2ShortOpenHashMap map = new Short2ShortOpenHashMap(CHUNK_SECTION_SIZE);
|
|
||||||
map.put((short) 0, (short) 0);
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the array length of one section based on the number of values which can be stored in one long.
|
|
||||||
*
|
|
||||||
* @param valuesPerLong the number of values per long
|
|
||||||
* @return the array length based on {@code valuesPerLong}
|
|
||||||
*/
|
|
||||||
private static int getSize(int valuesPerLong) {
|
|
||||||
return (BLOCK_COUNT + valuesPerLong - 1) / valuesPerLong;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a world coordinate to a chunk one.
|
* Converts a world coordinate to a chunk one.
|
||||||
*
|
*
|
||||||
@ -393,34 +124,4 @@ public class PaletteStorage implements PublicCloneable<PaletteStorage> {
|
|||||||
return xz;
|
return xz;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the index of the block on the section array based on the block position.
|
|
||||||
*
|
|
||||||
* @param x the chunk X
|
|
||||||
* @param y the chunk Y
|
|
||||||
* @param z the chunk Z
|
|
||||||
* @return the section index of the position
|
|
||||||
*/
|
|
||||||
public static int getSectionIndex(int x, int y, int z) {
|
|
||||||
y %= CHUNK_SECTION_SIZE;
|
|
||||||
return y << 8 | z << 4 | x;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fixes invalid bitsPerEntry values.
|
|
||||||
* <p>
|
|
||||||
* See https://wiki.vg/Chunk_Format#Direct
|
|
||||||
*
|
|
||||||
* @param bitsPerEntry the bits per entry value before fixing
|
|
||||||
* @return the fixed bits per entry value
|
|
||||||
*/
|
|
||||||
private static int fixBitsPerEntry(int bitsPerEntry) {
|
|
||||||
if (bitsPerEntry < MINIMUM_BITS_PER_ENTRY) {
|
|
||||||
return MINIMUM_BITS_PER_ENTRY;
|
|
||||||
} else if (MathUtils.isBetween(bitsPerEntry, 9, 14)) {
|
|
||||||
return MAXIMUM_BITS_PER_ENTRY;
|
|
||||||
}
|
|
||||||
return bitsPerEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
297
src/main/java/net/minestom/server/instance/palette/Section.java
Normal file
297
src/main/java/net/minestom/server/instance/palette/Section.java
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
package net.minestom.server.instance.palette;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.shorts.Short2ShortLinkedOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
|
||||||
|
import net.minestom.server.MinecraftServer;
|
||||||
|
import net.minestom.server.instance.Chunk;
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.utils.MathUtils;
|
||||||
|
import net.minestom.server.utils.clone.PublicCloneable;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_SIZE;
|
||||||
|
|
||||||
|
public class Section implements PublicCloneable<Section> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum bits per entry value.
|
||||||
|
*/
|
||||||
|
public final static int MAXIMUM_BITS_PER_ENTRY = 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum bits per entry value.
|
||||||
|
*/
|
||||||
|
public final static int MINIMUM_BITS_PER_ENTRY = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum bits per entry value which allow for a data palette.
|
||||||
|
*/
|
||||||
|
public final static int PALETTE_MAXIMUM_BITS = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of blocks that should be in one chunk section.
|
||||||
|
*/
|
||||||
|
public final static int BLOCK_COUNT = CHUNK_SECTION_SIZE * CHUNK_SECTION_SIZE * CHUNK_SECTION_SIZE;
|
||||||
|
|
||||||
|
// Magic values generated with "Integer.MAX_VALUE >> (31 - bitsPerIndex)" for bitsPerIndex between 4 and 15
|
||||||
|
private static final int[] MAGIC_MASKS =
|
||||||
|
{0, 0, 0, 0,
|
||||||
|
15, 31, 63, 127, 255,
|
||||||
|
511, 1023, 2047, 4095,
|
||||||
|
8191, 16383, 32767};
|
||||||
|
|
||||||
|
private long[] blocks;
|
||||||
|
|
||||||
|
// chunk section - palette index = block id
|
||||||
|
private Short2ShortLinkedOpenHashMap paletteBlockMap;
|
||||||
|
// chunk section - block id = palette index
|
||||||
|
private Short2ShortOpenHashMap blockPaletteMap;
|
||||||
|
|
||||||
|
private int bitsPerEntry;
|
||||||
|
private final int bitsIncrement;
|
||||||
|
|
||||||
|
private int valuesPerLong;
|
||||||
|
private boolean hasPalette;
|
||||||
|
|
||||||
|
protected Section(int bitsPerEntry, int bitsIncrement) {
|
||||||
|
this.bitsPerEntry = bitsPerEntry;
|
||||||
|
this.bitsIncrement = bitsIncrement;
|
||||||
|
|
||||||
|
this.valuesPerLong = Long.SIZE / bitsPerEntry;
|
||||||
|
this.hasPalette = bitsPerEntry <= PALETTE_MAXIMUM_BITS;
|
||||||
|
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockAt(int x, int y, int z, short blockId) {
|
||||||
|
if (blocks.length == 0) {
|
||||||
|
if (blockId == 0) {
|
||||||
|
// Section is empty and method is trying to place an air block, stop unnecessary computation
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the section
|
||||||
|
blocks = new long[getSize(valuesPerLong)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change to palette value
|
||||||
|
blockId = getPaletteIndex(blockId);
|
||||||
|
|
||||||
|
final int sectionIndex = getSectionIndex(x, y, z);
|
||||||
|
|
||||||
|
final int index = sectionIndex / valuesPerLong;
|
||||||
|
|
||||||
|
final int bitIndex = (sectionIndex % valuesPerLong) * bitsPerEntry;
|
||||||
|
|
||||||
|
long block = blocks[index];
|
||||||
|
{
|
||||||
|
final long clear = MAGIC_MASKS[bitsPerEntry];
|
||||||
|
|
||||||
|
block |= clear << bitIndex;
|
||||||
|
block ^= clear << bitIndex;
|
||||||
|
block |= (long) blockId << bitIndex;
|
||||||
|
|
||||||
|
blocks[index] = block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getBlockAt(int x, int y, int z) {
|
||||||
|
if (blocks.length == 0) {
|
||||||
|
// Section is not loaded, can only be air
|
||||||
|
return Block.AIR.getBlockId();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sectionIdentifier = getSectionIndex(x, y, z);
|
||||||
|
|
||||||
|
final int index = sectionIdentifier / valuesPerLong;
|
||||||
|
final int bitIndex = sectionIdentifier % valuesPerLong * bitsPerEntry;
|
||||||
|
|
||||||
|
final long value = blocks[index] >> bitIndex & MAGIC_MASKS[bitsPerEntry];
|
||||||
|
|
||||||
|
// Change to palette value and return
|
||||||
|
return hasPalette ?
|
||||||
|
paletteBlockMap.get((short) value) :
|
||||||
|
(short) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resizes the array.
|
||||||
|
* <p>
|
||||||
|
* Will create a new palette storage to set all the current blocks, and the data will be transferred to 'this'.
|
||||||
|
*
|
||||||
|
* @param newBitsPerEntry the new bits per entry count
|
||||||
|
*/
|
||||||
|
private void resize(int newBitsPerEntry) {
|
||||||
|
newBitsPerEntry = fixBitsPerEntry(newBitsPerEntry);
|
||||||
|
|
||||||
|
Section section = new Section(newBitsPerEntry, bitsIncrement);
|
||||||
|
section.paletteBlockMap = paletteBlockMap;
|
||||||
|
section.blockPaletteMap = blockPaletteMap;
|
||||||
|
|
||||||
|
for (int y = 0; y < Chunk.CHUNK_SIZE_Y; y++) {
|
||||||
|
for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
|
||||||
|
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
|
||||||
|
final short blockId = getBlockAt(x, y, z);
|
||||||
|
section.setBlockAt(x, y, z, blockId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bitsPerEntry = section.bitsPerEntry;
|
||||||
|
|
||||||
|
this.valuesPerLong = section.valuesPerLong;
|
||||||
|
this.hasPalette = section.hasPalette;
|
||||||
|
|
||||||
|
this.blocks = section.blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loops through all the sections and blocks to find unused array (empty chunk section)
|
||||||
|
* <p>
|
||||||
|
* Useful after clearing one or multiple sections of a chunk. Can be unnecessarily expensive if the chunk
|
||||||
|
* is composed of almost-empty sections since the loop will not stop until a non-air block is discovered.
|
||||||
|
*/
|
||||||
|
public synchronized void clean() {
|
||||||
|
if (blocks.length != 0) {
|
||||||
|
boolean canClear = true;
|
||||||
|
for (long blockGroup : blocks) {
|
||||||
|
if (blockGroup != 0) {
|
||||||
|
canClear = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (canClear) {
|
||||||
|
this.blocks = new long[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
this.blocks = new long[0];
|
||||||
|
this.paletteBlockMap = createPaletteBlockMap();
|
||||||
|
this.blockPaletteMap = createBlockPaletteMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long[] getBlocks() {
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Short2ShortLinkedOpenHashMap getPaletteBlockMap() {
|
||||||
|
return paletteBlockMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Short2ShortOpenHashMap getBlockPaletteMap() {
|
||||||
|
return blockPaletteMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBitsPerEntry() {
|
||||||
|
return bitsPerEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the palette index for the specified block id.
|
||||||
|
* <p>
|
||||||
|
* Also responsible for resizing the palette when full.
|
||||||
|
*
|
||||||
|
* @param blockId the block id to convert
|
||||||
|
* @return the palette index of {@code blockId}
|
||||||
|
*/
|
||||||
|
private short getPaletteIndex(short blockId) {
|
||||||
|
if (!hasPalette) {
|
||||||
|
return blockId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blockPaletteMap.containsKey(blockId)) {
|
||||||
|
// Resize the palette if full
|
||||||
|
if (paletteBlockMap.size() >= getMaxPaletteSize()) {
|
||||||
|
resize(bitsPerEntry + bitsIncrement);
|
||||||
|
}
|
||||||
|
|
||||||
|
final short paletteIndex = (short) (paletteBlockMap.lastShortKey() + 1);
|
||||||
|
paletteBlockMap.put(paletteIndex, blockId);
|
||||||
|
blockPaletteMap.put(blockId, paletteIndex);
|
||||||
|
return paletteIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockPaletteMap.get(blockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the maximum number of blocks that the current palette (could be the global one) can take.
|
||||||
|
*
|
||||||
|
* @return the number of blocks possible in the palette
|
||||||
|
*/
|
||||||
|
private int getMaxPaletteSize() {
|
||||||
|
return 1 << bitsPerEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Short2ShortLinkedOpenHashMap createPaletteBlockMap() {
|
||||||
|
Short2ShortLinkedOpenHashMap map = new Short2ShortLinkedOpenHashMap(CHUNK_SECTION_SIZE);
|
||||||
|
map.put((short) 0, (short) 0);
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Short2ShortOpenHashMap createBlockPaletteMap() {
|
||||||
|
Short2ShortOpenHashMap map = new Short2ShortOpenHashMap(CHUNK_SECTION_SIZE);
|
||||||
|
map.put((short) 0, (short) 0);
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of the block on the section array based on the block position.
|
||||||
|
*
|
||||||
|
* @param x the chunk X
|
||||||
|
* @param y the chunk Y
|
||||||
|
* @param z the chunk Z
|
||||||
|
* @return the section index of the position
|
||||||
|
*/
|
||||||
|
public static int getSectionIndex(int x, int y, int z) {
|
||||||
|
y %= CHUNK_SECTION_SIZE;
|
||||||
|
return y << 8 | z << 4 | x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the array length of one section based on the number of values which can be stored in one long.
|
||||||
|
*
|
||||||
|
* @param valuesPerLong the number of values per long
|
||||||
|
* @return the array length based on {@code valuesPerLong}
|
||||||
|
*/
|
||||||
|
private static int getSize(int valuesPerLong) {
|
||||||
|
return (BLOCK_COUNT + valuesPerLong - 1) / valuesPerLong;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixes invalid bitsPerEntry values.
|
||||||
|
* <p>
|
||||||
|
* See https://wiki.vg/Chunk_Format#Direct
|
||||||
|
*
|
||||||
|
* @param bitsPerEntry the bits per entry value before fixing
|
||||||
|
* @return the fixed bits per entry value
|
||||||
|
*/
|
||||||
|
private static int fixBitsPerEntry(int bitsPerEntry) {
|
||||||
|
if (bitsPerEntry < MINIMUM_BITS_PER_ENTRY) {
|
||||||
|
return MINIMUM_BITS_PER_ENTRY;
|
||||||
|
} else if (MathUtils.isBetween(bitsPerEntry, 9, 14)) {
|
||||||
|
return MAXIMUM_BITS_PER_ENTRY;
|
||||||
|
}
|
||||||
|
return bitsPerEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Section clone() {
|
||||||
|
try {
|
||||||
|
Section section = (Section) super.clone();
|
||||||
|
section.blocks = blocks.clone();
|
||||||
|
section.paletteBlockMap = paletteBlockMap.clone();
|
||||||
|
section.blockPaletteMap = blockPaletteMap.clone();
|
||||||
|
return section;
|
||||||
|
} catch (CloneNotSupportedException e) {
|
||||||
|
MinecraftServer.getExceptionManager().handleException(e);
|
||||||
|
throw new IllegalStateException("Weird thing happened");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package net.minestom.server.network.packet.server.play;
|
package net.minestom.server.network.packet.server.play;
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
@ -9,9 +8,11 @@ import net.minestom.server.data.Data;
|
|||||||
import net.minestom.server.instance.block.BlockManager;
|
import net.minestom.server.instance.block.BlockManager;
|
||||||
import net.minestom.server.instance.block.CustomBlock;
|
import net.minestom.server.instance.block.CustomBlock;
|
||||||
import net.minestom.server.instance.palette.PaletteStorage;
|
import net.minestom.server.instance.palette.PaletteStorage;
|
||||||
|
import net.minestom.server.instance.palette.Section;
|
||||||
import net.minestom.server.network.packet.server.ServerPacket;
|
import net.minestom.server.network.packet.server.ServerPacket;
|
||||||
import net.minestom.server.network.packet.server.ServerPacketIdentifier;
|
import net.minestom.server.network.packet.server.ServerPacketIdentifier;
|
||||||
import net.minestom.server.utils.BlockPosition;
|
import net.minestom.server.utils.BlockPosition;
|
||||||
|
import net.minestom.server.utils.BufUtils;
|
||||||
import net.minestom.server.utils.Utils;
|
import net.minestom.server.utils.Utils;
|
||||||
import net.minestom.server.utils.binary.BinaryWriter;
|
import net.minestom.server.utils.binary.BinaryWriter;
|
||||||
import net.minestom.server.utils.cache.CacheablePacket;
|
import net.minestom.server.utils.cache.CacheablePacket;
|
||||||
@ -23,7 +24,6 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
|
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class ChunkDataPacket implements ServerPacket, CacheablePacket {
|
public class ChunkDataPacket implements ServerPacket, CacheablePacket {
|
||||||
@ -63,13 +63,17 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
|
|||||||
writer.writeBoolean(fullChunk);
|
writer.writeBoolean(fullChunk);
|
||||||
|
|
||||||
int mask = 0;
|
int mask = 0;
|
||||||
ByteBuf blocks = Unpooled.buffer(MAX_BUFFER_SIZE);
|
ByteBuf blocks = BufUtils.getBuffer(MAX_BUFFER_SIZE);
|
||||||
for (byte i = 0; i < CHUNK_SECTION_COUNT; i++) {
|
for (byte i = 0; i < CHUNK_SECTION_COUNT; i++) {
|
||||||
if (fullChunk || (sections.length == CHUNK_SECTION_COUNT && sections[i] != 0)) {
|
if (fullChunk || (sections.length == CHUNK_SECTION_COUNT && sections[i] != 0)) {
|
||||||
final long[] section = paletteStorage.getSectionBlocks()[i];
|
final Section section = paletteStorage.getSections()[i];
|
||||||
if (section.length > 0) { // section contains at least one block
|
if (section == null) {
|
||||||
|
// Section not loaded
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (section.getBlocks().length > 0) { // section contains at least one block
|
||||||
mask |= 1 << i;
|
mask |= 1 << i;
|
||||||
Utils.writeBlocks(blocks, paletteStorage.getPalette(i), section, paletteStorage.getBitsPerEntry());
|
Utils.writeSectionBlocks(blocks, section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package net.minestom.server.utils;
|
package net.minestom.server.utils;
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import it.unimi.dsi.fastutil.shorts.Short2ShortLinkedOpenHashMap;
|
||||||
|
import net.minestom.server.instance.palette.Section;
|
||||||
import net.minestom.server.utils.binary.BinaryWriter;
|
import net.minestom.server.utils.binary.BinaryWriter;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -92,27 +94,32 @@ public final class Utils {
|
|||||||
return new UUID(uuidMost, uuidLeast);
|
return new UUID(uuidMost, uuidLeast);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void writeBlocks(ByteBuf buffer, short[] palette, long[] blocksId, int bitsPerEntry) {
|
public static void writeSectionBlocks(ByteBuf buffer, Section section) {
|
||||||
/*short count = 0;
|
/*short count = 0;
|
||||||
for (short id : blocksId)
|
for (short id : blocksId)
|
||||||
if (id != 0)
|
if (id != 0)
|
||||||
count++;*/
|
count++;*/
|
||||||
|
|
||||||
|
final int bitsPerEntry = section.getBitsPerEntry();
|
||||||
|
|
||||||
//buffer.writeShort(count);
|
//buffer.writeShort(count);
|
||||||
|
// TODO count blocks
|
||||||
buffer.writeShort(200);
|
buffer.writeShort(200);
|
||||||
buffer.writeByte((byte) bitsPerEntry);
|
buffer.writeByte((byte) bitsPerEntry);
|
||||||
|
|
||||||
// Palette
|
// Palette
|
||||||
if (bitsPerEntry < 9) {
|
if (bitsPerEntry < 9) {
|
||||||
// Palette has to exist
|
// Palette has to exist
|
||||||
writeVarIntBuf(buffer, palette.length);
|
final Short2ShortLinkedOpenHashMap paletteBlockMap = section.getPaletteBlockMap();
|
||||||
for (short paletteValue : palette) {
|
writeVarIntBuf(buffer, paletteBlockMap.size());
|
||||||
|
for (short paletteValue : paletteBlockMap.values()) {
|
||||||
writeVarIntBuf(buffer, paletteValue);
|
writeVarIntBuf(buffer, paletteValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeVarIntBuf(buffer, blocksId.length);
|
final long[] blocks = section.getBlocks();
|
||||||
for (long datum : blocksId) {
|
writeVarIntBuf(buffer, blocks.length);
|
||||||
|
for (long datum : blocks) {
|
||||||
buffer.writeLong(datum);
|
buffer.writeLong(datum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.IntFunction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenient interface to deep-copy single object or collections.
|
* Convenient interface to deep-copy single object or collections.
|
||||||
@ -14,15 +15,23 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
public final class CloneUtils {
|
public final class CloneUtils {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static <T extends PublicCloneable> T optionalClone(@Nullable T object) {
|
public static <T extends PublicCloneable<T>> T optionalClone(@Nullable T object) {
|
||||||
return object != null ? (T) object.clone() : null;
|
return object != null ? object.clone() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static <T extends PublicCloneable> CopyOnWriteArrayList cloneCopyOnWriteArrayList(@NotNull List<T> list) {
|
public static <T extends PublicCloneable<T>> CopyOnWriteArrayList<T> cloneCopyOnWriteArrayList(@NotNull List<T> list) {
|
||||||
CopyOnWriteArrayList<T> result = new CopyOnWriteArrayList<>();
|
CopyOnWriteArrayList<T> result = new CopyOnWriteArrayList<>();
|
||||||
for (T element : list) {
|
for (T element : list) {
|
||||||
result.add((T) element.clone());
|
result.add(element.clone());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends PublicCloneable<T>> T[] cloneArray(@NotNull T[] array, IntFunction<T[]> arraySupplier) {
|
||||||
|
T[] result = arraySupplier.apply(array.length);
|
||||||
|
for (int i = 0; i < result.length; i++) {
|
||||||
|
result[i] = optionalClone(array[i]);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user