Server structures for dealing with block data

This commit is contained in:
austin 2024-11-23 22:27:08 -05:00
parent 3777a9a37c
commit 874dcafa97
13 changed files with 1188 additions and 6 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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){

View File

@ -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);

View File

@ -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<Byte> temporaryByteQueue = new LinkedList<Byte>();
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

View File

@ -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;
/*

View File

@ -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<TerrainMessage> {
);
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<TerrainMessage> {
case SENDFLUIDDATA:
case REQUESTREDUCEDCHUNKDATA:
case SENDREDUCEDCHUNKDATA:
case REQUESTREDUCEDBLOCKDATA:
case SENDREDUCEDBLOCKDATA:
//silently ignore
break;
}
@ -262,6 +272,57 @@ public class TerrainProtocol implements ServerProtocolTemplate<TerrainMessage> {
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<BlockChunkData> 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

View File

@ -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<Long,String> worldPosFileMap;
/**
* Locks the chunk disk map for thread safety
*/
@Exclude
ReentrantLock lock = new ReentrantLock();
/**
* Constructor
*/
private ServerBlockChunkDiskMap(){
worldPosFileMap = new HashMap<Long,String>();
}
/**
* 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();
}
}

View File

@ -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");
}
}

View File

@ -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<Long,BlockChunkData> cacheMapFullRes = new ConcurrentHashMap<Long,BlockChunkData>();
/**
* The map of half res chunk key -> chunk data
*/
Map<Long,BlockChunkData> cacheMapHalfRes = new ConcurrentHashMap<Long,BlockChunkData>();
/**
* The map of quarter res chunk key -> chunk data
*/
Map<Long,BlockChunkData> cacheMapQuarterRes = new ConcurrentHashMap<Long,BlockChunkData>();
/**
* The map of eighth res chunk key -> chunk data
*/
Map<Long,BlockChunkData> cacheMapEighthRes = new ConcurrentHashMap<Long,BlockChunkData>();
/**
* The map of sixteenth res chunk key -> chunk data
*/
Map<Long,BlockChunkData> cacheMapSixteenthRes = new ConcurrentHashMap<Long,BlockChunkData>();
/**
* Tracks how recently a chunk has been queries for (used for evicting old chunks from cache)
*/
List<Long> queryRecencyQueue = new LinkedList<Long>();
/**
* Tracks what chunks are already queued to be asynchronously loaded. Used so we don't have two threads generating/fetching the same chunk
*/
Map<Long, Boolean> queuedChunkMap = new HashMap<Long,Boolean>();
/**
* 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<BlockChunkData> getContents(){
lock.acquireUninterruptibly();
Collection<BlockChunkData> 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<Long,BlockChunkData> 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<Long,BlockChunkData> 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<Long,BlockChunkData> 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<Long,BlockChunkData> 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);
}
}
}
}

View File

@ -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<BlockChunkData> 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<BlockChunkData> 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);
}
}
}

View File

@ -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<BlockChunkData> 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();
}
}

View File

@ -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",