diff --git a/connector/src/main/java/org/geysermc/connector/utils/BitArray256.java b/connector/src/main/java/org/geysermc/connector/utils/BitArray256.java new file mode 100644 index 000000000..f4308fc2e --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/BitArray256.java @@ -0,0 +1,91 @@ +package org.geysermc.connector.utils; + +/** + * This file property the NukkitX project + * https://github.com/NukkitX/Nukkit + * @author https://github.com/boy0001/ + */ +public final class BitArray256 { + private final int bitsPerEntry; + public final long[] data; + + public BitArray256(int bitsPerEntry) { + this.bitsPerEntry = bitsPerEntry; + int longLen = (this.bitsPerEntry * 256) >> 6; + this.data = new long[longLen]; + } + + public BitArray256(BitArray256 other) { + this.bitsPerEntry = other.bitsPerEntry; + this.data = other.data.clone(); + } + + public final void setAt(int index, int value) { + int bitIndexStart = index * bitsPerEntry; + int longIndexStart = bitIndexStart >> 6; + int localBitIndexStart = bitIndexStart & 63; + this.data[longIndexStart] = this.data[longIndexStart] & ~((long) ((1 << bitsPerEntry) - 1) << localBitIndexStart) | ((long) value) << localBitIndexStart; + + if(localBitIndexStart > 64 - bitsPerEntry) { + int longIndexEnd = longIndexStart + 1; + int localShiftStart = 64 - localBitIndexStart; + int localShiftEnd = bitsPerEntry - localShiftStart; + this.data[longIndexEnd] = this.data[longIndexEnd] >>> localShiftEnd << localShiftEnd | (((long) value) >> localShiftStart); + } + } + + public final int getAt(int index) { + int bitIndexStart = index * bitsPerEntry; + + int longIndexStart = bitIndexStart >> 6; + + int localBitIndexStart = bitIndexStart & 63; + if(localBitIndexStart <= 64 - bitsPerEntry) { + return (int)(this.data[longIndexStart] >>> localBitIndexStart & ((1 << bitsPerEntry) - 1)); + } else { + int localShift = 64 - localBitIndexStart; + return (int) ((this.data[longIndexStart] >>> localBitIndexStart | this.data[longIndexStart + 1] << localShift) & ((1 << bitsPerEntry) - 1)); + } + } + + public final void fromRaw(int[] arr) { + for (int i = 0; i < arr.length; i++) { + setAt(i, arr[i]); + } + } + + public BitArray256 grow(int newBitsPerEntry) { + int amtGrow = newBitsPerEntry - this.bitsPerEntry; + if (amtGrow <= 0) return this; + BitArray256 newBitArray = new BitArray256(newBitsPerEntry); + + int[] buffer = ThreadCache.intCache256.get(); + toRaw(buffer); + newBitArray.fromRaw(buffer); + + return newBitArray; + } + + public BitArray256 growSlow(int bitsPerEntry) { + BitArray256 newBitArray = new BitArray256(bitsPerEntry); + for (int i = 0; i < 256; i++) { + newBitArray.setAt(i, getAt(i)); + } + return newBitArray; + } + + public final int[] toRaw(int[] buffer) { + for (int i = 0; i < buffer.length; i++) { + buffer[i] = getAt(i); + } + return buffer; + } + + public final int[] toRaw() { + return toRaw(new int[256]); + } + + public BitArray256 clone() { + return new BitArray256(this); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java index 229b2c2c0..c7928546c 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java @@ -6,6 +6,7 @@ import com.github.steveice10.mc.protocol.data.game.world.block.BlockState; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import org.geysermc.connector.network.translators.TranslatorsInit; import org.geysermc.connector.network.translators.block.BlockEntry; +import org.geysermc.connector.world.BiomePalette; import org.geysermc.connector.world.chunk.ChunkSection; public class ChunkUtils { @@ -19,17 +20,27 @@ public class ChunkUtils { chunkData.sections = new ChunkSection[chunkSectionCount]; - for(int biome = 0; biome < 256; biome++) { - if (column.getBiomeData()[biome] <= Byte.MAX_VALUE) { - chunkData.biomes[biome] = (byte) (column.getBiomeData()[biome]); - } else { - chunkData.biomes[biome] = (byte) (column.getBiomeData()[biome] - 255); + int[] biomesConverted = new int[256]; + + try { + for (int biomeX = 0; biomeX < 16; biomeX++) { + for (int biomeZ = 0; biomeZ < 16; biomeZ++) { + biomesConverted[(biomeX << 4) | biomeZ] = column.getBiomeData()[(biomeZ * 4) | biomeX]; + } } - } - + + BiomePalette palette = new BiomePalette(biomesConverted); + + for (int biomeX = 0; biomeX < 16; biomeX++) { + for (int biomeZ = 0; biomeZ < 16; biomeZ++) { + chunkData.biomes[(biomeX << 4) | biomeZ] = (byte) (palette.get(biomeX, biomeZ)); + } + } + } catch (Exception e) {} for(CompoundTag tag : column.getTileEntities()) { - System.out.println(tag.toString()); + //TODO: Tiles + //System.out.println(tag.toString()); } for (int chunkY = 0; chunkY < chunkSectionCount; chunkY++) { diff --git a/connector/src/main/java/org/geysermc/connector/utils/IntPalette.java b/connector/src/main/java/org/geysermc/connector/utils/IntPalette.java new file mode 100644 index 000000000..17d1972a0 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/IntPalette.java @@ -0,0 +1,106 @@ +package org.geysermc.connector.utils; + +import java.util.Arrays; + +/** + * This file property the NukkitX project + * https://github.com/NukkitX/Nukkit + * @author https://github.com/boy0001/ + */ +public class IntPalette { + private static int[] INT0 = new int[0]; + private int[] keys = INT0; + private int lastIndex = Integer.MIN_VALUE; + + public void add(int key) { + keys = insert(key); + lastIndex = Integer.MIN_VALUE; + } + + protected void set(int[] keys) { + this.keys = keys; + lastIndex = Integer.MIN_VALUE; + } + + private int[] insert(int val) { + lastIndex = Integer.MIN_VALUE; + if (keys.length == 0) { + return new int[] { val }; + } + else if (val < keys[0]) { + int[] s = new int[keys.length + 1]; + System.arraycopy(keys, 0, s, 1, keys.length); + s[0] = val; + return s; + } else if (val > keys[keys.length - 1]) { + int[] s = Arrays.copyOf(keys, keys.length + 1); + s[keys.length] = val; + return s; + } + int[] s = Arrays.copyOf(keys, keys.length + 1); + for (int i = 0; i < s.length; i++) { + if (keys[i] < val) { + continue; + } + System.arraycopy(keys, i, s, i + 1, s.length - i - 1); + s[i] = val; + break; + } + return s; + } + + public int getKey(int index) { + return keys[index]; + } + + public int getValue(int key) { + int lastTmp = lastIndex; + boolean hasLast = lastTmp != Integer.MIN_VALUE; + int index; + if (hasLast) { + int lastKey = keys[lastTmp]; + if (lastKey == key) return lastTmp; + if (lastKey > key) { + index = binarySearch0(0, lastTmp, key); + } else { + index = binarySearch0(lastTmp + 1, keys.length, key); + } + } else { + index = binarySearch0(0, keys.length, key); + } + if (index >= keys.length || index < 0) { + return lastIndex = Integer.MIN_VALUE; + } else { + return lastIndex = index; + } + } + + private int binarySearch0(int fromIndex, int toIndex, int key) { + int low = fromIndex; + int high = toIndex - 1; + + while (low <= high) { + int mid = (low + high) >>> 1; + int midVal = keys[mid]; + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return mid; // key found + } + return -(low + 1); // key not found. + } + + public int length() { + return keys.length; + } + + public IntPalette clone() { + IntPalette p = new IntPalette(); + p.keys = this.keys != INT0 ? this.keys.clone() : INT0; + p.lastIndex = this.lastIndex; + return p; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/IterableThreadLocal.java b/connector/src/main/java/org/geysermc/connector/utils/IterableThreadLocal.java new file mode 100644 index 000000000..41bbc3699 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/IterableThreadLocal.java @@ -0,0 +1,123 @@ +package org.geysermc.connector.utils; + +import java.lang.ref.Reference; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.ConcurrentLinkedDeque; + +public abstract class IterableThreadLocal extends ThreadLocal implements Iterable { + private ThreadLocal flag; + private ConcurrentLinkedDeque allValues = new ConcurrentLinkedDeque<>(); + + public IterableThreadLocal() { + } + + @Override + protected final T initialValue() { + T value = init(); + if (value != null) { + allValues.add(value); + } + return value; + } + + @Override + public final Iterator iterator() { + return getAll().iterator(); + } + + public T init() { + return null; + } + + public void clean() { + IterableThreadLocal.clean(this); + } + + public static void clean(ThreadLocal instance) { + try { + ThreadGroup rootGroup = Thread.currentThread( ).getThreadGroup( ); + ThreadGroup parentGroup; + while ( ( parentGroup = rootGroup.getParent() ) != null ) { + rootGroup = parentGroup; + } + Thread[] threads = new Thread[ rootGroup.activeCount() ]; + if (threads.length != 0) { + while (rootGroup.enumerate(threads, true) == threads.length) { + threads = new Thread[threads.length * 2]; + } + } + Field tl = Thread.class.getDeclaredField("threadLocals"); + tl.setAccessible(true); + Method methodRemove = null; + for (Thread thread : threads) { + if (thread != null) { + Object tlm = tl.get(thread); + if (tlm != null) { + if (methodRemove == null) { + methodRemove = tlm.getClass().getDeclaredMethod("remove", ThreadLocal.class); + methodRemove.setAccessible(true); + } + if (methodRemove != null) { + try { + methodRemove.invoke(tlm, instance); + } catch (Throwable ignore) {} + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void cleanAll() { + try { + // Get a reference to the thread locals table of the current thread + Thread thread = Thread.currentThread(); + Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); + threadLocalsField.setAccessible(true); + Object threadLocalTable = threadLocalsField.get(thread); + + // Get a reference to the array holding the thread local variables inside the + // ThreadLocalMap of the current thread + Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); + Field tableField = threadLocalMapClass.getDeclaredField("table"); + tableField.setAccessible(true); + Object table = tableField.get(threadLocalTable); + + // The key to the ThreadLocalMap is a WeakReference object. The referent field of this object + // is a reference to the actual ThreadLocal variable + Field referentField = Reference.class.getDeclaredField("referent"); + referentField.setAccessible(true); + + for (int i = 0; i < Array.getLength(table); i++) { + // Each entry in the table array of ThreadLocalMap is an Entry object + // representing the thread local reference and its value + Object entry = Array.get(table, i); + if (entry != null) { + // Get a reference to the thread local object and remove it from the table + ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry); + clean(threadLocal); + } + } + } catch(Exception e) { + // We will tolerate an exception here and just log it + throw new IllegalStateException(e); + } + } + + public final Collection getAll() { + return Collections.unmodifiableCollection(allValues); + } + + @Override + protected void finalize() throws Throwable { + clean(this); + super.finalize(); + } +} \ No newline at end of file diff --git a/connector/src/main/java/org/geysermc/connector/utils/MathHelper.java b/connector/src/main/java/org/geysermc/connector/utils/MathHelper.java new file mode 100644 index 000000000..269598f59 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/MathHelper.java @@ -0,0 +1,11 @@ +package org.geysermc.connector.utils; + +/** + * This file property the NukkitX project + * https://github.com/NukkitX/Nukkit + */ +public class MathHelper { + public static int log2(int bits) { + return Integer.SIZE - Integer.numberOfLeadingZeros(bits); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/ThreadCache.java b/connector/src/main/java/org/geysermc/connector/utils/ThreadCache.java new file mode 100644 index 000000000..2e8806266 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/ThreadCache.java @@ -0,0 +1,10 @@ +package org.geysermc.connector.utils; + +public class ThreadCache { + public static final IterableThreadLocal intCache256 = new IterableThreadLocal() { + @Override + public int[] init() { + return new int[256]; + } + }; +} diff --git a/connector/src/main/java/org/geysermc/connector/world/BiomePalette.java b/connector/src/main/java/org/geysermc/connector/world/BiomePalette.java new file mode 100644 index 000000000..6a1cd1137 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/world/BiomePalette.java @@ -0,0 +1,130 @@ +package org.geysermc.connector.world; + +import org.geysermc.connector.utils.BitArray256; +import org.geysermc.connector.utils.IntPalette; +import org.geysermc.connector.utils.MathHelper; +import org.geysermc.connector.utils.ThreadCache; + +import java.util.Arrays; + +/** + * This file property the NukkitX project + * https://github.com/NukkitX/Nukkit + */ +public class BiomePalette { + private int biome; + private BitArray256 encodedData; + private IntPalette palette; + + private BiomePalette(BiomePalette clone) { + this.biome = clone.biome; + if (clone.encodedData != null) { + this.encodedData = clone.encodedData.clone(); + this.palette = clone.palette.clone(); + } + } + + public BiomePalette(int[] biomeColors) { + for (int i = 0; i < 256; i++) { + set(i, biomeColors[i]); + } + } + + public BiomePalette() { + this.biome = Integer.MIN_VALUE; + } + + public int get(int x, int z) { + return get(getIndex(x, z)); + } + + public synchronized int get(int index) { + if (encodedData == null) return biome; + return palette.getKey(encodedData.getAt(index)); + } + + public void set(int x, int z, int value) { + set(getIndex(x, z), value); + } + + public synchronized void set(int index, int value) { + if (encodedData == null) { + if (value == biome) return; + if (biome == Integer.MIN_VALUE) { + biome = value; + return; + } + synchronized (this) { + palette = new IntPalette(); + palette.add(biome); + palette.add(value); + encodedData = new BitArray256(1); + if (value < biome) { + Arrays.fill(encodedData.data, -1); + encodedData.setAt(index, 0); + } else { + encodedData.setAt(index, 1); + } + return; + } + } + + int encodedValue = palette.getValue(value); + if (encodedValue != Integer.MIN_VALUE) { + encodedData.setAt(index, encodedValue); + } else { + synchronized (this) { + int[] raw = encodedData.toRaw(ThreadCache.intCache256.get()); + + // TODO skip remapping of raw data and use grow instead if `remap` + // boolean remap = value < palette.getValue(palette.length() - 1); + + for (int i = 0; i < 256; i++) { + raw[i] = palette.getKey(raw[i]); + } + + int oldRaw = raw[4]; + + raw[index] = value; + + palette.add(value); + + int oldBits = MathHelper.log2(palette.length() - 2); + int newBits = MathHelper.log2(palette.length() - 1); + if (oldBits != newBits) { + encodedData = new BitArray256(newBits); + } + + for (int i = 0; i < raw.length; i++) { + raw[i] = palette.getValue(raw[i]); + } + + encodedData.fromRaw(raw); + } + } + + } + + public synchronized int[] toRaw() { + int[] buffer = ThreadCache.intCache256.get(); + if (encodedData == null) { + Arrays.fill(buffer, biome); + } else { + synchronized (this) { + buffer = encodedData.toRaw(buffer); + for (int i = 0; i < 256; i++) { + buffer[i] = palette.getKey(buffer[i]); + } + } + } + return buffer; + } + + public int getIndex(int x, int z) { + return (z << 4) | x; + } + + public synchronized BiomePalette clone() { + return new BiomePalette(this); + } +}