diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index 4b345dd4..afdc285c 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -1146,6 +1146,8 @@ Add asset manager support for queueing models asynchronously w/ promised path Add promised path support to queued textures Code formatting on queued asset classes Refactoring server side of terrain management +Add server manager for block chunk data & management +Add endpoint to request strided block data # TODO diff --git a/src/main/java/electrosphere/client/block/BlockChunkData.java b/src/main/java/electrosphere/client/block/BlockChunkData.java index e4b07904..563ca43b 100644 --- a/src/main/java/electrosphere/client/block/BlockChunkData.java +++ b/src/main/java/electrosphere/client/block/BlockChunkData.java @@ -12,6 +12,16 @@ public class BlockChunkData { */ public static final int CHUNK_DATA_WIDTH = 64; + /** + * Total width of the data arrays + */ + public static final int TOTAL_DATA_WIDTH = CHUNK_DATA_WIDTH * CHUNK_DATA_WIDTH * CHUNK_DATA_WIDTH; + + /** + * Size of a buffer that stores this chunk's data + */ + public static final int BUFFER_SIZE = TOTAL_DATA_WIDTH * 2 * 2; + /** * The number of blocks to place within each unit of distance */ @@ -27,6 +37,11 @@ public class BlockChunkData { */ public static final short NOT_HOMOGENOUS = -1; + /** + * The LOD value for a full res chunk data + */ + public static final int LOD_FULL_RES = 0; + /** * The type of block at a given position @@ -44,6 +59,26 @@ public class BlockChunkData { */ short homogenousValue = NOT_HOMOGENOUS; + /** + * The level of detail of the block data + */ + int lod; + + /** + * The x coordinate of the world position of the chunk + */ + int worldX; + + /** + * The y coordinate of the world position of the chunk + */ + int worldY; + + /** + * The z coordinate of the world position of the chunk + */ + int worldZ; + /** * Constructor */ @@ -174,4 +209,84 @@ public class BlockChunkData { return this.homogenousValue != NOT_HOMOGENOUS; } + /** + * Gets the level of detail of the block data + * @return The level of detail of the block data + */ + public int getLod() { + return lod; + } + + /** + * Sets + * @return + */ + public void setLod(int lod) { + this.lod = lod; + } + + /** + * Gets the x coordinate of the world position of the chunk + * @return The x coordinate of the world position of the chunk + */ + public int getWorldX() { + return worldX; + } + + /** + * Sets the x coordinate of the world position of the chunk + * @return The x coordinate of the world position of the chunk + */ + public void setWorldX(int worldX) { + this.worldX = worldX; + } + + /** + * Gets the y coordinate of the world position of the chunk + * @return The y coordinate of the world position of the chunk + */ + public int getWorldY() { + return worldY; + } + + /** + * Sets the y coordinate of the world position of the chunk + * @return The y coordinate of the world position of the chunk + */ + public void setWorldY(int worldY) { + this.worldY = worldY; + } + + /** + * Gets the z coordinate of the world position of the chunk + * @return The z coordinate of the world position of the chunk + */ + public int getWorldZ() { + return worldZ; + } + + /** + * Sets the z coordinate of the world position of the chunk + * @return The z coordinate of the world position of the chunk + */ + public void setWorldZ(int worldZ) { + this.worldZ = worldZ; + } + + /** + * Gets the homogenous value for this chunk + * @return The homogenous value + */ + public short getHomogenousValue(){ + return this.homogenousValue; + } + + /** + * Sets the homogenous value + * @param homogenousValue The homogenous value + */ + public void setHomogenousValue(short homogenousValue){ + this.homogenousValue = homogenousValue; + } + } diff --git a/src/main/java/electrosphere/game/server/world/ServerWorldData.java b/src/main/java/electrosphere/game/server/world/ServerWorldData.java index 187568c6..d32d4831 100644 --- a/src/main/java/electrosphere/game/server/world/ServerWorldData.java +++ b/src/main/java/electrosphere/game/server/world/ServerWorldData.java @@ -1,5 +1,6 @@ package electrosphere.game.server.world; +import electrosphere.server.block.manager.ServerBlockManager; import electrosphere.server.fluid.generation.DefaultFluidGenerator; import electrosphere.server.fluid.manager.ServerFluidManager; import electrosphere.server.terrain.generation.DefaultChunkGenerator; @@ -58,6 +59,11 @@ public class ServerWorldData { //fluid data private ServerFluidManager serverFluidManager; + + /** + * The block manager + */ + private ServerBlockManager serverBlockManager; /** @@ -119,19 +125,22 @@ public class ServerWorldData { ServerWorldData serverWorldData = null; ServerTerrainManager serverTerrainManager = null; ServerFluidManager serverFluidManager = null; + ServerBlockManager serverBlockManager = null; if(isScene){ serverWorldData = FileUtils.loadObjectFromSavePath(sceneOrSaveName, "world.json", ServerWorldData.class); serverTerrainManager = new ServerTerrainManager(serverWorldData, 0, new DefaultChunkGenerator()); serverTerrainManager.load(sceneOrSaveName); serverFluidManager = new ServerFluidManager(serverWorldData, serverTerrainManager, 0, new DefaultFluidGenerator()); + serverBlockManager = new ServerBlockManager(serverWorldData); } else { //TODO: Allow loading procedurally generated terrain from disk (the chunk generator is always default currently) serverWorldData = FileUtils.loadObjectFromSavePath(sceneOrSaveName, "world.json", ServerWorldData.class); serverTerrainManager = new ServerTerrainManager(serverWorldData, 0, new DefaultChunkGenerator()); serverTerrainManager.load(sceneOrSaveName); serverFluidManager = new ServerFluidManager(serverWorldData, serverTerrainManager, 0, new DefaultFluidGenerator()); + serverBlockManager = new ServerBlockManager(serverWorldData); } - serverWorldData.setManagers(serverTerrainManager, serverFluidManager); + serverWorldData.setManagers(serverTerrainManager, serverFluidManager, serverBlockManager); return serverWorldData; } @@ -146,6 +155,7 @@ public class ServerWorldData { ServerWorldData serverWorldData = null; ServerTerrainManager serverTerrainManager = null; ServerFluidManager serverFluidManager = null; + ServerBlockManager serverBlockManager = null; //TODO: Allow loading procedurally generated terrain from disk (the chunk generator is always default currently) serverWorldData = ServerWorldData.createFixedWorldData(new Vector3d(0),new Vector3d(TestGenerationChunkGenerator.GENERATOR_REALM_SIZE * ServerTerrainChunk.CHUNK_DIMENSION)); serverWorldData.worldSizeDiscrete = TestGenerationChunkGenerator.GENERATOR_REALM_SIZE; @@ -158,7 +168,8 @@ public class ServerWorldData { serverTerrainManager.genTestData(chunkGen); } serverFluidManager = new ServerFluidManager(serverWorldData, serverTerrainManager, 0, new DefaultFluidGenerator()); - serverWorldData.setManagers(serverTerrainManager, serverFluidManager); + serverBlockManager = new ServerBlockManager(serverWorldData); + serverWorldData.setManagers(serverTerrainManager, serverFluidManager, serverBlockManager); return serverWorldData; } @@ -276,14 +287,24 @@ public class ServerWorldData { return this.serverFluidManager; } + /** + * Gets the block manager for this world + * @return The block manager if it exists, null otherwise + */ + public ServerBlockManager getServerBlockManager(){ + return this.serverBlockManager; + } + /** * Sets the chunk managers * @param serverTerrainManager The terrain manager * @param serverFluidManager The fluid manager + * @param serverBlockManager The server block manager */ - public void setManagers(ServerTerrainManager serverTerrainManager, ServerFluidManager serverFluidManager){ + public void setManagers(ServerTerrainManager serverTerrainManager, ServerFluidManager serverFluidManager, ServerBlockManager serverBlockManager){ this.serverTerrainManager = serverTerrainManager; this.serverFluidManager = serverFluidManager; + this.serverBlockManager = serverBlockManager; this.serverTerrainManager.setParent(this); this.serverFluidManager.setParent(this); if(this.serverTerrainManager == null || this.serverFluidManager == null){ diff --git a/src/main/java/electrosphere/net/parser/net/message/NetworkMessage.java b/src/main/java/electrosphere/net/parser/net/message/NetworkMessage.java index 85380e8a..fe97a9ad 100644 --- a/src/main/java/electrosphere/net/parser/net/message/NetworkMessage.java +++ b/src/main/java/electrosphere/net/parser/net/message/NetworkMessage.java @@ -204,6 +204,16 @@ public abstract class NetworkMessage { rVal = TerrainMessage.parseSendReducedChunkDataMessage(byteBuffer); } break; + case TypeBytes.TERRAIN_MESSAGE_TYPE_REQUESTREDUCEDBLOCKDATA: + if(TerrainMessage.canParseMessage(byteBuffer,secondByte)){ + rVal = TerrainMessage.parseRequestReducedBlockDataMessage(byteBuffer); + } + break; + case TypeBytes.TERRAIN_MESSAGE_TYPE_SENDREDUCEDBLOCKDATA: + if(TerrainMessage.canParseMessage(byteBuffer,secondByte)){ + rVal = TerrainMessage.parseSendReducedBlockDataMessage(byteBuffer); + } + break; case TypeBytes.TERRAIN_MESSAGE_TYPE_REQUESTFLUIDDATA: if(TerrainMessage.canParseMessage(byteBuffer,secondByte)){ rVal = TerrainMessage.parseRequestFluidDataMessage(byteBuffer); diff --git a/src/main/java/electrosphere/net/parser/net/message/TerrainMessage.java b/src/main/java/electrosphere/net/parser/net/message/TerrainMessage.java index 2c688cfd..d7837fb0 100644 --- a/src/main/java/electrosphere/net/parser/net/message/TerrainMessage.java +++ b/src/main/java/electrosphere/net/parser/net/message/TerrainMessage.java @@ -21,6 +21,8 @@ public class TerrainMessage extends NetworkMessage { SENDCHUNKDATA, REQUESTREDUCEDCHUNKDATA, SENDREDUCEDCHUNKDATA, + REQUESTREDUCEDBLOCKDATA, + SENDREDUCEDBLOCKDATA, REQUESTFLUIDDATA, SENDFLUIDDATA, UPDATEFLUIDDATA, @@ -442,6 +444,14 @@ public class TerrainMessage extends NetworkMessage { } case TypeBytes.TERRAIN_MESSAGE_TYPE_SENDREDUCEDCHUNKDATA: return TerrainMessage.canParseSendReducedChunkDataMessage(byteBuffer); + case TypeBytes.TERRAIN_MESSAGE_TYPE_REQUESTREDUCEDBLOCKDATA: + if(byteBuffer.getRemaining() >= TypeBytes.TERRAIN_MESSAGE_TYPE_REQUESTREDUCEDBLOCKDATA_SIZE){ + return true; + } else { + return false; + } + case TypeBytes.TERRAIN_MESSAGE_TYPE_SENDREDUCEDBLOCKDATA: + return TerrainMessage.canParseSendReducedBlockDataMessage(byteBuffer); case TypeBytes.TERRAIN_MESSAGE_TYPE_REQUESTFLUIDDATA: if(byteBuffer.getRemaining() >= TypeBytes.TERRAIN_MESSAGE_TYPE_REQUESTFLUIDDATA_SIZE){ return true; @@ -802,6 +812,99 @@ public class TerrainMessage extends NetworkMessage { return rVal; } + /** + * Parses a message of type RequestReducedBlockData + */ + public static TerrainMessage parseRequestReducedBlockDataMessage(CircularByteBuffer byteBuffer){ + TerrainMessage rVal = new TerrainMessage(TerrainMessageType.REQUESTREDUCEDBLOCKDATA); + stripPacketHeader(byteBuffer); + rVal.setworldX(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + rVal.setworldY(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + rVal.setworldZ(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + rVal.setchunkResolution(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + return rVal; + } + + /** + * Constructs a message of type RequestReducedBlockData + */ + public static TerrainMessage constructRequestReducedBlockDataMessage(int worldX,int worldY,int worldZ,int chunkResolution){ + TerrainMessage rVal = new TerrainMessage(TerrainMessageType.REQUESTREDUCEDBLOCKDATA); + rVal.setworldX(worldX); + rVal.setworldY(worldY); + rVal.setworldZ(worldZ); + rVal.setchunkResolution(chunkResolution); + rVal.serialize(); + return rVal; + } + + /** + * Checks if a message of type SendReducedBlockData can be parsed from the byte stream + */ + public static boolean canParseSendReducedBlockDataMessage(CircularByteBuffer byteBuffer){ + int currentStreamLength = byteBuffer.getRemaining(); + List temporaryByteQueue = new LinkedList(); + if(currentStreamLength < 6){ + return false; + } + if(currentStreamLength < 10){ + return false; + } + if(currentStreamLength < 14){ + return false; + } + if(currentStreamLength < 18){ + return false; + } + if(currentStreamLength < 22){ + return false; + } + int chunkDataSize = 0; + if(currentStreamLength < 26){ + return false; + } else { + temporaryByteQueue.add(byteBuffer.peek(22 + 0)); + temporaryByteQueue.add(byteBuffer.peek(22 + 1)); + temporaryByteQueue.add(byteBuffer.peek(22 + 2)); + temporaryByteQueue.add(byteBuffer.peek(22 + 3)); + chunkDataSize = ByteStreamUtils.popIntFromByteQueue(temporaryByteQueue); + } + if(currentStreamLength < 26 + chunkDataSize){ + return false; + } + return true; + } + + /** + * Parses a message of type SendReducedBlockData + */ + public static TerrainMessage parseSendReducedBlockDataMessage(CircularByteBuffer byteBuffer){ + TerrainMessage rVal = new TerrainMessage(TerrainMessageType.SENDREDUCEDBLOCKDATA); + stripPacketHeader(byteBuffer); + rVal.setworldX(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + rVal.setworldY(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + rVal.setworldZ(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + rVal.setchunkResolution(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + rVal.sethomogenousValue(ByteStreamUtils.popIntFromByteQueue(byteBuffer)); + rVal.setchunkData(ByteStreamUtils.popByteArrayFromByteQueue(byteBuffer)); + return rVal; + } + + /** + * Constructs a message of type SendReducedBlockData + */ + public static TerrainMessage constructSendReducedBlockDataMessage(int worldX,int worldY,int worldZ,int chunkResolution,int homogenousValue,byte[] chunkData){ + TerrainMessage rVal = new TerrainMessage(TerrainMessageType.SENDREDUCEDBLOCKDATA); + rVal.setworldX(worldX); + rVal.setworldY(worldY); + rVal.setworldZ(worldZ); + rVal.setchunkResolution(chunkResolution); + rVal.sethomogenousValue(homogenousValue); + rVal.setchunkData(chunkData); + rVal.serialize(); + return rVal; + } + /** * Parses a message of type RequestFluidData */ @@ -1211,6 +1314,63 @@ public class TerrainMessage extends NetworkMessage { rawBytes[26+i] = chunkData[i]; } break; + case REQUESTREDUCEDBLOCKDATA: + rawBytes = new byte[2+4+4+4+4]; + //message header + rawBytes[0] = TypeBytes.MESSAGE_TYPE_TERRAIN; + //entity messaage header + rawBytes[1] = TypeBytes.TERRAIN_MESSAGE_TYPE_REQUESTREDUCEDBLOCKDATA; + intValues = ByteStreamUtils.serializeIntToBytes(worldX); + for(int i = 0; i < 4; i++){ + rawBytes[2+i] = intValues[i]; + } + intValues = ByteStreamUtils.serializeIntToBytes(worldY); + for(int i = 0; i < 4; i++){ + rawBytes[6+i] = intValues[i]; + } + intValues = ByteStreamUtils.serializeIntToBytes(worldZ); + for(int i = 0; i < 4; i++){ + rawBytes[10+i] = intValues[i]; + } + intValues = ByteStreamUtils.serializeIntToBytes(chunkResolution); + for(int i = 0; i < 4; i++){ + rawBytes[14+i] = intValues[i]; + } + break; + case SENDREDUCEDBLOCKDATA: + rawBytes = new byte[2+4+4+4+4+4+4+chunkData.length]; + //message header + rawBytes[0] = TypeBytes.MESSAGE_TYPE_TERRAIN; + //entity messaage header + rawBytes[1] = TypeBytes.TERRAIN_MESSAGE_TYPE_SENDREDUCEDBLOCKDATA; + intValues = ByteStreamUtils.serializeIntToBytes(worldX); + for(int i = 0; i < 4; i++){ + rawBytes[2+i] = intValues[i]; + } + intValues = ByteStreamUtils.serializeIntToBytes(worldY); + for(int i = 0; i < 4; i++){ + rawBytes[6+i] = intValues[i]; + } + intValues = ByteStreamUtils.serializeIntToBytes(worldZ); + for(int i = 0; i < 4; i++){ + rawBytes[10+i] = intValues[i]; + } + intValues = ByteStreamUtils.serializeIntToBytes(chunkResolution); + for(int i = 0; i < 4; i++){ + rawBytes[14+i] = intValues[i]; + } + intValues = ByteStreamUtils.serializeIntToBytes(homogenousValue); + for(int i = 0; i < 4; i++){ + rawBytes[18+i] = intValues[i]; + } + intValues = ByteStreamUtils.serializeIntToBytes(chunkData.length); + for(int i = 0; i < 4; i++){ + rawBytes[22+i] = intValues[i]; + } + for(int i = 0; i < chunkData.length; i++){ + rawBytes[26+i] = chunkData[i]; + } + break; case REQUESTFLUIDDATA: rawBytes = new byte[2+4+4+4]; //message header diff --git a/src/main/java/electrosphere/net/parser/net/message/TypeBytes.java b/src/main/java/electrosphere/net/parser/net/message/TypeBytes.java index 58831df0..f5228258 100644 --- a/src/main/java/electrosphere/net/parser/net/message/TypeBytes.java +++ b/src/main/java/electrosphere/net/parser/net/message/TypeBytes.java @@ -76,9 +76,11 @@ public class TypeBytes { public static final byte TERRAIN_MESSAGE_TYPE_SENDCHUNKDATA = 7; public static final byte TERRAIN_MESSAGE_TYPE_REQUESTREDUCEDCHUNKDATA = 8; public static final byte TERRAIN_MESSAGE_TYPE_SENDREDUCEDCHUNKDATA = 9; - public static final byte TERRAIN_MESSAGE_TYPE_REQUESTFLUIDDATA = 10; - public static final byte TERRAIN_MESSAGE_TYPE_SENDFLUIDDATA = 11; - public static final byte TERRAIN_MESSAGE_TYPE_UPDATEFLUIDDATA = 12; + public static final byte TERRAIN_MESSAGE_TYPE_REQUESTREDUCEDBLOCKDATA = 10; + public static final byte TERRAIN_MESSAGE_TYPE_SENDREDUCEDBLOCKDATA = 11; + public static final byte TERRAIN_MESSAGE_TYPE_REQUESTFLUIDDATA = 12; + public static final byte TERRAIN_MESSAGE_TYPE_SENDFLUIDDATA = 13; + public static final byte TERRAIN_MESSAGE_TYPE_UPDATEFLUIDDATA = 14; /* Terrain packet sizes */ @@ -90,6 +92,7 @@ public class TypeBytes { public static final byte TERRAIN_MESSAGE_TYPE_SPAWNPOSITION_SIZE = 26; public static final byte TERRAIN_MESSAGE_TYPE_REQUESTCHUNKDATA_SIZE = 14; public static final byte TERRAIN_MESSAGE_TYPE_REQUESTREDUCEDCHUNKDATA_SIZE = 18; + public static final byte TERRAIN_MESSAGE_TYPE_REQUESTREDUCEDBLOCKDATA_SIZE = 18; public static final byte TERRAIN_MESSAGE_TYPE_REQUESTFLUIDDATA_SIZE = 14; /* diff --git a/src/main/java/electrosphere/net/server/protocol/TerrainProtocol.java b/src/main/java/electrosphere/net/server/protocol/TerrainProtocol.java index 51b8531d..4dbba524 100644 --- a/src/main/java/electrosphere/net/server/protocol/TerrainProtocol.java +++ b/src/main/java/electrosphere/net/server/protocol/TerrainProtocol.java @@ -3,10 +3,12 @@ package electrosphere.net.server.protocol; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import java.nio.ShortBuffer; import java.util.function.Consumer; import org.joml.Vector3d; +import electrosphere.client.block.BlockChunkData; import electrosphere.client.terrain.cache.ChunkData; import electrosphere.engine.Globals; import electrosphere.logger.LoggerInterface; @@ -39,6 +41,12 @@ public class TerrainProtocol implements ServerProtocolTemplate { ); return null; } + case REQUESTREDUCEDBLOCKDATA: { + TerrainProtocol.sendBlocksAsyncStrided(connectionHandler, + message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution() + ); + return null; + } default: { } break; } @@ -80,6 +88,8 @@ public class TerrainProtocol implements ServerProtocolTemplate { case SENDFLUIDDATA: case REQUESTREDUCEDCHUNKDATA: case SENDREDUCEDCHUNKDATA: + case REQUESTREDUCEDBLOCKDATA: + case SENDREDUCEDBLOCKDATA: //silently ignore break; } @@ -262,6 +272,57 @@ public class TerrainProtocol implements ServerProtocolTemplate { Globals.profiler.endCpuSample(); } + /** + * Sends a subchunk to the client + * @param connectionHandler The connection handler + * @param worldX the world x + * @param worldY the world y + * @param worldZ the world z + * @param stride The stride of the data + */ + static void sendBlocksAsyncStrided(ServerConnectionHandler connectionHandler, int worldX, int worldY, int worldZ, int stride){ + Globals.profiler.beginAggregateCpuSample("TerrainProtocol(server).sendWorldSubChunk"); + + // System.out.println("Received request for chunk " + message.getworldX() + " " + message.getworldY()); + Realm realm = Globals.playerManager.getPlayerRealm(connectionHandler.getPlayer()); + if(realm.getServerWorldData().getServerBlockManager() == null){ + return; + } + + Consumer onLoad = (BlockChunkData chunk) -> { + //The length along each access of the chunk data. Typically, should be at least 17. + //Because CHUNK_SIZE is 16, 17 adds the necessary extra value. Each chunk needs the value of the immediately following position to generate + //chunk data that connects seamlessly to the next chunk. + byte[] toSend = null; + + if(chunk.getHomogenousValue() == ChunkData.NOT_HOMOGENOUS){ + ByteBuffer buffer = ByteBuffer.allocate(BlockChunkData.BUFFER_SIZE); + ShortBuffer shortView = buffer.asShortBuffer(); + + short[] type = chunk.getType(); + short[] metadata = chunk.getMetadata(); + + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + shortView.put(type[i]); + } + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + shortView.put(metadata[i]); + } + toSend = buffer.array(); + } else { + toSend = new byte[]{ 0 }; + } + + LoggerInterface.loggerNetworking.DEBUG("(Server) Send block data at " + worldX + " " + worldY + " " + worldZ); + connectionHandler.addMessagetoOutgoingQueue(TerrainMessage.constructSendReducedBlockDataMessage(worldX, worldY, worldZ, stride, chunk.getHomogenousValue(), toSend)); + }; + + //request chunk + realm.getServerWorldData().getServerBlockManager().getChunkAsync(worldX, worldY, worldZ, stride, onLoad); + + Globals.profiler.endCpuSample(); + } + /** * Sends a fluid sub chunk to the client diff --git a/src/main/java/electrosphere/server/block/diskmap/ServerBlockChunkDiskMap.java b/src/main/java/electrosphere/server/block/diskmap/ServerBlockChunkDiskMap.java new file mode 100644 index 00000000..8076bc1e --- /dev/null +++ b/src/main/java/electrosphere/server/block/diskmap/ServerBlockChunkDiskMap.java @@ -0,0 +1,205 @@ +package electrosphere.server.block.diskmap; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterOutputStream; + +import electrosphere.client.block.BlockChunkData; +import electrosphere.engine.Globals; +import electrosphere.logger.LoggerInterface; +import electrosphere.util.FileUtils; +import electrosphere.util.annotation.Exclude; +import io.github.studiorailgun.HashUtils; + +/** + * An interface for accessing the disk map of chunk information + */ +public class ServerBlockChunkDiskMap { + + /** + * The map of world position+chunk type to the file that actually houses that information + */ + Map worldPosFileMap; + + /** + * Locks the chunk disk map for thread safety + */ + @Exclude + ReentrantLock lock = new ReentrantLock(); + + /** + * Constructor + */ + private ServerBlockChunkDiskMap(){ + worldPosFileMap = new HashMap(); + } + + /** + * Gets a key for a given chunk file based on a world coordinate + * @param worldX The x component + * @param worldY The y component + * @param worldZ The z component + * @return The key + */ + private static long getBlockChunkKey(int worldX, int worldY, int worldZ){ + return HashUtils.cantorHash(worldX, worldY, worldZ); + } + + /** + * Initializes a diskmap based on a given save name + * @param saveName The save name + */ + public static ServerBlockChunkDiskMap init(String saveName){ + ServerBlockChunkDiskMap rVal = null; + LoggerInterface.loggerEngine.DEBUG("INIT CHUNK MAP " + saveName); + if(FileUtils.getSaveFile(saveName, "chunk.map").exists()){ + rVal = FileUtils.loadObjectFromSavePath(saveName, "chunk.map", ServerBlockChunkDiskMap.class); + LoggerInterface.loggerEngine.DEBUG("POS FILE MAP: " + rVal.worldPosFileMap.keySet()); + } else { + rVal = new ServerBlockChunkDiskMap(); + } + return rVal; + } + + /** + * Initializes a diskmap based on a given save name + * @param saveName The save name + */ + public static ServerBlockChunkDiskMap init(){ + return new ServerBlockChunkDiskMap(); + } + + /** + * Saves the disk map to disk + */ + public void save(){ + FileUtils.serializeObjectToSavePath(Globals.currentSave.getName(), "blockchunk.map", worldPosFileMap); + } + + /** + * Checks if the map contains a given chunk position + * @param worldX The x component + * @param worldY The y component + * @param worldZ The z component + * @return True if the map contains the chunk, false otherwise + */ + public boolean containsBlocksAtPosition(int worldX, int worldY, int worldZ){ + lock.lock(); + boolean rVal = worldPosFileMap.containsKey(getBlockChunkKey(worldX, worldY, worldZ)); + lock.unlock(); + return rVal; + } + + + /** + * Gets the block data chunk from disk if it exists, otherwise returns null + * @param worldX The x coordinate + * @param worldY The y coordinate + * @param worldZ The z coordinate + * @return The block data chunk if it exists, null otherwise + */ + public BlockChunkData getBlockChunk(int worldX, int worldY, int worldZ){ + lock.lock(); + LoggerInterface.loggerEngine.INFO("Load chunk " + worldX + " " + worldY + " " + worldZ); + BlockChunkData rVal = null; + if(containsBlocksAtPosition(worldX, worldY, worldZ)){ + //read file + String fileName = worldPosFileMap.get(getBlockChunkKey(worldX, worldY, worldZ)); + byte[] rawDataCompressed = FileUtils.loadBinaryFromSavePath(Globals.currentSave.getName(), fileName); + //decompress + byte[] rawData = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InflaterOutputStream inflaterInputStream = new InflaterOutputStream(out); + try { + inflaterInputStream.write(rawDataCompressed); + inflaterInputStream.flush(); + inflaterInputStream.close(); + rawData = out.toByteArray(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + //parse + if(rawData != null){ + ByteBuffer buffer = ByteBuffer.wrap(rawData); + ShortBuffer shortView = buffer.asShortBuffer(); + short[] type = new short[BlockChunkData.TOTAL_DATA_WIDTH]; + short[] metadata = new short[BlockChunkData.TOTAL_DATA_WIDTH]; + short firstType = -1; + boolean homogenous = true; + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + type[i] = shortView.get(); + if(firstType == -1){ + firstType = type[i]; + } else if(homogenous && firstType == type[i]){ + homogenous = false; + } + } + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + metadata[i] = shortView.get(); + } + rVal = new BlockChunkData(); + rVal.setType(type); + rVal.setMetadata(metadata); + rVal.setHomogenousValue(homogenous ? firstType : BlockChunkData.NOT_HOMOGENOUS); + rVal.setWorldX(worldX); + rVal.setWorldY(worldY); + rVal.setWorldZ(worldZ); + rVal.setLod(BlockChunkData.LOD_FULL_RES); + } + } + lock.unlock(); + return rVal; + } + + /** + * Saves a block data chunk to disk + * @param chunkData The block data chunk + */ + public void saveToDisk(BlockChunkData chunkData){ + lock.lock(); + LoggerInterface.loggerEngine.DEBUG("Save to disk: " + chunkData.getWorldX() + " " + chunkData.getWorldY() + " " + chunkData.getWorldZ()); + //get the file name for this chunk + String fileName = null; + Long chunkKey = getBlockChunkKey(chunkData.getWorldX(),chunkData.getWorldY(),chunkData.getWorldZ()); + if(worldPosFileMap.containsKey(chunkKey)){ + fileName = worldPosFileMap.get(chunkKey); + } else { + fileName = chunkKey + "b.dat"; + } + //generate binary for the file + short[] type = chunkData.getType(); + short[] metadata = chunkData.getMetadata(); + ByteBuffer buffer = ByteBuffer.allocate(BlockChunkData.TOTAL_DATA_WIDTH * 2 * 2); + ShortBuffer shortView = buffer.asShortBuffer(); + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + shortView.put(type[i]); + } + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + shortView.put(metadata[i]); + } + //compress + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DeflaterOutputStream deflaterInputStream = new DeflaterOutputStream(out); + try { + deflaterInputStream.write(buffer.array()); + deflaterInputStream.flush(); + deflaterInputStream.close(); + //write to disk + FileUtils.saveBinaryToSavePath(Globals.currentSave.getName(), fileName, out.toByteArray()); + //save to the map of filenames + worldPosFileMap.put(chunkKey,fileName); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + lock.unlock(); + } + +} diff --git a/src/main/java/electrosphere/server/block/editing/ServerBlockEditing.java b/src/main/java/electrosphere/server/block/editing/ServerBlockEditing.java new file mode 100644 index 00000000..6cc90b38 --- /dev/null +++ b/src/main/java/electrosphere/server/block/editing/ServerBlockEditing.java @@ -0,0 +1,30 @@ +package electrosphere.server.block.editing; + +import org.joml.Vector3d; +import org.joml.Vector3i; + +import electrosphere.server.datacell.Realm; + +/** + * Provides utilities for editing block (particularly brushes, etc) + */ +public class ServerBlockEditing { + + /** + * The minimum value before hard setting to 0 + */ + static final float MINIMUM_FULL_VALUE = 0.01f; + + /** + * Performs a block chunk edit. Basically has a sphere around the provided position that it attempts to add value to + * @param realm The realm to modify in + * @param worldPos The world position + * @param voxelPos The block position within the chunk at the world position + * @param type The new type of block + * @param metadata The new metadata for the block + */ + public static void editBlockChunk(Realm realm, Vector3d worldPos, Vector3i voxelPos, short type, short metadata){ + throw new UnsupportedOperationException("Unimplemented"); + } + +} diff --git a/src/main/java/electrosphere/server/block/manager/BlockChunkCache.java b/src/main/java/electrosphere/server/block/manager/BlockChunkCache.java new file mode 100644 index 00000000..ba51e02b --- /dev/null +++ b/src/main/java/electrosphere/server/block/manager/BlockChunkCache.java @@ -0,0 +1,237 @@ +package electrosphere.server.block.manager; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; + +import electrosphere.client.block.BlockChunkData; +import io.github.studiorailgun.HashUtils; + +/** + * Caches chunk data on the server + */ +public class BlockChunkCache { + + /** + * Number of chunks to cache + */ + static final int CACHE_SIZE = 1500; + + /** + * The size of the cache + */ + int cacheSize = CACHE_SIZE; + + /** + * The map of full res chunk key -> chunk data + */ + Map cacheMapFullRes = new ConcurrentHashMap(); + + /** + * The map of half res chunk key -> chunk data + */ + Map cacheMapHalfRes = new ConcurrentHashMap(); + + /** + * The map of quarter res chunk key -> chunk data + */ + Map cacheMapQuarterRes = new ConcurrentHashMap(); + + /** + * The map of eighth res chunk key -> chunk data + */ + Map cacheMapEighthRes = new ConcurrentHashMap(); + + /** + * The map of sixteenth res chunk key -> chunk data + */ + Map cacheMapSixteenthRes = new ConcurrentHashMap(); + + /** + * Tracks how recently a chunk has been queries for (used for evicting old chunks from cache) + */ + List queryRecencyQueue = new LinkedList(); + + /** + * Tracks what chunks are already queued to be asynchronously loaded. Used so we don't have two threads generating/fetching the same chunk + */ + Map queuedChunkMap = new HashMap(); + + /** + * The lock for thread safety + */ + Semaphore lock = new Semaphore(1); + + /** + * Gets the collection of server block chunks that are cached + * @return The collection of chunks + */ + public Collection getContents(){ + lock.acquireUninterruptibly(); + Collection rVal = Collections.unmodifiableCollection(cacheMapFullRes.values()); + lock.release(); + return rVal; + } + + /** + * Evicts all chunks in the cache + */ + public void clear(){ + lock.acquireUninterruptibly(); + cacheMapFullRes.clear(); + cacheMapHalfRes.clear(); + cacheMapQuarterRes.clear(); + cacheMapEighthRes.clear(); + cacheMapSixteenthRes.clear(); + lock.release(); + } + + /** + * Gets the chunk at a given world position + * @param worldX The world x coordinate + * @param worldY The world y coordinate + * @param worldZ The world z coordinate + * @param stride The stride of the data + * @return The chunk + */ + public BlockChunkData get(int worldX, int worldY, int worldZ, int stride){ + BlockChunkData rVal = null; + Long key = this.getKey(worldX, worldY, worldZ); + lock.acquireUninterruptibly(); + queryRecencyQueue.remove(key); + queryRecencyQueue.add(0, key); + Map cache = this.getCache(stride); + rVal = cache.get(key); + lock.release(); + return rVal; + } + + /** + * Adds a chunk to the cache + * @param worldX The world x coordinate of the chunk + * @param worldY The world y coordinate of the chunk + * @param worldZ The world z coordinate of the chunk + * @param stride The stride of the data + * @param chunk The chunk itself + */ + public void add(int worldX, int worldY, int worldZ, int stride, BlockChunkData chunk){ + Long key = this.getKey(worldX, worldY, worldZ); + lock.acquireUninterruptibly(); + queryRecencyQueue.add(0, key); + Map cache = this.getCache(stride); + cache.put(key, chunk); + while(queryRecencyQueue.size() > cacheSize){ + Long oldKey = queryRecencyQueue.remove(queryRecencyQueue.size() - 1); + cacheMapFullRes.remove(oldKey); + cacheMapHalfRes.remove(oldKey); + cacheMapQuarterRes.remove(oldKey); + cacheMapEighthRes.remove(oldKey); + cacheMapSixteenthRes.remove(oldKey); + } + lock.release(); + } + + /** + * Checks if the cache contains the chunk at a given world position + * @param worldX The world x coordinate + * @param worldY The world y coordinate + * @param worldZ The world z coordinate + * @param stride The stride of the data + * @return true if the cache contains this chunk, false otherwise + */ + public boolean containsChunk(int worldX, int worldY, int worldZ, int stride){ + Long key = this.getKey(worldX,worldY,worldZ); + lock.acquireUninterruptibly(); + Map cache = this.getCache(stride); + boolean rVal = cache.containsKey(key); + lock.release(); + return rVal; + } + + /** + * Gets the key for a given world position + * @param worldX The x component + * @param worldY The y component + * @param worldZ The z component + * @return The key + */ + public long getKey(int worldX, int worldY, int worldZ){ + return HashUtils.cantorHash(worldX, worldY, worldZ); + } + + /** + * Checks if the chunk is already queued or not + * @param worldX The world x position of the chunk + * @param worldY The world y position of the chunk + * @param worldZ The world z position of the chunk + * @return true if the chunk is already queued, false otherwise + */ + public boolean chunkIsQueued(int worldX, int worldY, int worldZ){ + Long key = this.getKey(worldX,worldY,worldZ); + lock.acquireUninterruptibly(); + boolean rVal = this.queuedChunkMap.containsKey(key); + lock.release(); + return rVal; + } + + /** + * Flags a chunk as queued + * @param worldX The world x position of the chunk + * @param worldY The world y position of the chunk + * @param worldZ The world z position of the chunk + */ + public void queueChunk(int worldX, int worldY, int worldZ){ + Long key = this.getKey(worldX,worldY,worldZ); + lock.acquireUninterruptibly(); + this.queuedChunkMap.put(key,true); + lock.release(); + } + + /** + * Unflags a chunk as queued + * @param worldX The world x position of the chunk + * @param worldY The world y position of the chunk + * @param worldZ The world z position of the chunk + * @param stride The stride of the chunk + */ + public void unqueueChunk(int worldX, int worldY, int worldZ, int stride){ + Long key = this.getKey(worldX,worldY,worldZ); + lock.acquireUninterruptibly(); + this.queuedChunkMap.remove(key); + lock.release(); + } + + /** + * Gets the cache + * @param stride The stride of the data + * @return The cache to use + */ + public Map getCache(int stride){ + switch(stride){ + case 0: { + return cacheMapFullRes; + } + case 1: { + return cacheMapHalfRes; + } + case 2: { + return cacheMapQuarterRes; + } + case 3: { + return cacheMapEighthRes; + } + case 4: { + return cacheMapSixteenthRes; + } + default: { + throw new Error("Invalid stride probided! " + stride); + } + } + } + +} diff --git a/src/main/java/electrosphere/server/block/manager/ServerBlockChunkGenerationThread.java b/src/main/java/electrosphere/server/block/manager/ServerBlockChunkGenerationThread.java new file mode 100644 index 00000000..589468fb --- /dev/null +++ b/src/main/java/electrosphere/server/block/manager/ServerBlockChunkGenerationThread.java @@ -0,0 +1,131 @@ +package electrosphere.server.block.manager; + +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import electrosphere.client.block.BlockChunkData; +import electrosphere.engine.Globals; +import electrosphere.logger.LoggerInterface; +import electrosphere.server.block.diskmap.ServerBlockChunkDiskMap; + +/** + * A job that fetches a chunk, either by generating it or by reading it from disk + */ +public class ServerBlockChunkGenerationThread implements Runnable { + + /** + * The number of milliseconds to wait per iteration + */ + static final int WAIT_TIME_MS = 2; + + /** + * The maximum number of iterations to wait before failing + */ + static final int MAX_TIME_TO_WAIT = 10; + + /** + * The chunk disk map + */ + ServerBlockChunkDiskMap chunkDiskMap; + + /** + * The chunk cache on the server + */ + BlockChunkCache chunkCache; + + /** + * The world x coordinate + */ + int worldX; + + /** + * The world y coordinate + */ + int worldY; + + /** + * The world z coordinate + */ + int worldZ; + + /** + * The stride of the data + */ + int stride; + + /** + * The work to do once the chunk is available + */ + Consumer onLoad; + + /** + * Creates the chunk generation job + * @param chunkDiskMap The chunk disk map + * @param chunkCache The chunk cache on the server + * @param worldX The world x coordinate + * @param worldY The world y coordinate + * @param worldZ The world z coordinate + * @param stride The stride of the data + * @param onLoad The work to do once the chunk is available + */ + public ServerBlockChunkGenerationThread( + ServerBlockChunkDiskMap chunkDiskMap, + BlockChunkCache chunkCache, + int worldX, int worldY, int worldZ, + int stride, + Consumer onLoad + ){ + this.chunkDiskMap = chunkDiskMap; + this.chunkCache = chunkCache; + this.worldX = worldX; + this.worldY = worldY; + this.worldZ = worldZ; + this.stride = stride; + this.onLoad = onLoad; + } + + @Override + public void run() { + BlockChunkData chunk = null; + int i = 0; + try { + while(chunk == null && i < MAX_TIME_TO_WAIT && Globals.threadManager.shouldKeepRunning()){ + if(chunkCache.containsChunk(worldX, worldY, worldZ, stride)){ + chunk = chunkCache.get(worldX, worldY, worldZ, stride); + } else { + //pull from disk if it exists + if(chunkDiskMap != null){ + if(chunkDiskMap.containsBlocksAtPosition(worldX, worldY, worldZ)){ + chunk = chunkDiskMap.getBlockChunk(worldX, worldY, worldZ); + } + } + //generate if it does not exist + if(chunk == null){ + //TODO: generate from macro-level data + chunk = BlockChunkData.allocate(); + } + if(chunk != null){ + chunkCache.add(worldX, worldY, worldZ, stride, chunk); + } + } + if(chunk == null){ + try { + TimeUnit.MILLISECONDS.sleep(WAIT_TIME_MS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + i++; + } + if(i >= MAX_TIME_TO_WAIT){ + throw new Error("Failed to resolve chunk!"); + } + this.onLoad.accept(chunk); + } catch (Error e){ + LoggerInterface.loggerEngine.ERROR(e); + } catch(Exception e){ + LoggerInterface.loggerEngine.ERROR(e); + } + } + +} diff --git a/src/main/java/electrosphere/server/block/manager/ServerBlockManager.java b/src/main/java/electrosphere/server/block/manager/ServerBlockManager.java new file mode 100644 index 00000000..c12a1c75 --- /dev/null +++ b/src/main/java/electrosphere/server/block/manager/ServerBlockManager.java @@ -0,0 +1,184 @@ +package electrosphere.server.block.manager; + +import electrosphere.client.block.BlockChunkData; +import electrosphere.engine.Globals; +import electrosphere.game.server.world.ServerWorldData; +import electrosphere.server.block.diskmap.ServerBlockChunkDiskMap; +import electrosphere.util.annotation.Exclude; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import org.joml.Vector3i; + +/** + * Provides an interface for the server to query information about block chunks + */ +public class ServerBlockManager { + + /** + * The number of threads for chunk generation + */ + public static final int GENERATION_THREAD_POOL_SIZE = 2; + + /** + * The parent world data + */ + ServerWorldData parent; + + /** + * The cache of chunks + */ + @Exclude + BlockChunkCache chunkCache = new BlockChunkCache(); + + //The map of chunk position <-> file on disk containing chunk data + ServerBlockChunkDiskMap chunkDiskMap = null; + + /** + * The threadpool for chunk generation + */ + @Exclude + static final ExecutorService chunkExecutorService = Executors.newFixedThreadPool(GENERATION_THREAD_POOL_SIZE); + + /** + * Constructor + */ + public ServerBlockManager( + ServerWorldData parent + ){ + this.parent = parent; + } + + ServerBlockManager(){ + + } + + /** + * Inits the chunk disk map + */ + public void generate(){ + this.chunkDiskMap = ServerBlockChunkDiskMap.init(); + } + + /** + * Saves the block cache backing this manager to a save file + * @param saveName The name of the save + */ + public void save(String saveName){ + //for each chunk, save via disk map + for(BlockChunkData chunk : this.chunkCache.getContents()){ + chunkDiskMap.saveToDisk(chunk); + } + //save disk map itself + if(chunkDiskMap != null){ + chunkDiskMap.save(); + } + } + + /** + * Loads a block manager from a save file + * @param saveName The name of the save + */ + public void load(String saveName){ + //load chunk disk map + chunkDiskMap = ServerBlockChunkDiskMap.init(saveName); + } + + /** + * Evicts all cached chunks + */ + public void evictAll(){ + this.chunkCache.clear(); + } + + /** + * Performs logic once a server chunk is available + * @param worldX The world x position + * @param worldY The world y position + * @param worldZ The world z position + * @return The BlockChunkData + */ + public BlockChunkData getChunk(int worldX, int worldY, int worldZ){ + Globals.profiler.beginAggregateCpuSample("ServerBlockManager.getChunk"); + //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING + BlockChunkData returnedChunk = null; + if(chunkCache.containsChunk(worldX,worldY,worldZ,BlockChunkData.LOD_FULL_RES)){ + returnedChunk = chunkCache.get(worldX,worldY,worldZ, BlockChunkData.LOD_FULL_RES); + } else { + //pull from disk if it exists + if(chunkDiskMap != null){ + if(chunkDiskMap.containsBlocksAtPosition(worldX, worldY, worldZ)){ + returnedChunk = chunkDiskMap.getBlockChunk(worldX, worldY, worldZ); + } + } + //generate if it does not exist + if(returnedChunk == null){ + returnedChunk = BlockChunkData.allocate(); + returnedChunk.setWorldX(worldX); + returnedChunk.setWorldY(worldY); + returnedChunk.setWorldZ(worldZ); + } + this.chunkCache.add(worldX, worldY, worldZ, BlockChunkData.LOD_FULL_RES, returnedChunk); + } + Globals.profiler.endCpuSample(); + return returnedChunk; + } + + /** + * Performs logic once a server chunk is available + * @param worldX The world x position + * @param worldY The world y position + * @param worldZ The world z position + * @param stride The stride of the data + * @param onLoad The logic to run once the chunk is available + */ + public void getChunkAsync(int worldX, int worldY, int worldZ, int stride, Consumer onLoad){ + Globals.profiler.beginAggregateCpuSample("ServerBlockManager.getChunkAsync"); + chunkExecutorService.submit(new ServerBlockChunkGenerationThread(chunkDiskMap, chunkCache, worldX, worldY, worldZ, stride, onLoad)); + Globals.profiler.endCpuSample(); + } + + /** + * Saves a given position's chunk to disk. + * Uses the current global save name + * @param position The position to save + */ + public void savePositionToDisk(Vector3i position){ + if(chunkDiskMap != null){ + chunkDiskMap.saveToDisk(getChunk(position.x, position.y, position.z)); + } + } + + /** + * Applies an edit to a block at a given location + * @param worldPos The world coordinates of the chunk to modify + * @param voxelPos The voxel coordinates of the voxel to modify + * @param type The type of block + * @param metadata The metadata of the block + */ + public void editBlockAtLocationToValue(Vector3i worldPos, Vector3i voxelPos, short type, short metadata){ + if(chunkCache.containsChunk(worldPos.x,worldPos.y,worldPos.z,BlockChunkData.LOD_FULL_RES)){ + BlockChunkData chunk = chunkCache.get(worldPos.x,worldPos.y,worldPos.z, BlockChunkData.LOD_FULL_RES); + chunk.setType(voxelPos.x, voxelPos.y, voxelPos.z, type); + chunk.setType(voxelPos.x, voxelPos.y, voxelPos.z, metadata); + } + } + + /** + * Sets the parent world data of this manager + * @param serverWorldData The parent world data + */ + public void setParent(ServerWorldData serverWorldData){ + this.parent = serverWorldData; + } + + /** + * Closes the generation threadpool + */ + public void closeThreads(){ + chunkExecutorService.shutdownNow(); + } + +} diff --git a/src/net/terrain.json b/src/net/terrain.json index 496656ba..1de957f8 100644 --- a/src/net/terrain.json +++ b/src/net/terrain.json @@ -218,6 +218,29 @@ "chunkData" ] }, + + { + "messageName" : "RequestReducedBlockData", + "description" : "Requests reduced resolution block data from the server", + "data" : [ + "worldX", + "worldY", + "worldZ", + "chunkResolution" + ] + }, + { + "messageName" : "SendReducedBlockData", + "description" : "Sends block data to the client", + "data" : [ + "worldX", + "worldY", + "worldZ", + "chunkResolution", + "homogenousValue", + "chunkData" + ] + }, { "messageName" : "RequestFluidData", "description" : "Requests a fluid data from the server",