From 749b9c63b45ae9d24e1d32e0d9de5f2657781ccc Mon Sep 17 00:00:00 2001 From: austin Date: Sat, 23 Nov 2024 22:52:52 -0500 Subject: [PATCH] Add client side handling of block endpoints --- docs/src/progress/renderertodo.md | 1 + .../client/block/BlockChunkData.java | 10 + .../client/block/ClientBlockManager.java | 296 ++++++++++++++++++ .../client/sim/ClientSimulation.java | 19 +- .../terrain/manager/ClientTerrainManager.java | 45 ++- .../java/electrosphere/engine/Globals.java | 10 +- .../net/client/protocol/TerrainProtocol.java | 4 + 7 files changed, 375 insertions(+), 10 deletions(-) create mode 100644 src/main/java/electrosphere/client/block/ClientBlockManager.java diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index afdc285c..f4bf22d7 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -1148,6 +1148,7 @@ 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 +Add client side handling of block endpoint # TODO diff --git a/src/main/java/electrosphere/client/block/BlockChunkData.java b/src/main/java/electrosphere/client/block/BlockChunkData.java index 563ca43b..7bd53ce8 100644 --- a/src/main/java/electrosphere/client/block/BlockChunkData.java +++ b/src/main/java/electrosphere/client/block/BlockChunkData.java @@ -1,5 +1,7 @@ package electrosphere.client.block; +import org.joml.Vector3i; + import electrosphere.server.terrain.manager.ServerTerrainChunk; /** @@ -273,6 +275,14 @@ public class BlockChunkData { this.worldZ = worldZ; } + /** + * Gets the world position of this chunk + * @return The world position + */ + public Vector3i getWorldPos(){ + return new Vector3i(worldX, worldY, worldZ); + } + /** * Gets the homogenous value for this chunk * @return The homogenous value diff --git a/src/main/java/electrosphere/client/block/ClientBlockManager.java b/src/main/java/electrosphere/client/block/ClientBlockManager.java new file mode 100644 index 00000000..b78f5d35 --- /dev/null +++ b/src/main/java/electrosphere/client/block/ClientBlockManager.java @@ -0,0 +1,296 @@ +package electrosphere.client.block; + +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; + +import org.joml.Vector3i; + +import electrosphere.client.scene.ClientWorldData; +import electrosphere.client.terrain.cells.ClientDrawCellManager; +import electrosphere.client.terrain.cells.DrawCell; +import electrosphere.engine.Globals; +import electrosphere.entity.Entity; +import electrosphere.logger.LoggerInterface; +import electrosphere.net.parser.net.message.TerrainMessage; +import electrosphere.renderer.meshgen.BlockMeshgen.BlockMeshData; +import electrosphere.server.terrain.manager.ServerTerrainChunk; +import electrosphere.server.terrain.manager.ServerTerrainManager; +import io.github.studiorailgun.HashUtils; + +public class ClientBlockManager { + //queues messages from server + List messageQueue = new LinkedList(); + + /** + * Locks the block manager (eg if adding message from network) + */ + static Semaphore lock = new Semaphore(1); + + /** + * Maximum concurrent block 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 block + public static final int INTERPOLATION_RATIO = ServerTerrainManager.SERVER_TERRAIN_MANAGER_INTERPOLATION_RATIO; + + //caches chunks from server + static final int CACHE_SIZE = 2500 + (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; + + /** + * Caches block data + */ + BlockChunkCache blockCache; + + //The world data for the client + ClientWorldData clientWorldData; + + //The queue of block chunk data to be buffered to gpu + // static List terrainChunkGenerationQueue = new LinkedList(); + + /** + * Tracks what outgoing requests are currently active + */ + Map requestedMap = new HashMap(); + + /** + * Used to clear the request map + */ + List toClearFailedRequests = new LinkedList(); + + /** + * Constructor + */ + public ClientBlockManager(){ + blockCache = new BlockChunkCache(); + } + + + /** + * Handles messages that have been received from the server + */ + public void handleMessages(){ + Globals.profiler.beginCpuSample("ClientBlockManager.handleMessages"); + lock.acquireUninterruptibly(); + List bouncedMessages = new LinkedList(); + for(TerrainMessage message : messageQueue){ + switch(message.getMessageSubtype()){ + case SENDREDUCEDBLOCKDATA: { + //construct return obj + BlockChunkData data = new BlockChunkData(); + data.setWorldX(message.getworldX()); + data.setWorldY(message.getworldY()); + data.setWorldZ(message.getworldZ()); + data.setHomogenousValue((short)message.gethomogenousValue()); + + //read main data + if(data.getHomogenousValue() == BlockChunkData.NOT_HOMOGENOUS){ + short[] type = new short[BlockChunkData.TOTAL_DATA_WIDTH]; + short[] metadata = new short[BlockChunkData.TOTAL_DATA_WIDTH]; + ByteBuffer buffer = ByteBuffer.wrap(message.getchunkData()); + ShortBuffer shortBuffer = buffer.asShortBuffer(); + + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + type[i] = shortBuffer.get(); + } + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + metadata[i] = shortBuffer.get(); + } + data.setType(type); + data.setMetadata(metadata); + } + blockCache.add(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution(), data); + //remove from request map + this.requestedMap.remove(this.getRequestKey(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution())); + } break; + default: + LoggerInterface.loggerEngine.WARNING("ClientBlockManager: unhandled network message of type" + message.getMessageSubtype()); + break; + } + } + messageQueue.clear(); + for(TerrainMessage message : bouncedMessages){ + messageQueue.add(message); + } + //evaluate if any terrain chunks have failed to request + for(Long key : this.requestedMap.keySet()){ + int duration = this.requestedMap.get(key); + if(duration > FAILED_REQUEST_THRESHOLD){ + toClearFailedRequests.add(key); + } else { + this.requestedMap.put(key,duration + 1); + } + } + if(this.toClearFailedRequests.size() > 0){ + for(Long key : toClearFailedRequests){ + this.requestedMap.remove(key); + } + this.toClearFailedRequests.clear(); + } + lock.release(); + Globals.profiler.endCpuSample(); + } + + /** + * Evicts all cached blocks + */ + public void evictAll(){ + this.blockCache.clear(); + } + + /** + * 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 block 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 blockCache.containsChunk(worldX, worldY, worldZ, stride); + } + + /** + * Checks if the block 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.constructRequestReducedBlockDataMessage( + 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 BlockChunkData getChunkDataAtWorldPoint(int worldX, int worldY, int worldZ, int stride){ + return blockCache.get(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 BlockChunkData getChunkDataAtWorldPoint(Vector3i worldPos, int stride){ + return this.getChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, stride); + } + + + + /** + * Queues a block 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 block model when it makes it to gpu + */ + public static String queueBlockGridGeneration(BlockMeshData data, DrawCell notifyTarget, Entity toDelete){ + throw new UnsupportedOperationException("Unimplemented"); + // String promisedHash = ""; + // UUID newUUID = UUID.randomUUID(); + // promisedHash = newUUID.toString(); + // TerrainChunkGenQueueItem queueItem = new TerrainChunkGenQueueItem(data, promisedHash, atlas, notifyTarget, toDelete); + // lock.acquireUninterruptibly(); + // terrainChunkGenerationQueue.add(queueItem); + // lock.release(); + // return promisedHash; + } + + /** + * Pushes all block data in queue to the gpu and registers the resulting models + */ + public static void generateBlockChunkGeometry(){ + throw new UnsupportedOperationException("Unimplemented"); + // Globals.profiler.beginCpuSample("ClientBlockManager.generateTerrainChunkGeometry"); + // lock.acquireUninterruptibly(); + // for(TerrainChunkGenQueueItem queueItem : terrainChunkGenerationQueue){ + // Model terrainModel = TransvoxelModelGeneration.generateTerrainModel(queueItem.getData(), queueItem.getAtlas()); + // Globals.assetManager.registerModelToSpecificString(terrainModel, queueItem.getPromisedHash()); + // if(queueItem.notifyTarget != null){ + // queueItem.notifyTarget.alertToGeneration(); + // } + // if(queueItem.toDelete != null){ + // ClientEntityUtils.destroyEntity(queueItem.toDelete); + // } + // } + // 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 blockCache.getContents(); + } + + /** + * 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); + } +} diff --git a/src/main/java/electrosphere/client/sim/ClientSimulation.java b/src/main/java/electrosphere/client/sim/ClientSimulation.java index cc53f728..81dc6ed5 100644 --- a/src/main/java/electrosphere/client/sim/ClientSimulation.java +++ b/src/main/java/electrosphere/client/sim/ClientSimulation.java @@ -169,11 +169,15 @@ public class ClientSimulation { Globals.profiler.beginCpuSample("ClientSimulation.loadTerrain"); if(Globals.clientTerrainManager != null){ Globals.clientTerrainManager.handleMessages(); - updateTerrainCellManager(); + this.updateTerrainCellManager(); } if(Globals.clientFluidManager != null){ Globals.clientFluidManager.handleMessages(); - updateFluidCellManager(); + this.updateFluidCellManager(); + } + if(Globals.clientBlockManager != null){ + Globals.clientBlockManager.handleMessages(); + this.updateBlockCellManager(); } Globals.profiler.endCpuSample(); } @@ -181,7 +185,7 @@ public class ClientSimulation { /** * Updates the terrain cell manager (specifically position handling) */ - void updateTerrainCellManager(){ + private void updateTerrainCellManager(){ /// /// C L I E N T C E L L M A N A G E R /// @@ -194,12 +198,19 @@ public class ClientSimulation { /** * Updates the fluid cell manager (specifically position handling) */ - void updateFluidCellManager(){ + private void updateFluidCellManager(){ //fluid work if(Globals.fluidCellManager != null && Globals.clientWorldData != null){ Globals.fluidCellManager.update(); } } + + /** + * Updates the block cell manager + */ + private void updateBlockCellManager(){ + + } /** * Gets whether the client simulation is ready to execute diff --git a/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java b/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java index c98985f0..4488c06f 100644 --- a/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java +++ b/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java @@ -3,6 +3,7 @@ package electrosphere.client.terrain.manager; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import java.nio.ShortBuffer; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; @@ -13,6 +14,8 @@ import java.util.concurrent.Semaphore; import org.joml.Vector3i; +import electrosphere.client.block.BlockChunkCache; +import electrosphere.client.block.BlockChunkData; import electrosphere.client.scene.ClientWorldData; import electrosphere.client.terrain.cache.ChunkData; import electrosphere.client.terrain.cache.ClientTerrainCache; @@ -67,6 +70,11 @@ public class ClientTerrainManager { //used for caching the macro values ClientTerrainCache terrainCache; + + /** + * Caches block data + */ + BlockChunkCache blockCache; //The world data for the client ClientWorldData clientWorldData; @@ -79,6 +87,11 @@ public class ClientTerrainManager { */ Map requestedMap = new HashMap(); + /** + * Tracks what outgoing block requests are currently active + */ + Map requestedBlockMap = new HashMap(); + /** * Used to clear the request map */ @@ -89,6 +102,7 @@ public class ClientTerrainManager { */ public ClientTerrainManager(){ terrainCache = new ClientTerrainCache(CACHE_SIZE); + blockCache = new BlockChunkCache(); } @@ -170,6 +184,34 @@ public class ClientTerrainManager { //remove from request map this.requestedMap.remove(this.getRequestKey(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution())); } break; + case SENDREDUCEDBLOCKDATA: { + //construct return obj + BlockChunkData data = new BlockChunkData(); + data.setWorldX(message.getworldX()); + data.setWorldY(message.getworldY()); + data.setWorldZ(message.getworldZ()); + data.setHomogenousValue((short)message.gethomogenousValue()); + + //read main data + if(data.getHomogenousValue() == BlockChunkData.NOT_HOMOGENOUS){ + short[] type = new short[BlockChunkData.TOTAL_DATA_WIDTH]; + short[] metadata = new short[BlockChunkData.TOTAL_DATA_WIDTH]; + ByteBuffer buffer = ByteBuffer.wrap(message.getchunkData()); + ShortBuffer shortBuffer = buffer.asShortBuffer(); + + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + type[i] = shortBuffer.get(); + } + for(int i = 0; i < BlockChunkData.TOTAL_DATA_WIDTH; i++){ + metadata[i] = shortBuffer.get(); + } + data.setType(type); + data.setMetadata(metadata); + } + blockCache.add(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution(), data); + //remove from request map + this.requestedBlockMap.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; @@ -179,7 +221,7 @@ public class ClientTerrainManager { for(TerrainMessage message : bouncedMessages){ messageQueue.add(message); } - //evaluate if any chunks have failed to request + //evaluate if any terrain chunks have failed to request for(Long key : this.requestedMap.keySet()){ int duration = this.requestedMap.get(key); if(duration > FAILED_REQUEST_THRESHOLD){ @@ -203,6 +245,7 @@ public class ClientTerrainManager { */ public void evictAll(){ this.terrainCache.evictAll(); + this.blockCache.clear(); } /** diff --git a/src/main/java/electrosphere/engine/Globals.java b/src/main/java/electrosphere/engine/Globals.java index ed6747c9..82cdae57 100644 --- a/src/main/java/electrosphere/engine/Globals.java +++ b/src/main/java/electrosphere/engine/Globals.java @@ -13,6 +13,7 @@ import electrosphere.audio.VirtualAudioSourceManager; import electrosphere.audio.collision.HitboxAudioService; import electrosphere.audio.movement.MovementAudioService; import electrosphere.auth.AuthenticationManager; +import electrosphere.client.block.ClientBlockManager; import electrosphere.client.chemistry.ClientChemistryCollisionCallback; import electrosphere.client.entity.particle.ParticleService; import electrosphere.client.fluid.cells.FluidCellManager; @@ -349,11 +350,10 @@ public class Globals { //client world data public static ClientWorldData clientWorldData; - //client terrain manager + //client gridded manager public static ClientTerrainManager clientTerrainManager; - - //client fluid manager public static ClientFluidManager clientFluidManager; + public static ClientBlockManager clientBlockManager; //client player data public static ClientPlayerData clientPlayerData = new ClientPlayerData(); @@ -501,10 +501,10 @@ public class Globals { entityDataCellMapper = new EntityDataCellMapper(); //nav mesh manager navMeshManager = new NavMeshManager(); - //terrain + //gridded managers Globals.clientTerrainManager = new ClientTerrainManager(); - //fluid Globals.clientFluidManager = new ClientFluidManager(); + Globals.clientBlockManager = new ClientBlockManager(); //game config gameConfigDefault = electrosphere.game.data.Config.loadDefaultConfig(); gameConfigCurrent = gameConfigDefault; diff --git a/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java b/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java index f0414a80..7997797d 100644 --- a/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java +++ b/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java @@ -60,6 +60,10 @@ public class TerrainProtocol implements ClientProtocolTemplate { LoggerInterface.loggerNetworking.DEBUG("(Client) Received terrain at " + message.getworldX() + " " + message.getworldY() + " " + message.getworldZ() + " " + message.getchunkResolution()); Globals.clientTerrainManager.attachTerrainMessage(message); } break; + case SENDREDUCEDBLOCKDATA: { + LoggerInterface.loggerNetworking.DEBUG("(Client) Received blocks at " + message.getworldX() + " " + message.getworldY() + " " + message.getworldZ() + " " + message.getchunkResolution()); + Globals.clientBlockManager.attachTerrainMessage(message); + } break; case UPDATEVOXEL: { // //find what all drawcells might be updated by this voxel update