package electrosphere.client.terrain.manager; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.Semaphore; import org.joml.Vector3i; import electrosphere.client.scene.ClientWorldData; import electrosphere.client.terrain.cache.ChunkData; import electrosphere.client.terrain.cache.ClientTerrainCache; import electrosphere.client.terrain.cells.ClientDrawCellManager; import electrosphere.client.terrain.cells.VoxelTextureAtlas; import electrosphere.engine.Globals; import electrosphere.entity.types.terrain.TerrainChunkData; import electrosphere.logger.LoggerInterface; import electrosphere.net.parser.net.message.TerrainMessage; import electrosphere.renderer.meshgen.TerrainChunkModelGeneration; import electrosphere.renderer.model.Model; import electrosphere.server.terrain.manager.ServerTerrainChunk; import electrosphere.server.terrain.manager.ServerTerrainManager; import io.github.studiorailgun.HashUtils; /** * Manages terrain storage and access on the client */ public class ClientTerrainManager { //queues messages from server List messageQueue = new LinkedList(); /** * Locks the terrain manager (eg if adding message from network) */ static Semaphore lock = new Semaphore(1); /** * Maximum concurrent terrain requests */ public static final int MAX_CONCURRENT_REQUESTS = 500; /** * Number of frames to wait before flagging a request as failed */ public static final int FAILED_REQUEST_THRESHOLD = 500; //The interpolation ratio of terrain public static final int INTERPOLATION_RATIO = ServerTerrainManager.SERVER_TERRAIN_MANAGER_INTERPOLATION_RATIO; //caches chunks from server static final int CACHE_SIZE = 5000 + (int)(ClientDrawCellManager.FULL_RES_DIST * 10) + (int)(ClientDrawCellManager.HALF_RES_DIST * 10); /** * Size of the cache in bytes */ static final int CACHE_SIZE_IN_MB = (CACHE_SIZE * ServerTerrainChunk.CHUNK_DIMENSION * ServerTerrainChunk.CHUNK_DIMENSION * ServerTerrainChunk.CHUNK_DIMENSION * 2 * 4) / 1024 / 1024; //used for caching the macro values ClientTerrainCache terrainCache; //The world data for the client ClientWorldData clientWorldData; //The queue of terrain chunk data to be buffered to gpu static List terrainChunkGenerationQueue = new LinkedList(); /** * Tracks what outgoing requests are currently active */ Map requestedMap = new HashMap(); /** * Constructor */ public ClientTerrainManager(){ terrainCache = new ClientTerrainCache(CACHE_SIZE); } /** * Handles messages that have been received from the server */ public void handleMessages(){ Globals.profiler.beginCpuSample("ClientTerrainManager.handleMessages"); lock.acquireUninterruptibly(); List bouncedMessages = new LinkedList(); for(TerrainMessage message : messageQueue){ switch(message.getMessageSubtype()){ case SENDCHUNKDATA: { int[][][] values = new int[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE]; float[][][] weights = new float[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE]; ByteBuffer buffer = ByteBuffer.wrap(message.getchunkData()); FloatBuffer floatBuffer = buffer.asFloatBuffer(); for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){ for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){ for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){ weights[x][y][z] = floatBuffer.get(); } } } IntBuffer intView = buffer.asIntBuffer(); intView.position(floatBuffer.position()); for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){ for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){ for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){ values[x][y][z] = intView.get(); } } } ChunkData data = new ChunkData(message.getworldX(), message.getworldY(), message.getworldZ(), ChunkData.NO_STRIDE, ChunkData.NOT_HOMOGENOUS); data.setVoxelType(values); data.setVoxelWeight(weights); terrainCache.addChunkDataToCache( message.getworldX(), message.getworldY(), message.getworldZ(), data ); } break; case SENDREDUCEDCHUNKDATA: { int[][][] values = new int[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE]; float[][][] weights = new float[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE]; ByteBuffer buffer = ByteBuffer.wrap(message.getchunkData()); FloatBuffer floatBuffer = buffer.asFloatBuffer(); for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){ for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){ for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){ weights[x][y][z] = floatBuffer.get(); } } } IntBuffer intView = buffer.asIntBuffer(); intView.position(floatBuffer.position()); int firstType = -1; boolean homogenous = true; for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){ for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){ for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){ values[x][y][z] = intView.get(); if(firstType == -1){ firstType = values[x][y][z]; } else if(homogenous && firstType == values[x][y][z]){ homogenous = false; } } } } ChunkData data = new ChunkData(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution(),homogenous ? firstType : ChunkData.NOT_HOMOGENOUS); data.setVoxelType(values); data.setVoxelWeight(weights); terrainCache.addChunkDataToCache( message.getworldX(), message.getworldY(), message.getworldZ(), data ); //remove from request map this.requestedMap.remove(this.getRequestKey(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution())); } break; default: LoggerInterface.loggerEngine.WARNING("ClientTerrainManager: unhandled network message of type" + message.getMessageSubtype()); break; } } messageQueue.clear(); for(TerrainMessage message : bouncedMessages){ messageQueue.add(message); } //evaluate if any chunks have failed to request for(Long key : this.requestedMap.keySet()){ int duration = this.requestedMap.get(key); if(duration > FAILED_REQUEST_THRESHOLD){ this.requestedMap.remove(key); } else { this.requestedMap.put(key,duration + 1); } } lock.release(); Globals.profiler.endCpuSample(); } /** * Evicts all cached terrain */ public void evictAll(){ this.terrainCache.evictAll(); } /** * Attaches a terrain message to the queue of messages that this manager needs to process * @param message The message */ public void attachTerrainMessage(TerrainMessage message){ lock.acquireUninterruptibly(); messageQueue.add(message); lock.release(); } /** * Checks if the terrain cache contains chunk data at a given world position * @param worldX the x position * @param worldY the y position * @param worldZ the z position * @param stride The stride of the data * @return true if the data exists, false otherwise */ public boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ, int stride){ return terrainCache.containsChunkDataAtWorldPoint(worldX, worldY, worldZ, stride); } /** * Checks if the terrain cache contains chunk data at a given world position * @param worldPos The vector containing the world-space position * @param stride The stride of the data * @return true if the data exists, false otherwise */ public boolean containsChunkDataAtWorldPoint(Vector3i worldPos, int stride){ return this.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, stride); } /** * Requests a chunk from the server * @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 * @return true if the request was successfully sent, false otherwise */ public boolean requestChunk(int worldX, int worldY, int worldZ, int stride){ boolean rVal = false; lock.acquireUninterruptibly(); if(this.requestedMap.size() < MAX_CONCURRENT_REQUESTS && !this.requestedMap.containsKey(this.getRequestKey(worldX, worldY, worldZ, stride))){ Globals.clientConnection.queueOutgoingMessage(TerrainMessage.constructRequestReducedChunkDataMessage( worldX, worldY, worldZ, stride )); this.requestedMap.put(this.getRequestKey(worldX, worldY, worldZ, stride), 0); rVal = true; } lock.release(); return rVal; } /** * Gets the chunk data at a given world position * @param worldX The x component of the world coordinate * @param worldY The y component of the world coordinate * @param worldZ The z component of the world coordinate * @param stride The stride of the data * @return The chunk data if it exists, otherwise null */ public ChunkData getChunkDataAtWorldPoint(int worldX, int worldY, int worldZ, int stride){ return terrainCache.getSubChunkDataAtPoint(worldX, worldY, worldZ, stride); } /** * Gets the chunk data at a given world position * @param worldPos The world position as a joml vector * @param stride The stride of the data * @return The chunk data if it exists, otherwise null */ public ChunkData getChunkDataAtWorldPoint(Vector3i worldPos, int stride){ return this.getChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, stride); } /** * Queues a terrain chunk to be pushed to GPU based on chunk data * @param data The chunk data (triangles, normals, etc) * @return The model path that is promised to eventually reflect the terrain model when it makes it to gpu */ public static String queueTerrainGridGeneration(TerrainChunkData data, VoxelTextureAtlas atlas){ String promisedHash = ""; UUID newUUID = UUID.randomUUID(); promisedHash = newUUID.toString(); TerrainChunkGenQueueItem queueItem = new TerrainChunkGenQueueItem(data, promisedHash, atlas); lock.acquireUninterruptibly(); terrainChunkGenerationQueue.add(queueItem); lock.release(); return promisedHash; } /** * Pushes all terrain data in queue to the gpu and registers the resulting models */ public static void generateTerrainChunkGeometry(){ Globals.profiler.beginCpuSample("ClientTerrainManager.generateTerrainChunkGeometry"); lock.acquireUninterruptibly(); for(TerrainChunkGenQueueItem queueItem : terrainChunkGenerationQueue){ Model terrainModel = TerrainChunkModelGeneration.generateTerrainModel(queueItem.getData(), queueItem.getAtlas()); Globals.assetManager.registerModelToSpecificString(terrainModel, queueItem.getPromisedHash()); } terrainChunkGenerationQueue.clear(); lock.release(); Globals.profiler.endCpuSample(); } /** * Gets all chunks in the terrain cache * @return The collection of all chunk data objects */ public Collection getAllChunks(){ return terrainCache.getAllChunks(); } /** * Gets the world position of a given chunk * @param chunk The chunk * @return The world position of the chunk */ public Vector3i getPositionOfChunk(ChunkData chunk){ return terrainCache.getChunkPosition(chunk); } /** * Gets the key for a given request * @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 key */ private Long getRequestKey(int worldX, int worldY, int worldZ, int stride){ return (long)HashUtils.cantorHash(worldY, worldZ, worldZ); } }