Basic chunk serializer & reworked chunk multi-threading

This commit is contained in:
TheMode 2019-08-23 23:55:09 +02:00
parent d5d3dab6c7
commit b933a83c31
33 changed files with 657 additions and 168 deletions

View File

@ -16,4 +16,5 @@ dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
implementation 'com.github.Adamaq01:ozao-net:2.3.1'
compile 'com.github.Querz:NBT:4.1'
implementation 'com.github.luben:zstd-jni:1.4.3-1'
}

View File

@ -23,9 +23,9 @@ public class Main {
// Thread number
public static final int THREAD_COUNT_PACKET_WRITER = 3;
public static final int THREAD_COUNT_CHUNK_IO = 2;
public static final int THREAD_COUNT_CHUNK_BATCH = 2;
public static final int THREAD_COUNT_OBJECTS_ENTITIES = 2;
public static final int THREAD_COUNT_CREATURES_ENTITIES = 2;
public static final int THREAD_COUNT_ENTITIES = 2;
public static final int THREAD_COUNT_PLAYERS_ENTITIES = 2;
public static final int TICK_MS = 50;

View File

@ -0,0 +1,21 @@
package fr.themode.minestom.data;
import java.util.concurrent.ConcurrentHashMap;
public class Data {
private ConcurrentHashMap<String, Object> data = new ConcurrentHashMap();
private ConcurrentHashMap<String, DataType> dataType = new ConcurrentHashMap<>();
public <T> void set(String key, T value, DataType<T> type) {
this.data.put(key, value);
this.dataType.put(key, type);
}
public <T> T get(String key) {
return (T) data.get(key);
}
// TODO serialize
}

View File

@ -0,0 +1,9 @@
package fr.themode.minestom.data;
public interface DataContainer {
Data getData();
void setData(Data data);
}

View File

@ -0,0 +1,15 @@
package fr.themode.minestom.data;
import fr.themode.minestom.data.type.IntegerData;
public abstract class DataType<T> {
public static final DataType INTEGER = new IntegerData();
public abstract byte[] encode(T value);
public abstract T decode(byte[] value);
// TODO get object type class ?
}

View File

@ -0,0 +1,17 @@
package fr.themode.minestom.data.type;
import fr.themode.minestom.data.DataType;
import fr.themode.minestom.utils.SerializerUtils;
public class IntegerData extends DataType<Integer> {
@Override
public byte[] encode(Integer value) {
return SerializerUtils.intToBytes(value);
}
@Override
public Integer decode(byte[] value) {
return SerializerUtils.bytesToInt(value);
}
}

View File

@ -3,6 +3,8 @@ package fr.themode.minestom.entity;
import fr.adamaq01.ozao.net.Buffer;
import fr.themode.minestom.Main;
import fr.themode.minestom.Viewable;
import fr.themode.minestom.data.Data;
import fr.themode.minestom.data.DataContainer;
import fr.themode.minestom.event.Callback;
import fr.themode.minestom.event.CancellableEvent;
import fr.themode.minestom.event.Event;
@ -17,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class Entity implements Viewable {
public abstract class Entity implements Viewable, DataContainer {
private static Map<Integer, Entity> entityById = new HashMap<>();
private static AtomicInteger lastEntityId = new AtomicInteger();
@ -41,6 +43,7 @@ public abstract class Entity implements Viewable {
protected Entity vehicle;
private Map<Class<Event>, Callback> eventCallbacks = new ConcurrentHashMap<>();
private Set<Player> viewers = new CopyOnWriteArraySet<>();
private Data data;
private Set<Entity> passengers = new CopyOnWriteArraySet<>();
protected UUID uuid;
@ -124,7 +127,20 @@ public abstract class Entity implements Viewable {
return Collections.unmodifiableSet(viewers);
}
@Override
public Data getData() {
return data;
}
@Override
public void setData(Data data) {
this.data = data;
}
public void tick() {
if (instance == null)
return;
if (scheduledRemoveTime != 0) { // Any entity with scheduled remove does not update
boolean finished = System.currentTimeMillis() >= scheduledRemoveTime;
if (finished) {
@ -132,10 +148,18 @@ public abstract class Entity implements Viewable {
}
return;
}
if (shouldUpdate()) {
if (shouldRemove()) {
remove();
return;
} else if (shouldUpdate()) {
update();
this.lastUpdate = System.currentTimeMillis();
}
if (shouldRemove()) {
remove();
}
}
public <E extends Event> void setEventCallback(Class<E> eventClass, Callback<E> callback) {
@ -286,6 +310,7 @@ public abstract class Entity implements Viewable {
synchronized (entityById) {
entityById.remove(id);
}
instance.removeEntity(this);
}
public void scheduleRemove(long delay) {

View File

@ -6,8 +6,11 @@ import fr.themode.minestom.net.packet.server.play.SpawnMobPacket;
import fr.themode.minestom.net.player.PlayerConnection;
import fr.themode.minestom.utils.Position;
// TODO viewers synchronization each X ticks?
public abstract class EntityCreature extends LivingEntity {
protected boolean isDead;
public EntityCreature(int entityType) {
super(entityType);
}
@ -33,6 +36,7 @@ public abstract class EntityCreature extends LivingEntity {
}
public void kill() {
this.isDead = true;
triggerStatus((byte) 3);
scheduleRemove(1000);
}
@ -54,4 +58,8 @@ public abstract class EntityCreature extends LivingEntity {
playerConnection.sendPacket(spawnMobPacket);
playerConnection.sendPacket(getMetadataPacket());
}
public boolean isDead() {
return isDead;
}
}

View File

@ -1,11 +1,14 @@
package fr.themode.minestom.entity;
import fr.themode.minestom.Main;
import fr.themode.minestom.event.PlayerLoginEvent;
import fr.themode.minestom.event.PlayerSpawnPacket;
import fr.themode.minestom.instance.Chunk;
import fr.themode.minestom.instance.Instance;
import fr.themode.minestom.instance.InstanceManager;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -13,50 +16,51 @@ public class EntityManager {
private static InstanceManager instanceManager = Main.getInstanceManager();
private ExecutorService objectsPool = Executors.newFixedThreadPool(Main.THREAD_COUNT_OBJECTS_ENTITIES);
private ExecutorService creaturesPool = Executors.newFixedThreadPool(Main.THREAD_COUNT_CREATURES_ENTITIES);
private ExecutorService entitiesPool = Executors.newFixedThreadPool(Main.THREAD_COUNT_ENTITIES);
private ExecutorService playersPool = Executors.newFixedThreadPool(Main.THREAD_COUNT_PLAYERS_ENTITIES);
// TODO API for custom thread division (
private ConcurrentLinkedQueue<Player> waitingPlayers = new ConcurrentLinkedQueue<>();
public void update() {
waitingPlayersTick();
for (Instance instance : instanceManager.getInstances()) {
testTick2(instance); // TODO optimize update engine for when there are too many entities on one chunk
}
}
private void waitingPlayersTick() {
Player waitingPlayer = null;
while ((waitingPlayer = waitingPlayers.poll()) != null) {
final Player playerCache = waitingPlayer;
playersPool.submit(() -> {
PlayerLoginEvent loginEvent = new PlayerLoginEvent();
playerCache.callEvent(PlayerLoginEvent.class, loginEvent);
Instance spawningInstance = loginEvent.getSpawningInstance() == null ? instanceManager.createInstance() : loginEvent.getSpawningInstance();
spawningInstance.loadChunk(playerCache.getPosition(), chunk -> {
playerCache.spawned = true;
playerCache.setInstance(spawningInstance);
PlayerSpawnPacket spawnPacket = new PlayerSpawnPacket();
playerCache.callEvent(PlayerSpawnPacket.class, spawnPacket);
});
});
}
}
private void testTick2(Instance instance) {
for (Chunk chunk : instance.getChunks()) {
Set<ObjectEntity> objects = chunk.getObjectEntities();
Set<EntityCreature> creatures = chunk.getCreatures();
Set<Player> players = chunk.getPlayers();
if (!objects.isEmpty()) {
objectsPool.submit(() -> {
for (ObjectEntity objectEntity : objects) {
boolean shouldRemove = objectEntity.shouldRemove();
if (!shouldRemove) {
objectEntity.tick();
}
if (objectEntity.shouldRemove()) {
instance.removeEntity(objectEntity);
}
}
});
}
if (!creatures.isEmpty()) {
creaturesPool.submit(() -> {
if (!creatures.isEmpty() || !objects.isEmpty()) {
entitiesPool.submit(() -> {
for (EntityCreature creature : creatures) {
boolean shouldRemove = creature.shouldRemove();
if (!shouldRemove) {
creature.tick();
}
if (creature.shouldRemove()) {
instance.removeEntity(creature);
}
creature.tick();
}
for (ObjectEntity objectEntity : objects) {
objectEntity.tick();
}
});
}
@ -64,21 +68,18 @@ public class EntityManager {
if (!players.isEmpty()) {
playersPool.submit(() -> {
for (Player player : players) {
boolean shouldRemove = player.shouldRemove();
if (!shouldRemove) {
player.tick();
}
if (player.shouldRemove()) {
instance.removeEntity(player);
}
player.tick();
}
});
}
}
}
private void testTick(Instance instance) {
public void addWaitingPlayer(Player player) {
this.waitingPlayers.add(player);
}
/*private void testTick(Instance instance) {
// Creatures
for (EntityCreature creature : instance.getCreatures()) {
creaturesPool.submit(() -> {
@ -106,6 +107,6 @@ public class EntityManager {
}
});
}
}
}*/
}

View File

@ -34,7 +34,7 @@ public class ItemEntity extends ObjectEntity {
}
@Override
public int getData() {
public int getObjectData() {
return 1;
}

View File

@ -3,13 +3,14 @@ package fr.themode.minestom.entity;
import fr.themode.minestom.net.packet.server.play.SpawnObjectPacket;
import fr.themode.minestom.net.player.PlayerConnection;
// TODO viewers synchronization each X ticks?
public abstract class ObjectEntity extends Entity {
public ObjectEntity(int entityType) {
super(entityType);
}
public abstract int getData();
public abstract int getObjectData();
@Override
public void addViewer(Player player) {
@ -21,7 +22,7 @@ public abstract class ObjectEntity extends Entity {
spawnObjectPacket.uuid = getUuid();
spawnObjectPacket.type = getEntityType();
spawnObjectPacket.position = getPosition();
spawnObjectPacket.data = getData();
spawnObjectPacket.data = getObjectData();
playerConnection.sendPacket(spawnObjectPacket);
playerConnection.sendPacket(getMetadataPacket());
}

View File

@ -1,12 +1,14 @@
package fr.themode.minestom.entity;
import fr.themode.minestom.Main;
import fr.themode.minestom.bossbar.BossBar;
import fr.themode.minestom.chat.Chat;
import fr.themode.minestom.event.AttackEvent;
import fr.themode.minestom.event.BlockPlaceEvent;
import fr.themode.minestom.event.PickupItemEvent;
import fr.themode.minestom.entity.demo.ChickenCreature;
import fr.themode.minestom.event.*;
import fr.themode.minestom.instance.Chunk;
import fr.themode.minestom.instance.CustomBlock;
import fr.themode.minestom.instance.Instance;
import fr.themode.minestom.instance.demo.ChunkGeneratorDemo;
import fr.themode.minestom.inventory.Inventory;
import fr.themode.minestom.inventory.PlayerInventory;
import fr.themode.minestom.item.ItemStack;
@ -19,6 +21,7 @@ import fr.themode.minestom.utils.Position;
import fr.themode.minestom.world.Dimension;
import fr.themode.minestom.world.LevelType;
import java.io.File;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
@ -36,6 +39,8 @@ public class Player extends LivingEntity {
private Dimension dimension;
private GameMode gameMode;
private LevelType levelType;
// DEBUG
private static Instance instance;
private PlayerSettings settings;
private PlayerInventory inventory;
private short heldSlot;
@ -56,6 +61,23 @@ public class Player extends LivingEntity {
private float sideways;
private float forward;
static {
ChunkGeneratorDemo chunkGeneratorDemo = new ChunkGeneratorDemo();
instance = Main.getInstanceManager().createInstance(new File("C:\\Users\\themo\\OneDrive\\Bureau\\Minestom data"));
//instance = Main.getInstanceManager().createInstance();
instance.setChunkGenerator(chunkGeneratorDemo);
int loopStart = -2;
int loopEnd = 2;
long time = System.currentTimeMillis();
for (int x = loopStart; x < loopEnd; x++)
for (int z = loopStart; z < loopEnd; z++) {
instance.loadChunk(x, z);
}
System.out.println("Time to load all chunks: " + (System.currentTimeMillis() - time) + " ms");
}
protected boolean spawned;
public Player(UUID uuid, String username, PlayerConnection playerConnection) {
super(93); // FIXME verify
this.uuid = uuid;
@ -96,7 +118,39 @@ public class Player extends LivingEntity {
});
setEventCallback(BlockPlaceEvent.class, event -> {
sendMessage("Placed block!");
/*sendMessage("Placed block! " + event.getHand());
Data data = new Data();
data.set("test", 5, DataType.INTEGER);
setData(data);
sendMessage("Data: " + getData().get("test"));*/
if (event.getHand() != Hand.MAIN)
return;
sendMessage("Save chunk data...");
long time = System.currentTimeMillis();
getInstance().saveToFolder(() -> {
sendMessage("Saved in " + (System.currentTimeMillis() - time) + " ms");
});
});
// TODO loginevent set instance
setEventCallback(PlayerLoginEvent.class, event -> {
System.out.println("PLAYER LOGIN EVENT");
event.setSpawningInstance(instance);
});
setEventCallback(PlayerSpawnPacket.class, event -> {
System.out.println("TELEPORT");
teleport(new Position(0, 66, 0));
for (int cx = 0; cx < 4; cx++)
for (int cz = 0; cz < 4; cz++) {
ChickenCreature chickenCreature = new ChickenCreature();
chickenCreature.refreshPosition(0 + (float) cx * 1, 65, 0 + (float) cz * 1);
//chickenCreature.setOnFire(true);
chickenCreature.setInstance(instance);
//chickenCreature.addPassenger(player);
}
});
}
@ -108,7 +162,7 @@ public class Player extends LivingEntity {
}
// Target block stage
if (instance != null && targetCustomBlock != null) {
if (targetCustomBlock != null) {
int timeBreak = targetCustomBlock.getBreakDelay(this);
int animationCount = 10;
long since = System.currentTimeMillis() - targetBlockTime;
@ -124,33 +178,31 @@ public class Player extends LivingEntity {
}
// Item pickup
if (instance != null) {
Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks
Set<ObjectEntity> objectEntities = chunk.getObjectEntities();
for (ObjectEntity objectEntity : objectEntities) {
if (objectEntity instanceof ItemEntity) {
ItemEntity itemEntity = (ItemEntity) objectEntity;
if (!itemEntity.isPickable())
continue;
float distance = getDistance(objectEntity);
if (distance <= 2.04) {
synchronized (itemEntity) {
if (itemEntity.shouldRemove())
continue;
ItemStack item = itemEntity.getItemStack();
PickupItemEvent pickupItemEvent = new PickupItemEvent(item);
callCancellableEvent(PickupItemEvent.class, pickupItemEvent, () -> {
boolean result = getInventory().addItemStack(item);
if (result) {
CollectItemPacket collectItemPacket = new CollectItemPacket();
collectItemPacket.collectedEntityId = itemEntity.getEntityId();
collectItemPacket.collectorEntityId = getEntityId();
collectItemPacket.pickupItemCount = item.getAmount();
sendPacketToViewersAndSelf(collectItemPacket);
objectEntity.remove();
}
});
}
Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks
Set<ObjectEntity> objectEntities = chunk.getObjectEntities();
for (ObjectEntity objectEntity : objectEntities) {
if (objectEntity instanceof ItemEntity) {
ItemEntity itemEntity = (ItemEntity) objectEntity;
if (!itemEntity.isPickable())
continue;
float distance = getDistance(objectEntity);
if (distance <= 2.04) {
synchronized (itemEntity) {
if (itemEntity.shouldRemove())
continue;
ItemStack item = itemEntity.getItemStack();
PickupItemEvent pickupItemEvent = new PickupItemEvent(item);
callCancellableEvent(PickupItemEvent.class, pickupItemEvent, () -> {
boolean result = getInventory().addItemStack(item);
if (result) {
CollectItemPacket collectItemPacket = new CollectItemPacket();
collectItemPacket.collectedEntityId = itemEntity.getEntityId();
collectItemPacket.collectorEntityId = getEntityId();
collectItemPacket.pickupItemCount = item.getAmount();
sendPacketToViewersAndSelf(collectItemPacket);
objectEntity.remove();
}
});
}
}
}
@ -266,6 +318,14 @@ public class Player extends LivingEntity {
player.playerConnection.sendPacket(playerInfoPacket);
}
@Override
public void setInstance(Instance instance) {
if (!spawned)
throw new IllegalStateException("Player#setInstance is only available during and after PlayerSpawnEvent");
super.setInstance(instance);
}
public void sendBlockBreakAnimation(BlockPosition blockPosition, byte destroyStage) {
BlockBreakAnimationPacket breakAnimationPacket = new BlockBreakAnimationPacket();
breakAnimationPacket.entityId = getEntityId() + 1;

View File

@ -13,7 +13,7 @@ public class TestArrow extends ObjectEntity {
}
@Override
public int getData() {
public int getObjectData() {
return shooter.getEntityId() + 1;
}

View File

@ -0,0 +1,20 @@
package fr.themode.minestom.event;
import fr.themode.minestom.entity.Player;
public class AnimationEvent extends CancellableEvent {
private Player.Hand hand;
public AnimationEvent(Player.Hand hand) {
this.hand = hand;
}
public Player.Hand getHand() {
return hand;
}
public void setHand(Player.Hand hand) {
this.hand = hand;
}
}

View File

@ -1,15 +1,18 @@
package fr.themode.minestom.event;
import fr.themode.minestom.entity.Player;
import fr.themode.minestom.utils.BlockPosition;
public class BlockPlaceEvent extends CancellableEvent {
private short blockId;
private BlockPosition blockPosition;
private Player.Hand hand;
public BlockPlaceEvent(short blockId, BlockPosition blockPosition) {
public BlockPlaceEvent(short blockId, BlockPosition blockPosition, Player.Hand hand) {
this.blockId = blockId;
this.blockPosition = blockPosition;
this.hand = hand;
}
public short getBlockId() {
@ -19,4 +22,8 @@ public class BlockPlaceEvent extends CancellableEvent {
public BlockPosition getBlockPosition() {
return blockPosition;
}
public Player.Hand getHand() {
return hand;
}
}

View File

@ -0,0 +1,17 @@
package fr.themode.minestom.event;
import fr.themode.minestom.instance.Instance;
public class PlayerLoginEvent extends Event {
private Instance spawningInstance;
public Instance getSpawningInstance() {
return spawningInstance;
}
public void setSpawningInstance(Instance instance) {
this.spawningInstance = instance;
}
}

View File

@ -0,0 +1,5 @@
package fr.themode.minestom.event;
public class PlayerSpawnPacket extends Event {
}

View File

@ -9,14 +9,20 @@ import fr.themode.minestom.entity.ObjectEntity;
import fr.themode.minestom.entity.Player;
import fr.themode.minestom.net.packet.server.play.ChunkDataPacket;
import fr.themode.minestom.utils.PacketUtils;
import fr.themode.minestom.utils.SerializerUtils;
import java.io.*;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class Chunk {
private static final int CHUNK_SIZE = 16 * 256 * 16;
private static final int CHUNK_SIZE_X = 16;
private static final int CHUNK_SIZE_Y = 256;
private static final int CHUNK_SIZE_Z = 16;
private static final int CHUNK_SIZE = CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z;
protected Set<ObjectEntity> objectEntities = new CopyOnWriteArraySet<>();
protected Set<EntityCreature> creatures = new CopyOnWriteArraySet<>();
@ -52,8 +58,16 @@ public class Chunk {
setBlock(x, y, z, customBlock.getType(), customBlock.getId());
}
protected void setCustomBlock(byte x, byte y, byte z, short customBlockId) {
CustomBlock customBlock = Main.getBlockManager().getBlock(customBlockId);
if (customBlock == null)
throw new IllegalArgumentException("The custom block " + customBlockId + " does not exist or isn't registered");
setBlock(x, y, z, customBlock.getType(), customBlockId);
}
private void setBlock(byte x, byte y, byte z, short blockType, short customId) {
int index = getIndex(x, y, z);
int index = SerializerUtils.chunkCoordToIndex(x, y, z);
this.blocksId[index] = blockType;
this.customBlocks[index] = customId;
if (isBlockEntity(blockType)) {
@ -64,11 +78,11 @@ public class Chunk {
}
public short getBlockId(byte x, byte y, byte z) {
return this.blocksId[getIndex(x, y, z)];
return this.blocksId[SerializerUtils.chunkCoordToIndex(x, y, z)];
}
public CustomBlock getCustomBlock(byte x, byte y, byte z) {
short id = this.customBlocks[getIndex(x, y, z)];
short id = this.customBlocks[SerializerUtils.chunkCoordToIndex(x, y, z)];
return id != 0 ? Main.getBlockManager().getBlock(id) : null;
}
@ -151,17 +165,55 @@ public class Chunk {
return blockEntities;
}
private int getIndex(byte x, byte y, byte z) {
short index = (short) (x & 0x000F);
index |= (y << 4) & 0x0FF0;
index |= (z << 12) & 0xF000;
return index & 0xffff;
}
public void setFullDataPacket(Buffer fullDataPacket) {
this.fullDataPacket = fullDataPacket;
}
protected byte[] getSerializedData() throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(output);
dos.writeByte(biome.getId());
// TODO customblock id map (StringId -> short id)
// TODO List of (sectionId;blockcount;blocktype;blockarray)
for (byte x = 0; x < CHUNK_SIZE_X; x++) {
for (byte y = -128; y < 127; y++) {
for (byte z = 0; z < CHUNK_SIZE_Z; z++) {
int index = SerializerUtils.chunkCoordToIndex(x, y, z);
boolean isCustomBlock = customBlocks[index] != 0;
short id = isCustomBlock ? customBlocks[index] : blocksId[index];
if (id != 0) {
dos.writeInt(index); // Correspond to chunk coord
dos.writeBoolean(isCustomBlock);
dos.writeShort(id);
}
}
}
}
byte[] result = output.toByteArray();
return result;
}
protected void loadFromFile(File file) throws IOException {
System.out.println("LOAD FROM FILE");
byte[] array = Files.readAllBytes(file.toPath());
DataInputStream stream = new DataInputStream(new ByteArrayInputStream(array));
this.chunkX = stream.readInt();
this.chunkZ = stream.readInt();
System.out.println("chunk: " + chunkX + " : " + chunkZ);
try {
while (true) {
int index = stream.readInt();
boolean isCustomBlock = stream.readBoolean();
short block = stream.readShort();
}
} catch (EOFException e) {
System.out.println("END");
}
}
protected ChunkDataPacket getFreshFullDataPacket() {
ChunkDataPacket fullDataPacket = new ChunkDataPacket();
fullDataPacket.chunk = this;

View File

@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
public class ChunkBatch implements BlockModifier {
@ -43,7 +44,7 @@ public class ChunkBatch implements BlockModifier {
this.dataList.add(data);
}
public void flush() {
public void flush(Consumer<Chunk> callback) {
synchronized (chunk) {
batchesPool.submit(() -> {
for (BlockData data : dataList) {
@ -51,6 +52,8 @@ public class ChunkBatch implements BlockModifier {
}
chunk.refreshDataPacket(); // TODO partial refresh instead of full
instance.sendChunkUpdate(chunk); // TODO partial chunk data
if (callback != null)
callback.accept(chunk);
});
}
}

View File

@ -0,0 +1,85 @@
package fr.themode.minestom.instance;
import fr.themode.minestom.Main;
import fr.themode.minestom.utils.SerializerUtils;
import java.io.*;
import java.nio.file.Files;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
// TODO compression
public class ChunkLoaderIO {
private ExecutorService chunkLoaderPool = Executors.newFixedThreadPool(Main.THREAD_COUNT_CHUNK_IO);
private static File getChunkFile(int chunkX, int chunkZ, File folder) {
return new File(folder, getChunkFileName(chunkX, chunkZ));
}
private static String getChunkFileName(int chunkX, int chunkZ) {
return "chunk." + chunkX + "." + chunkZ + ".data";
}
protected void saveChunk(Chunk chunk, File folder, Runnable callback) {
chunkLoaderPool.submit(() -> {
File chunkFile = getChunkFile(chunk.getChunkX(), chunk.getChunkZ(), folder);
try (FileOutputStream fos = new FileOutputStream(chunkFile)) {
byte[] data = chunk.getSerializedData();
// Zstd.compress(data, 1)
fos.write(data);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
if (callback != null)
callback.run();
});
}
protected void loadChunk(int chunkX, int chunkZ, Instance instance, Consumer<Chunk> callback) {
chunkLoaderPool.submit(() -> {
File chunkFile = getChunkFile(chunkX, chunkZ, instance.getFolder());
if (!chunkFile.exists()) {
instance.createChunk(chunkX, chunkZ, callback); // Chunk file does not exist, create new chunk
return;
}
byte[] array = new byte[0];
try {
array = Files.readAllBytes(getChunkFile(chunkX, chunkZ, instance.getFolder()).toPath());
} catch (IOException e) {
e.printStackTrace();
}
DataInputStream stream = new DataInputStream(new ByteArrayInputStream(array));
Chunk chunk = null;
try {
Biome biome = Biome.fromId(stream.readByte());
chunk = new Chunk(biome, chunkX, chunkZ);
while (true) {
int index = stream.readInt();
boolean isCustomBlock = stream.readBoolean();
short blockId = stream.readShort();
byte[] chunkPos = SerializerUtils.indexToChunkPosition(index);
if (isCustomBlock) {
chunk.setCustomBlock(chunkPos[0], chunkPos[1], chunkPos[2], blockId);
} else {
chunk.setBlock(chunkPos[0], chunkPos[1], chunkPos[2], blockId);
}
}
} catch (EOFException e) {
} catch (IOException e) {
e.printStackTrace();
}
callback.accept(chunk); // Success, null if file isn't properly encoded
});
}
}

View File

@ -7,23 +7,24 @@ import fr.themode.minestom.entity.ObjectEntity;
import fr.themode.minestom.entity.Player;
import fr.themode.minestom.event.BlockBreakEvent;
import fr.themode.minestom.net.PacketWriter;
import fr.themode.minestom.net.packet.server.play.ChunkDataPacket;
import fr.themode.minestom.net.packet.server.play.DestroyEntitiesPacket;
import fr.themode.minestom.net.packet.server.play.ParticlePacket;
import fr.themode.minestom.utils.BlockPosition;
import fr.themode.minestom.utils.GroupedCollections;
import fr.themode.minestom.utils.Position;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public class Instance implements BlockModifier {
private static ChunkLoaderIO chunkLoaderIO = new ChunkLoaderIO();
private UUID uniqueId;
private File folder;
private GroupedCollections<ObjectEntity> objectEntities = new GroupedCollections<>(new CopyOnWriteArrayList<>());
private GroupedCollections<EntityCreature> creatures = new GroupedCollections<>(new CopyOnWriteArrayList());
@ -32,8 +33,9 @@ public class Instance implements BlockModifier {
private ChunkGenerator chunkGenerator;
private Map<Long, Chunk> chunks = new ConcurrentHashMap<>();
public Instance(UUID uniqueId) {
public Instance(UUID uniqueId, File folder) {
this.uniqueId = uniqueId;
this.folder = folder;
}
@Override
@ -41,10 +43,12 @@ public class Instance implements BlockModifier {
Chunk chunk = getChunkAt(x, z);
synchronized (chunk) {
chunk.setBlock((byte) (x % 16), (byte) y, (byte) (z % 16), blockId);
PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> {
chunk.refreshDataPacket();
sendChunkUpdate(chunk);
/*PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> {
chunk.setFullDataPacket(buffer);
sendChunkUpdate(chunk);
});
});*/
}
}
@ -53,10 +57,12 @@ public class Instance implements BlockModifier {
Chunk chunk = getChunkAt(x, z);
synchronized (chunk) {
chunk.setBlock((byte) (x % 16), (byte) y, (byte) (z % 16), blockId);
PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> {
chunk.refreshDataPacket();
sendChunkUpdate(chunk);
/*PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> {
chunk.setFullDataPacket(buffer);
sendChunkUpdate(chunk);
});
});*/
}
}
@ -92,9 +98,28 @@ public class Instance implements BlockModifier {
breakBlock(player, blockPosition, customBlock.getType());
}
public Chunk loadChunk(int chunkX, int chunkZ) {
public void loadChunk(int chunkX, int chunkZ, Consumer<Chunk> callback) {
Chunk chunk = getChunk(chunkX, chunkZ);
return chunk == null ? createChunk(chunkX, chunkZ) : chunk; // TODO load from file
if (chunk != null) {
if (callback != null)
callback.accept(chunk);
} else {
retrieveChunk(chunkX, chunkZ, callback);
}
}
public void loadChunk(int chunkX, int chunkZ) {
loadChunk(chunkX, chunkZ, null);
}
public void loadChunk(Position position, Consumer<Chunk> callback) {
int chunkX = Math.floorDiv((int) position.getX(), 16);
int chunkZ = Math.floorDiv((int) position.getY(), 16);
loadChunk(chunkX, chunkZ, callback);
}
public boolean isChunkLoaded(int chunkX, int chunkZ) {
return getChunk(chunkX, chunkZ) != null;
}
public short getBlockId(int x, int y, int z) {
@ -133,6 +158,23 @@ public class Instance implements BlockModifier {
return getChunkAt(position.getX(), position.getZ());
}
public void saveToFolder(Runnable callback) {
if (folder == null)
throw new UnsupportedOperationException("You cannot save an instance without specified folder.");
Iterator<Chunk> chunks = getChunks().iterator();
while (chunks.hasNext()) {
Chunk chunk = chunks.next();
boolean isLast = !chunks.hasNext();
chunkLoaderIO.saveChunk(chunk, getFolder(), isLast ? callback : null);
}
}
public void saveToFolder() {
saveToFolder(null);
}
public void setChunkGenerator(ChunkGenerator chunkGenerator) {
this.chunkGenerator = chunkGenerator;
}
@ -203,26 +245,50 @@ public class Instance implements BlockModifier {
return uniqueId;
}
public void sendChunkUpdate(Player player, Chunk chunk) {
ChunkDataPacket chunkDataPacket = new ChunkDataPacket();
chunkDataPacket.fullChunk = false;
chunkDataPacket.chunk = chunk;
player.getPlayerConnection().sendPacket(chunkDataPacket); // TODO write packet buffer in another thread (Chunk packets are heavy)
public File getFolder() {
return folder;
}
protected Chunk createChunk(int chunkX, int chunkZ) {
public void setFolder(File folder) {
this.folder = folder;
}
public void sendChunkUpdate(Player player, Chunk chunk) {
Buffer chunkData = chunk.getFullDataPacket();
chunkData.getData().retain(1).markReaderIndex();
player.getPlayerConnection().sendUnencodedPacket(chunkData);
chunkData.getData().resetReaderIndex();
}
protected void retrieveChunk(int chunkX, int chunkZ, Consumer<Chunk> callback) {
if (folder != null) {
// Load from file if possible
chunkLoaderIO.loadChunk(chunkX, chunkZ, this, chunk -> {
cacheChunk(chunk);
if (callback != null)
callback.accept(chunk);
});
} else {
createChunk(chunkX, chunkZ, callback);
}
}
public void createChunk(int chunkX, int chunkZ, Consumer<Chunk> callback) {
Biome biome = chunkGenerator != null ? chunkGenerator.getBiome(chunkX, chunkZ) : Biome.VOID;
Chunk chunk = new Chunk(biome, chunkX, chunkZ);
this.objectEntities.addCollection(chunk.objectEntities);
this.creatures.addCollection(chunk.creatures);
this.players.addCollection(chunk.players);
this.chunks.put(getChunkKey(chunkX, chunkZ), chunk);
cacheChunk(chunk);
if (chunkGenerator != null) {
ChunkBatch chunkBatch = createChunkBatch(chunk);
chunkGenerator.generateChunkData(chunkBatch, chunkX, chunkZ);
chunkBatch.flush();
chunkBatch.flush(callback);
}
return chunk;
}
private void cacheChunk(Chunk chunk) {
this.objectEntities.addCollection(chunk.objectEntities);
this.creatures.addCollection(chunk.creatures);
this.players.addCollection(chunk.players);
this.chunks.put(getChunkKey(chunk.getChunkX(), chunk.getChunkZ()), chunk);
}
protected ChunkBatch createChunkBatch(Chunk chunk) {
@ -230,6 +296,9 @@ public class Instance implements BlockModifier {
}
protected void sendChunkUpdate(Chunk chunk) {
if (getPlayers().isEmpty())
return;
Buffer chunkData = chunk.getFullDataPacket();
chunkData.getData().retain(getPlayers().size()).markReaderIndex();
getPlayers().forEach(player -> {

View File

@ -1,5 +1,6 @@
package fr.themode.minestom.instance;
import java.io.File;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ -9,12 +10,16 @@ public class InstanceManager {
private Set<Instance> instances = Collections.synchronizedSet(new HashSet<>());
public Instance createInstance() {
Instance instance = new Instance(UUID.randomUUID());
public Instance createInstance(File folder) {
Instance instance = new Instance(UUID.randomUUID(), folder);
this.instances.add(instance);
return instance;
}
public Instance createInstance() {
return createInstance(null);
}
public Set<Instance> getInstances() {
return Collections.unmodifiableSet(instances);
}

View File

@ -1,16 +1,20 @@
package fr.themode.minestom.listener;
import fr.themode.minestom.entity.Player;
import fr.themode.minestom.event.AnimationEvent;
import fr.themode.minestom.net.packet.client.play.ClientAnimationPacket;
import fr.themode.minestom.net.packet.server.play.AnimationPacket;
public class AnimationListener {
public static void animationListener(ClientAnimationPacket packet, Player player) {
AnimationPacket animationPacket = new AnimationPacket();
animationPacket.entityId = player.getEntityId();
animationPacket.animation = packet.hand == Player.Hand.MAIN ? AnimationPacket.Animation.SWING_MAIN_ARM : AnimationPacket.Animation.SWING_OFF_HAND;
player.sendPacketToViewers(animationPacket);
AnimationEvent animationEvent = new AnimationEvent(packet.hand);
player.callCancellableEvent(AnimationEvent.class, animationEvent, () -> {
AnimationPacket animationPacket = new AnimationPacket();
animationPacket.entityId = player.getEntityId();
animationPacket.animation = animationEvent.getHand() == Player.Hand.MAIN ? AnimationPacket.Animation.SWING_MAIN_ARM : AnimationPacket.Animation.SWING_OFF_HAND;
player.sendPacketToViewers(animationPacket);
});
}
}

View File

@ -23,7 +23,7 @@ public class BlockPlacementListener {
int offsetZ = blockFace == ClientPlayerDiggingPacket.BlockFace.NORTH ? -1 : blockFace == ClientPlayerDiggingPacket.BlockFace.SOUTH ? 1 : 0;
blockPosition.add(offsetX, offsetY, offsetZ);
BlockPlaceEvent blockPlaceEvent = new BlockPlaceEvent((short) 10, blockPosition);
BlockPlaceEvent blockPlaceEvent = new BlockPlaceEvent((short) 10, blockPosition, packet.hand);
player.callEvent(BlockPlaceEvent.class, blockPlaceEvent);
if (!blockPlaceEvent.isCancelled()) {
instance.setBlock(blockPosition, "custom_block");

View File

@ -1,6 +1,7 @@
package fr.themode.minestom.listener;
import fr.themode.minestom.entity.Entity;
import fr.themode.minestom.entity.EntityCreature;
import fr.themode.minestom.entity.Player;
import fr.themode.minestom.event.AttackEvent;
import fr.themode.minestom.event.InteractEvent;
@ -14,6 +15,9 @@ public class UseEntityListener {
return;
ClientUseEntityPacket.Type type = packet.type;
if (type == ClientUseEntityPacket.Type.ATTACK) {
if (entity instanceof EntityCreature && ((EntityCreature) entity).isDead()) // Can't attack dead entities
return;
AttackEvent attackEvent = new AttackEvent(entity);
player.callEvent(AttackEvent.class, attackEvent);
} else if (type == ClientUseEntityPacket.Type.INTERACT) {

View File

@ -50,7 +50,7 @@ public class PacketProcessor {
ConnectionState connectionState = playerConnection.getConnectionState();
if (!printBlackList.contains(id)) {
System.out.println("RECEIVED ID: 0x" + Integer.toHexString(id) + " State: " + connectionState);
//System.out.println("RECEIVED ID: 0x" + Integer.toHexString(id) + " State: " + connectionState);
}
if (connectionState == ConnectionState.UNKNOWN) {

View File

@ -17,7 +17,7 @@ public class PacketWriter {
public static void writeCallbackPacket(ServerPacket serverPacket, Consumer<Buffer> consumer) {
batchesPool.submit(() -> {
Packet p = PacketUtils.writePacket(serverPacket);
consumer.accept(PacketUtils.encode(p));
consumer.accept(PacketUtils.encode(p)); // TODO accept in another thread?
});
}

View File

@ -2,25 +2,13 @@ package fr.themode.minestom.net.packet.client.login;
import fr.adamaq01.ozao.net.Buffer;
import fr.themode.minestom.Main;
import fr.themode.minestom.bossbar.BarColor;
import fr.themode.minestom.bossbar.BarDivision;
import fr.themode.minestom.bossbar.BossBar;
import fr.themode.minestom.entity.GameMode;
import fr.themode.minestom.entity.ItemEntity;
import fr.themode.minestom.entity.Player;
import fr.themode.minestom.entity.demo.ChickenCreature;
import fr.themode.minestom.entity.demo.TestArrow;
import fr.themode.minestom.instance.Instance;
import fr.themode.minestom.instance.demo.ChunkGeneratorDemo;
import fr.themode.minestom.inventory.PlayerInventory;
import fr.themode.minestom.item.ItemStack;
import fr.themode.minestom.item.Material;
import fr.themode.minestom.net.ConnectionManager;
import fr.themode.minestom.net.ConnectionState;
import fr.themode.minestom.net.packet.client.ClientPreplayPacket;
import fr.themode.minestom.net.packet.server.login.JoinGamePacket;
import fr.themode.minestom.net.packet.server.login.LoginSuccessPacket;
import fr.themode.minestom.net.packet.server.play.DeclareCommandsPacket;
import fr.themode.minestom.net.packet.server.play.PlayerInfoPacket;
import fr.themode.minestom.net.packet.server.play.PlayerPositionAndLookPacket;
import fr.themode.minestom.net.packet.server.play.SpawnPositionPacket;
@ -34,21 +22,24 @@ import java.util.UUID;
public class LoginStartPacket implements ClientPreplayPacket {
// Test
private static Instance instance;
/*private static Instance instance;
static {
ChunkGeneratorDemo chunkGeneratorDemo = new ChunkGeneratorDemo();
instance = Main.getInstanceManager().createInstance();
instance = Main.getInstanceManager().createInstance(new File("C:\\Users\\themo\\OneDrive\\Bureau\\Minestom data"));
//instance = Main.getInstanceManager().createInstance();
instance.setChunkGenerator(chunkGeneratorDemo);
int loopStart = -2;
int loopEnd = 2;
long time = System.currentTimeMillis();
for (int x = loopStart; x < loopEnd; x++)
for (int z = loopStart; z < loopEnd; z++) {
instance.loadChunk(x, z);
instance.loadChunk(x, z, chunk -> {
System.out.println("JE SUIS LE CALLBACK CHUNK");
});
}
System.out.println("Time to load all chunks: " + (System.currentTimeMillis() - time) + " ms");
}
}*/
public String username;
@ -73,9 +64,9 @@ public class LoginStartPacket implements ClientPreplayPacket {
GameMode gameMode = GameMode.SURVIVAL;
Dimension dimension = Dimension.OVERWORLD;
LevelType levelType = LevelType.DEFAULT;
float x = 5;
float y = 65;
float z = 5;
float x = 0;
float y = 0;
float z = 0;
player.refreshDimension(dimension);
player.refreshGameMode(gameMode);
@ -121,10 +112,14 @@ public class LoginStartPacket implements ClientPreplayPacket {
// Next is optional TODO put all that somewhere else (LoginEvent)
// TODO LoginEvent in another thread (here we are in netty thread)
System.out.println("ADD WAITING PLAYER");
Main.getEntityManager().addWaitingPlayer(player);
player.setInstance(instance);
for (int cx = 0; cx < 4; cx++)
// TODO REMOVE EVERYTHING DOWN THERE
//player.setInstance(instance);
/*for (int cx = 0; cx < 4; cx++)
for (int cz = 0; cz < 4; cz++) {
ChickenCreature chickenCreature = new ChickenCreature();
chickenCreature.refreshPosition(0 + (float) cx * 1, 65, 0 + (float) cz * 1);
@ -140,7 +135,7 @@ public class LoginStartPacket implements ClientPreplayPacket {
/*Inventory inv = new Inventory(InventoryType.WINDOW_3X3, "Salut je suis le titre");
inv.setItemStack(0, new ItemStack(1, (byte) 1));
player.openInventory(inv);
inv.setItemStack(1, new ItemStack(1, (byte) 2));*/
inv.setItemStack(1, new ItemStack(1, (byte) 2));
BossBar bossBar = new BossBar("Bossbar Title", BarColor.BLUE, BarDivision.SEGMENT_12);
bossBar.setProgress(0.75f);
@ -178,7 +173,7 @@ public class LoginStartPacket implements ClientPreplayPacket {
declareCommandsPacket.rootIndex = 0;
connection.sendPacket(declareCommandsPacket);
connection.sendPacket(declareCommandsPacket);*/
}
@Override

View File

@ -1,13 +1,14 @@
package fr.themode.minestom.net.packet.client.play;
import fr.adamaq01.ozao.net.Buffer;
import fr.themode.minestom.entity.Player;
import fr.themode.minestom.net.packet.client.ClientPlayPacket;
import fr.themode.minestom.utils.BlockPosition;
import fr.themode.minestom.utils.Utils;
public class ClientPlayerBlockPlacementPacket extends ClientPlayPacket {
public Hand hand;
public Player.Hand hand;
public BlockPosition blockPosition;
public ClientPlayerDiggingPacket.BlockFace blockFace;
public float cursorPositionX, cursorPositionY, cursorPositionZ;
@ -15,7 +16,7 @@ public class ClientPlayerBlockPlacementPacket extends ClientPlayPacket {
@Override
public void read(Buffer buffer) {
this.hand = Hand.values()[Utils.readVarInt(buffer)];
this.hand = Player.Hand.values()[Utils.readVarInt(buffer)];
this.blockPosition = Utils.readPosition(buffer);
this.blockFace = ClientPlayerDiggingPacket.BlockFace.values()[Utils.readVarInt(buffer)];
this.cursorPositionX = buffer.getFloat();
@ -24,9 +25,4 @@ public class ClientPlayerBlockPlacementPacket extends ClientPlayPacket {
this.insideBlock = buffer.getBoolean();
}
public enum Hand {
MAIN_HAND,
OFF_HAND;
}
}

View File

@ -4,6 +4,7 @@ import fr.adamaq01.ozao.net.Buffer;
import fr.themode.minestom.instance.Chunk;
import fr.themode.minestom.net.packet.server.ServerPacket;
import fr.themode.minestom.utils.BlockPosition;
import fr.themode.minestom.utils.SerializerUtils;
import fr.themode.minestom.utils.Utils;
import net.querz.nbt.CompoundTag;
import net.querz.nbt.DoubleTag;
@ -81,7 +82,7 @@ public class ChunkDataPacket implements ServerPacket {
Utils.writeVarInt(buffer, blockEntities.size());
for (Integer index : blockEntities) {
BlockPosition blockPosition = indexToBlockPosition(index);
BlockPosition blockPosition = SerializerUtils.indexToBlockPosition(index, chunk.getChunkX(), chunk.getChunkZ());
CompoundTag blockEntity = new CompoundTag();
blockEntity.put("x", new DoubleTag(blockPosition.getX()));
blockEntity.put("y", new DoubleTag(blockPosition.getY()));
@ -110,13 +111,6 @@ public class ChunkDataPacket implements ServerPacket {
return blocks;
}
private BlockPosition indexToBlockPosition(int index) {
byte z = (byte) (index >> 12 & 0xF);
byte y = (byte) (index >> 4 & 0xFF);
byte x = (byte) (index >> 0 & 0xF);
return new BlockPosition(x + 16 * chunk.getChunkX(), y, z + 16 * chunk.getChunkZ());
}
@Override
public int getId() {
return 0x21;

View File

@ -6,6 +6,7 @@ import fr.adamaq01.ozao.net.server.Connection;
import fr.themode.minestom.net.ConnectionState;
import fr.themode.minestom.net.packet.server.ServerPacket;
import fr.themode.minestom.utils.PacketUtils;
import io.netty.channel.Channel;
import io.netty.channel.socket.SocketChannel;
import java.lang.reflect.Field;
@ -40,12 +41,15 @@ public class PlayerConnection {
}
public void sendUnencodedPacket(Buffer packet) {
try {
SocketChannel channel = ((SocketChannel) field.get(connection));
channel.writeAndFlush(packet.getData());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
getChannel().writeAndFlush(packet.getData());
}
public void writeUnencodedPacket(Buffer packet) {
getChannel().write(packet.getData());
}
public void flush() {
getChannel().flush();
}
public void sendPacket(ServerPacket serverPacket) {
@ -63,4 +67,13 @@ public class PlayerConnection {
public ConnectionState getConnectionState() {
return connectionState;
}
private Channel getChannel() {
try {
return ((SocketChannel) field.get(connection));
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
}

View File

@ -0,0 +1,20 @@
package fr.themode.minestom.utils;
public class ArraysUtils {
public static byte[] concenateByteArrays(byte[]... arrays) {
int totalLength = 0;
for (byte[] array : arrays) {
totalLength += array.length;
}
byte[] result = new byte[totalLength];
int startingPos = 0;
for (byte[] array : arrays) {
System.arraycopy(array, 0, result, startingPos, array.length);
}
return result;
}
}

View File

@ -0,0 +1,42 @@
package fr.themode.minestom.utils;
public class SerializerUtils {
public static byte[] intToBytes(int value) {
byte[] result = new byte[4];
result[0] = (byte) (value >> 24);
result[1] = (byte) (value >> 16);
result[2] = (byte) (value >> 8);
result[3] = (byte) (value >> 0);
return result;
}
public static int bytesToInt(byte[] value) {
return ((value[0] & 0xFF) << 24) |
((value[1] & 0xFF) << 16) |
((value[2] & 0xFF) << 8) |
((value[3] & 0xFF) << 0);
}
public static int chunkCoordToIndex(byte x, byte y, byte z) {
short index = (short) (x & 0x000F);
index |= (y << 4) & 0x0FF0;
index |= (z << 12) & 0xF000;
return index & 0xffff;
}
public static byte[] indexToChunkPosition(int index) {
byte z = (byte) (index >> 12 & 0xF);
byte y = (byte) (index >> 4 & 0xFF);
byte x = (byte) (index >> 0 & 0xF);
return new byte[]{x, y, z};
}
public static BlockPosition indexToBlockPosition(int index, int chunkX, int chunkZ) {
byte z = (byte) (index >> 12 & 0xF);
byte y = (byte) (index >> 4 & 0xFF);
byte x = (byte) (index >> 0 & 0xF);
return new BlockPosition(x + 16 * chunkX, y, z + 16 * chunkZ);
}
}