mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Biomes
This commit is contained in:
parent
a55784ea50
commit
5511ef8da8
7 changed files with 490 additions and 8 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import com.github.steveice10.mc.protocol.data.game.world.block.BlockState;
|
||||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
import org.geysermc.connector.network.translators.TranslatorsInit;
|
import org.geysermc.connector.network.translators.TranslatorsInit;
|
||||||
import org.geysermc.connector.network.translators.block.BlockEntry;
|
import org.geysermc.connector.network.translators.block.BlockEntry;
|
||||||
|
import org.geysermc.connector.world.BiomePalette;
|
||||||
import org.geysermc.connector.world.chunk.ChunkSection;
|
import org.geysermc.connector.world.chunk.ChunkSection;
|
||||||
|
|
||||||
public class ChunkUtils {
|
public class ChunkUtils {
|
||||||
|
@ -19,17 +20,27 @@ public class ChunkUtils {
|
||||||
|
|
||||||
chunkData.sections = new ChunkSection[chunkSectionCount];
|
chunkData.sections = new ChunkSection[chunkSectionCount];
|
||||||
|
|
||||||
for(int biome = 0; biome < 256; biome++) {
|
int[] biomesConverted = new int[256];
|
||||||
if (column.getBiomeData()[biome] <= Byte.MAX_VALUE) {
|
|
||||||
chunkData.biomes[biome] = (byte) (column.getBiomeData()[biome]);
|
try {
|
||||||
} else {
|
for (int biomeX = 0; biomeX < 16; biomeX++) {
|
||||||
chunkData.biomes[biome] = (byte) (column.getBiomeData()[biome] - 255);
|
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()) {
|
for(CompoundTag tag : column.getTileEntities()) {
|
||||||
System.out.println(tag.toString());
|
//TODO: Tiles
|
||||||
|
//System.out.println(tag.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int chunkY = 0; chunkY < chunkSectionCount; chunkY++) {
|
for (int chunkY = 0; chunkY < chunkSectionCount; chunkY++) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T> extends ThreadLocal<T> implements Iterable<T> {
|
||||||
|
private ThreadLocal<T> flag;
|
||||||
|
private ConcurrentLinkedDeque<T> 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<T> 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<T> getAll() {
|
||||||
|
return Collections.unmodifiableCollection(allValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void finalize() throws Throwable {
|
||||||
|
clean(this);
|
||||||
|
super.finalize();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.geysermc.connector.utils;
|
||||||
|
|
||||||
|
public class ThreadCache {
|
||||||
|
public static final IterableThreadLocal<int[]> intCache256 = new IterableThreadLocal<int[]>() {
|
||||||
|
@Override
|
||||||
|
public int[] init() {
|
||||||
|
return new int[256];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue