package electrosphere.server.datacell.gridded; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.ReentrantLock; import org.joml.Vector3d; import org.joml.Vector3i; import electrosphere.client.block.BlockChunkData; import electrosphere.engine.Globals; import electrosphere.entity.Entity; import electrosphere.entity.EntityCreationUtils; import electrosphere.entity.EntityUtils; import electrosphere.entity.ServerEntityUtils; import electrosphere.entity.state.server.ServerPlayerViewDirTree; import electrosphere.game.server.world.ServerWorldData; import electrosphere.logger.LoggerInterface; import electrosphere.net.parser.net.message.EntityMessage; import electrosphere.net.parser.net.message.TerrainMessage; import electrosphere.net.server.player.Player; import electrosphere.net.server.protocol.TerrainProtocol; import electrosphere.server.block.manager.ServerBlockManager; import electrosphere.server.content.ServerContentManager; import electrosphere.server.content.serialization.ContentSerialization; import electrosphere.server.datacell.Realm; import electrosphere.server.datacell.ServerDataCell; import electrosphere.server.datacell.interfaces.DataCellManager; import electrosphere.server.datacell.interfaces.VoxelCellManager; import electrosphere.server.datacell.physics.PhysicsDataCell; import electrosphere.server.fluid.manager.ServerFluidChunk; import electrosphere.server.fluid.manager.ServerFluidManager; import electrosphere.server.terrain.manager.ServerTerrainManager; import electrosphere.server.terrain.models.TerrainModel; import electrosphere.util.math.HashUtils; import electrosphere.server.terrain.manager.ServerTerrainChunk; /** * Implementation of DataCellManager that lays out cells in a logical grid (array). Useful for eg 3d terrain gridded world. */ public class GriddedDataCellManager implements DataCellManager, VoxelCellManager { /** * The minimum grid size allowed */ public static final int MIN_GRID_SIZE = 1; /** * The max grid size allowed */ public static final int MAX_GRID_SIZE = TerrainModel.MAX_MACRO_DATA_SIZE * TerrainModel.DEFAULT_MACRO_DATA_SCALE * ServerTerrainChunk.CHUNK_DIMENSION; /** * The number of frames without players that must pass before a server data cell is unloaded */ static final int UNLOAD_FRAME_THRESHOLD = 100; /** * Used for generating physics chunks */ static final ExecutorService generationService = Executors.newFixedThreadPool(4); /** * Tracks whether this manager has been flagged to unload cells or not */ boolean unloadCells = true; /** * These are going to be the natural ground grid of data cells, but we're going to have more than this */ Map groundDataCells = new HashMap(); /** * Map of server cell to its world position */ Map cellPositionMap = new HashMap(); /** * Map of server data cell to the number of frames said cell has had no players */ Map cellPlayerlessFrameMap = new HashMap(); /** * Loaded cells */ ReentrantLock loadedCellsLock = new ReentrantLock(); /** * Parent realm */ Realm parent; /** * The world data of the parent */ ServerWorldData serverWorldData; /** * Manager for terrain for this particular cell manager */ ServerTerrainManager serverTerrainManager; /** * Manager for fluids for this particular cell manager */ ServerFluidManager serverFluidManager; /** * Lock for terrain editing */ Semaphore terrainEditLock = new Semaphore(1); /** * Manager for getting entities to fill in a cell */ ServerContentManager serverContentManager; /** * Used for cleaning server data cells no longer in use from the realm */ Set toCleanQueue = new HashSet(); /** * Map of world position key -> physics cell */ Map posPhysicsMap = new HashMap(); /** * Number of data cells cleaned up in the most recent frame */ int numCleaned = 0; /** * Constructor * @param parent The gridded data cell manager's parent realm */ public GriddedDataCellManager( Realm parent ) { this.parent = parent; this.serverWorldData = this.parent.getServerWorldData(); this.serverTerrainManager = serverWorldData.getServerTerrainManager(); this.serverFluidManager = serverWorldData.getServerFluidManager(); this.serverContentManager = this.parent.getServerContentManager(); //Assert the gridded data cell manager was given good data if( this.parent == null || this.serverWorldData == null || this.serverTerrainManager == null || this.serverFluidManager == null || this.serverContentManager == null ){ throw new IllegalStateException("Tried to create a GriddedDataCellManager with invalid parameters " + this.parent + " " + this.serverWorldData + " " + this.serverTerrainManager + " " + this.serverFluidManager + " " + this.serverContentManager + " " ); } } /** * Adds a player to the realm that this manager controls. Should do this intelligently based on the player's location * @param player The player */ public void addPlayerToRealm(Player player){ Globals.realmManager.setPlayerRealm(player, parent); int playerSimulationRadius = player.getSimulationRadius(); Vector3i worldPos = player.getWorldPos(); for(int x = worldPos.x - playerSimulationRadius; x < worldPos.x + playerSimulationRadius + 1; x++){ for(int y = worldPos.y - playerSimulationRadius; y < worldPos.y + playerSimulationRadius + 1; y++){ for(int z = worldPos.z - playerSimulationRadius; z < worldPos.z + playerSimulationRadius + 1; z++){ if(this.canCreateCell(x, y, z) && this.shouldContainPlayer(new Vector3i(x,y,z), worldPos, playerSimulationRadius)){ Vector3i targetPos = new Vector3i(x,y,z); LoggerInterface.loggerEngine.DEBUG("GriddedDataCellManager: Add player to " + x + " " + y + " " + z); loadedCellsLock.lock(); if(groundDataCells.get(getServerDataCellKey(targetPos)) != null){ groundDataCells.get(getServerDataCellKey(targetPos)).addPlayer(player); } else { LoggerInterface.loggerEngine.DEBUG("Creating new cell @ " + x + " " + y + " " + z); //create data cell this.createServerDataCell(targetPos); //add player groundDataCells.get(getServerDataCellKey(targetPos)).addPlayer(player); } loadedCellsLock.unlock(); } } } } } /** * Moves a player to a new position * @param player The player * @param newPosition The new position */ public void movePlayer(Player player, Vector3i newPosition){ int playerSimulationRadius = player.getSimulationRadius(); player.setWorldPos(newPosition); for(ServerDataCell cell : this.groundDataCells.values()){ Vector3i worldPos = this.getCellWorldPosition(cell); if(cell.containsPlayer(player) && !this.shouldContainPlayer(worldPos, newPosition, playerSimulationRadius)){ cell.removePlayer(player); this.broadcastDestructionToPlayer(player, cell); if(cell.getScene().containsEntity(player.getPlayerEntity())){ throw new Error( "Unregistering player from cell that contains player's entity!\n " + player + "\n " + worldPos + "\n " + player.getPlayerEntity() + "\n " + EntityUtils.getPosition(player.getPlayerEntity()) ); } } } for(int x = newPosition.x - playerSimulationRadius; x < newPosition.x + playerSimulationRadius + 1; x++){ for(int y = newPosition.y - playerSimulationRadius; y < newPosition.y + playerSimulationRadius + 1; y++){ for(int z = newPosition.x - playerSimulationRadius; z < newPosition.z + playerSimulationRadius + 1; z++){ if(this.canCreateCell(x, y, z) && this.shouldContainPlayer(new Vector3i(x, y, z), newPosition, playerSimulationRadius)){ Vector3i targetPos = new Vector3i(x,y,z); if(groundDataCells.get(this.getServerDataCellKey(targetPos)) != null){ loadedCellsLock.lock(); groundDataCells.get(this.getServerDataCellKey(targetPos)).addPlayer(player); loadedCellsLock.unlock(); } else { loadedCellsLock.lock(); //create data cell this.createServerDataCell(targetPos); //add player groundDataCells.get(this.getServerDataCellKey(targetPos)).addPlayer(player); loadedCellsLock.unlock(); } } } } } } /** * Checks if a player should be contained in a cell * @param cellWorldPos The world position of the cell * @param playerPos The world position of the player * @param simRadius The simulation radius of the player * @return true if the player should be contained in the cell, false otherwise */ private boolean shouldContainPlayer(Vector3i cellWorldPos, Vector3i playerPos, int simRadius){ return cellWorldPos.distance(playerPos) < simRadius; } /** * Checks if a cell can be created at the position * @param cellPos The position to check * @return true if a cell can be created at that position, false otherwise */ private boolean canCreateCell(Vector3i cellPos){ return this.canCreateCell(cellPos.x, cellPos.y, cellPos.z); } /** * Checks if a cell can be created at the position * @return true if a cell can be created at that position, false otherwise */ private boolean canCreateCell(int x, int y, int z){ return x >= 0 && x < this.serverWorldData.getWorldSizeDiscrete() && y >= 0 && y < this.serverWorldData.getWorldSizeDiscrete() && z >= 0 && z < this.serverWorldData.getWorldSizeDiscrete() ; } /** * Broadcasts messages to player to destroy all entities in a given cell * @param player The player * @param cell The cell */ private void broadcastDestructionToPlayer(Player player, ServerDataCell cell){ for(Entity entity : cell.getScene().getEntityList()){ player.addMessage(EntityMessage.constructDestroyMessage(entity.getId())); } } /** * Creates physics entities when new data cell being created */ private void createTerrainPhysicsEntities(Vector3i worldPos){ Long key = this.getServerDataCellKey(worldPos); loadedCellsLock.lock(); if(posPhysicsMap.containsKey(key)){ PhysicsDataCell cell = posPhysicsMap.get(key); cell.retireCell(); } loadedCellsLock.unlock(); //get data to generate with Vector3d realPos = new Vector3d( worldPos.x * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET, worldPos.y * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET, worldPos.z * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET ); BlockChunkData blockChunkData = parent.getServerWorldData().getServerBlockManager().getChunk(worldPos.x, worldPos.y, worldPos.z); ServerTerrainChunk terrainChunk = parent.getServerWorldData().getServerTerrainManager().getChunk(worldPos.x, worldPos.y, worldPos.z); //create entities Entity blockEntity = EntityCreationUtils.createServerEntity(parent, realPos); Entity terrainEntity = EntityCreationUtils.createServerEntity(parent, realPos); //position entity //this needs to be called at the end of this function. //Burried underneath this is function call to initialize a server side entity. //The server initialization logic checks what type of entity this is, if this function is called prior to its type being stored //the server will not be able to synchronize it properly. ServerEntityUtils.initiallyPositionEntity(parent,blockEntity,realPos); ServerEntityUtils.initiallyPositionEntity(parent,terrainEntity,realPos); PhysicsDataCell cell = PhysicsDataCell.createPhysicsCell(worldPos, terrainEntity, blockEntity); cell.setTerrainChunk(terrainChunk); cell.setBlockChunk(blockChunkData); cell.generatePhysics(); loadedCellsLock.lock(); posPhysicsMap.put(key, cell); loadedCellsLock.unlock(); } /** * For every player, looks at their entity and determines what data cell they should be considered inside of * @return True if the player changed cell, false otherwise */ public boolean updatePlayerPositions(){ Globals.profiler.beginCpuSample("GriddedDataCellManager.updatePlayerPositions"); boolean playerChangedChunk = false; for(Player player : Globals.playerManager.getPlayers()){ Entity playerEntity = player.getPlayerEntity(); if(playerEntity != null && !parent.getLoadingDataCell().containsPlayer(player)){ Vector3d position = EntityUtils.getPosition(playerEntity); int currentWorldX = parent.getServerWorldData().convertRealToChunkSpace(position.x); int currentWorldY = parent.getServerWorldData().convertRealToChunkSpace(position.y); int currentWorldZ = parent.getServerWorldData().convertRealToChunkSpace(position.z); Vector3i newPosition = new Vector3i(currentWorldX,currentWorldY,currentWorldZ); player.setWorldPos(newPosition); int playerSimulationRadius = player.getSimulationRadius(); //remove from cells that are out of range loadedCellsLock.lock(); Globals.profiler.beginCpuSample("GriddedDataCellManager.updatePlayerPositions - Remove from old cells"); for(ServerDataCell cell : this.groundDataCells.values()){ Vector3i cellWorldPos = this.getCellWorldPosition(cell); if(cell.containsPlayer(player) && !this.shouldContainPlayer(cellWorldPos, newPosition, playerSimulationRadius)){ if(cell.getScene().containsEntity(player.getPlayerEntity())){ throw new Error("Trying to remove player from a cell that contains its entity!"); } cell.removePlayer(player); this.broadcastDestructionToPlayer(player, cell); } } Globals.profiler.endCpuSample(); loadedCellsLock.unlock(); //Add to cells that are in range Globals.profiler.beginCpuSample("GriddedDataCellManager.updatePlayerPositions - Create new cells"); for(int x = newPosition.x - playerSimulationRadius + 1; x < newPosition.x + playerSimulationRadius; x++){ for(int y = newPosition.y - playerSimulationRadius + 1; y < newPosition.y + playerSimulationRadius; y++){ for(int z = newPosition.z - playerSimulationRadius + 1; z < newPosition.z + playerSimulationRadius; z++){ if(this.canCreateCell(x,y,z) && this.shouldContainPlayer(new Vector3i(x,y,z), newPosition, playerSimulationRadius)){ Vector3i targetPos = new Vector3i(x,y,z); if(groundDataCells.get(this.getServerDataCellKey(targetPos)) != null){ loadedCellsLock.lock(); ServerDataCell cell = groundDataCells.get(this.getServerDataCellKey(targetPos)); if(!cell.containsPlayer(player)){ cell.addPlayer(player); } loadedCellsLock.unlock(); } else { loadedCellsLock.lock(); //create data cell this.createServerDataCell(targetPos); //add player groundDataCells.get(this.getServerDataCellKey(targetPos)).addPlayer(player); loadedCellsLock.unlock(); } } } } } Globals.profiler.endCpuSample(); } } Globals.profiler.endCpuSample(); return playerChangedChunk; } /** * Unloads all chunks that haven't had players in them for a set amount of time */ public void unloadPlayerlessChunks(){ Globals.profiler.beginCpuSample("GriddedDataCellManager.unloadPlayerlessChunks"); if(this.unloadCells){ //TODO: improve to make have less performance impact loadedCellsLock.lock(); Globals.profiler.beginCpuSample("GriddedDataCellManager.unloadPlayerlessChunks - Increment cleaning time"); for(ServerDataCell cell : this.groundDataCells.values()){ if(cellPlayerlessFrameMap.containsKey(cell)){ if(cell.isReady() && cell.getPlayers().size() < 1){ int frameCount = cellPlayerlessFrameMap.get(cell) + 1; cellPlayerlessFrameMap.put(cell,frameCount); if(frameCount > UNLOAD_FRAME_THRESHOLD){ toCleanQueue.add(cell); } } else { if(cellPlayerlessFrameMap.get(cell) > 0){ cellPlayerlessFrameMap.put(cell, 0); } } } } Globals.profiler.endCpuSample(); Globals.profiler.beginCpuSample("GriddedDataCellManager.unloadPlayerlessChunks - Deconstruct timed out cells"); this.numCleaned = toCleanQueue.size(); for(ServerDataCell cell : toCleanQueue){ //error check before actually queueing for deletion boolean containsPlayerEntity = false; for(Entity entity : cell.getScene().getEntityList()){ if(ServerPlayerViewDirTree.hasTree(entity)){ containsPlayerEntity = true; break; // int playerId = CreatureUtils.getControllerPlayerId(entity); // Player player = Globals.playerManager.getPlayerFromId(playerId); // throw new Error( // "Trying to unload a player's entity! " + // "entity: " + entity + "\n" + // "entity pos (real): " + EntityUtils.getPosition(entity) + "\n" + // "entity pos (world): " + serverWorldData.convertRealToWorldSpace(EntityUtils.getPosition(entity)) + "\n" + // "chunk pos (world): " + worldPos + "\n" + // "player pos (world): " + player.getWorldPos() + "\n" + // "Number of players in cell: " + cell.getPlayers().size() // ); } // ServerEntityUtils.destroyEntity(entity); } if(containsPlayerEntity){ continue; } Vector3i worldPos = this.getCellWorldPosition(cell); Long key = this.getServerDataCellKey(worldPos); //entities are serialized before tracking is removed. This makes sure that any side effects from calling destroyEntity (ie if it looks up the chunk that we're deleting) //don't trigger the chunk to be re-created Globals.profiler.beginCpuSample("GriddedDataCellManager.unloadPlayerlessChunks - Serialize entities"); ContentSerialization serializedEntities = ContentSerialization.constructContentSerialization(cell.getScene().getEntityList()); Globals.profiler.endCpuSample(); Globals.profiler.beginCpuSample("GriddedDataCellManager.unloadPlayerlessChunks - Destroy entities"); for(Entity entity : cell.getScene().getEntityList()){ ServerEntityUtils.destroyEntity(entity); } Globals.profiler.endCpuSample(); //save terrain to disk //terrain is saved before tracking is removed. This makes sure that any side effects from calling savePositionToDisk (ie if it looks up the chunk that we're deleting) //don't trigger the chunk to be re-created Globals.profiler.beginCpuSample("GriddedDataCellManager.unloadPlayerlessChunks - Store data"); GriddedDataCellLoaderService.queueLocationBasedOperation(key, () -> { serverContentManager.saveSerializationToDisk(key, serializedEntities); serverTerrainManager.savePositionToDisk(worldPos); }); Globals.profiler.endCpuSample(); //deregister from all tracking structures parent.deregisterCell(cell); groundDataCells.remove(key); this.posPhysicsMap.remove(key); this.cellPositionMap.remove(cell); this.cellPlayerlessFrameMap.remove(cell); } Globals.profiler.endCpuSample(); loadedCellsLock.unlock(); toCleanQueue.clear(); } Globals.profiler.endCpuSample(); } /** * Evicts all loaded chunks. */ public void evictAll(){ //TODO: improve to make have less performance impact loadedCellsLock.lock(); for(ServerDataCell cell : this.groundDataCells.values()){ int frameCount = cellPlayerlessFrameMap.get(cell) + 1; cellPlayerlessFrameMap.put(cell,frameCount); toCleanQueue.add(cell); } for(ServerDataCell cell : toCleanQueue){ parent.deregisterCell(cell); Vector3i worldPos = getCellWorldPosition(cell); Long key = getServerDataCellKey(worldPos); groundDataCells.remove(key); this.posPhysicsMap.remove(key); this.cellPositionMap.remove(cell); this.cellPlayerlessFrameMap.remove(cell); //offload all entities in cell to chunk file serverContentManager.saveContentToDisk(key, cell.getScene().getEntityList()); //clear all entities in cell for(Entity entity : cell.getScene().getEntityList()){ ServerEntityUtils.destroyEntity(entity); } } loadedCellsLock.unlock(); this.serverTerrainManager.evictAll(); toCleanQueue.clear(); } /** * Get data cell at a given real point in this realm * @param point The real point * @return Either the data cell if found, or null if not found */ public ServerDataCell getDataCellAtPoint(Vector3d point){ ServerDataCell rVal = null; int worldX = parent.getServerWorldData().convertRealToChunkSpace(point.x); int worldY = parent.getServerWorldData().convertRealToChunkSpace(point.y); int worldZ = parent.getServerWorldData().convertRealToChunkSpace(point.z); Vector3i worldPos = new Vector3i(worldX,worldY,worldZ); if( //in bounds of array worldX >= 0 && worldX < this.serverWorldData.getWorldSizeDiscrete() && worldY >= 0 && worldY < this.serverWorldData.getWorldSizeDiscrete() && worldZ >= 0 && worldZ < this.serverWorldData.getWorldSizeDiscrete() && //isn't null groundDataCells.get(getServerDataCellKey(worldPos)) != null ){ LoggerInterface.loggerEngine.DEBUG("Get server data cell key: " + this.getServerDataCellKey(worldPos)); rVal = groundDataCells.get(getServerDataCellKey(worldPos)); } else { LoggerInterface.loggerEngine.DEBUG("Failed to get server data cell at: " + worldPos); } return rVal; } /** * Tries to create a data cell at a given real point * @param point The real point * @return The data cell if created, null otherwise */ public ServerDataCell tryCreateCellAtPoint(Vector3d point){ int worldX = parent.getServerWorldData().convertRealToChunkSpace(point.x); int worldY = parent.getServerWorldData().convertRealToChunkSpace(point.y); int worldZ = parent.getServerWorldData().convertRealToChunkSpace(point.z); Vector3i worldPos = new Vector3i(worldX,worldY,worldZ); return tryCreateCellAtPoint(worldPos); } /** * Tries to create a data cell at a given discrete point * @param point The discrete point * @return The data cell if created, null otherwise */ public ServerDataCell tryCreateCellAtPoint(Vector3i worldPos){ if(this.canCreateCell(worldPos) && groundDataCells.get(this.getServerDataCellKey(worldPos)) == null){ loadedCellsLock.lock(); //create data cell this.createServerDataCell(worldPos); loadedCellsLock.unlock(); } else if(groundDataCells.get(this.getServerDataCellKey(worldPos)) == null) { LoggerInterface.loggerEngine.ERROR( new Error( "Trying to create data cell outside world bounds!\n" + worldPos + "\n" + this.serverWorldData.getWorldSizeDiscrete() ) ); } return groundDataCells.get(this.getServerDataCellKey(worldPos)); } /** * Gets a data cell at a given world position * @param position The world position * @return The data cell if found, null otherwise */ public ServerDataCell getCellAtWorldPosition(Vector3i position){ if(this.canCreateCell(position) && groundDataCells.get(this.getServerDataCellKey(position)) != null){ return groundDataCells.get(this.getServerDataCellKey(position)); } return null; } /** * Calls the simulate function on all loaded cells */ public void simulate(){ Globals.profiler.beginCpuSample("GriddedDataCellManager.simulate"); loadedCellsLock.lock(); boolean runMicroSim = Globals.microSimulation != null && Globals.microSimulation.isReady(); for(ServerDataCell cell : this.groundDataCells.values()){ if(runMicroSim && this.shouldSimulate(cell)){ Globals.microSimulation.simulate(cell); } //queue fluid simulation if(Globals.RUN_FLUIDS){ Vector3i cellPos = this.getCellWorldPosition(cell); if(cellPos != null){ this.serverFluidManager.queue(cellPos.x, cellPos.y, cellPos.z); } } } //simulate fluids if(Globals.RUN_FLUIDS){ this.serverFluidManager.simulate((ServerFluidChunk fluidChunk) -> { ServerDataCell cell = getCellAtWorldPosition(fluidChunk.getWorldPosition()); ServerFluidChunk chunk = getFluidChunkAtPosition(fluidChunk.getWorldPosition()); cell.broadcastNetworkMessage( TerrainMessage.constructupdateFluidDataMessage(fluidChunk.getWorldX(), fluidChunk.getWorldY(), fluidChunk.getWorldZ(), TerrainProtocol.constructFluidByteBuffer(chunk).array()) ); }); } loadedCellsLock.unlock(); this.unloadPlayerlessChunks(); this.updatePlayerPositions(); Globals.profiler.endCpuSample(); } /** * Checks if a server data cell should be simulated or not * @param cell The cell * @return true if it should be simulated, false otherwise */ private boolean shouldSimulate(ServerDataCell cell){ return cell.getPlayers().size() > 0; } /** * Gets the server terrain manager for this realm if it exists * @return The server terrain manager if it exists, null otherwise */ public ServerTerrainManager getServerTerrainManager(){ return serverTerrainManager; } /** * Gets the server fluid manager for this realm if it exists * @return The server fluid manager if it exists, null otherwise */ public ServerFluidManager getServerFluidManager(){ return serverFluidManager; } /** * Runs code to generate physics entities and register cell in a dedicated thread. * Because cell hasn't been registered yet, no simulation is performed until the physics is created. * @param worldPos */ private static void runPhysicsGenerationThread( Vector3i worldPos, Long key, PhysicsDataCell cell, Map posPhysicsMap, Map groundDataCells, Realm realm ){ //get data to generate with Vector3d realPos = new Vector3d( worldPos.x * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET, worldPos.y * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET, worldPos.z * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET ); ServerDataCell dataCell = groundDataCells.get(key); //create entities Entity blockEntity = EntityCreationUtils.createServerEntity(realm, realPos); Entity terrainEntity = EntityCreationUtils.createServerEntity(realm, realPos); //position entity //this needs to be called at the end of this function. //Burried underneath this is function call to initialize a server side entity. //The server initialization logic checks what type of entity this is, if this function is called prior to its type being stored //the server will not be able to synchronize it properly. ServerEntityUtils.initiallyPositionEntity(realm,blockEntity,realPos); ServerEntityUtils.initiallyPositionEntity(realm,terrainEntity,realPos); PhysicsDataCell targetCell = PhysicsDataCell.createPhysicsCell(worldPos, terrainEntity, blockEntity); if(cell == null){ posPhysicsMap.put(key, targetCell); } else { ServerEntityUtils.destroyEntity(terrainEntity); ServerEntityUtils.destroyEntity(blockEntity); } generationService.submit(() -> { BlockChunkData blockChunkData = realm.getServerWorldData().getServerBlockManager().getChunk(worldPos.x, worldPos.y, worldPos.z); ServerTerrainChunk terrainChunk = realm.getServerWorldData().getServerTerrainManager().getChunk(worldPos.x, worldPos.y, worldPos.z); targetCell.setTerrainChunk(terrainChunk); targetCell.setBlockChunk(blockChunkData); //create physics entities if(cell != null){ cell.retireCell(); cell.generatePhysics(); } else { targetCell.generatePhysics(); } //set ready dataCell.setReady(true); }); } /** * Gets the key in the groundDataCells map for the data cell at the provided world pos * @param worldPos The position in world coordinates of the server data cell * @return The server data cell if it exists, otherwise null */ private Long getServerDataCellKey(Vector3i worldPos){ return (long)HashUtils.hashIVec(worldPos.x, worldPos.y, worldPos.z); } /** * Registers a server data cell with the internal datastructure for tracking them * @param key The key to register the cell at * @param cell The cell itself */ private ServerDataCell createServerDataCell(Vector3i worldPos){ Globals.profiler.beginCpuSample("GriddedDataCellManager.createServerDataCell"); ServerDataCell rVal = parent.createNewCell(); Long cellKey = this.getServerDataCellKey(worldPos); loadedCellsLock.lock(); groundDataCells.put(cellKey,rVal); cellPlayerlessFrameMap.put(rVal,0); LoggerInterface.loggerEngine.DEBUG("Create server data cell with key " + cellKey); cellPositionMap.put(rVal,new Vector3i(worldPos)); loadedCellsLock.unlock(); Long key = this.getServerDataCellKey(worldPos); //generate content GriddedDataCellLoaderService.queueLocationBasedOperation(key, () -> { serverContentManager.generateContentForDataCell(parent, worldPos, rVal, cellKey); }); //generates physics for the cell in a dedicated thread then finally registers loadedCellsLock.lock(); PhysicsDataCell cell = posPhysicsMap.get(key); GriddedDataCellManager.runPhysicsGenerationThread(worldPos,key,cell,this.posPhysicsMap,this.groundDataCells,this.parent); loadedCellsLock.unlock(); Globals.profiler.endCpuSample(); return rVal; } @Override /** * Gets the weight of a single voxel at a position * @param worldPosition The position in world coordinates of the chunk to grab data from * @param voxelPosition The position in voxel coordinates (local/relative to the chunk) to get voxel values from * @return The weight of the described voxel */ public float getVoxelWeightAtLocalPosition(Vector3i worldPosition, Vector3i voxelPosition) { return serverTerrainManager.getChunk(worldPosition.x, worldPosition.y, worldPosition.z).getWeights()[voxelPosition.x][voxelPosition.y][voxelPosition.z]; } @Override /** * Gets the type of a single voxel at a position * @param worldPosition The position in world coordinates of the chunk to grab data from * @param voxelPosition The position in voxel coordinates (local/relative to the chunk) to get voxel values from * @return The type of the described voxel */ public int getVoxelTypeAtLocalPosition(Vector3i worldPosition, Vector3i voxelPosition) { return serverTerrainManager.getChunk(worldPosition.x, worldPosition.y, worldPosition.z).getValues()[voxelPosition.x][voxelPosition.y][voxelPosition.z]; } @Override /** * Gets the chunk data at a given world position * @param worldPosition The position in world coordinates * @return The ServerTerrainChunk of data at that position, or null if it is out of bounds or otherwise doesn't exist */ public ServerTerrainChunk getChunkAtPosition(Vector3i worldPosition) { return serverTerrainManager.getChunk(worldPosition.x, worldPosition.y, worldPosition.z); } @Override /** * Edits a single voxel * @param worldPosition The world position of the chunk to edit * @param voxelPosition The voxel position of the voxel to edit * @param weight The weight to set the voxel to * @param type The type to set the voxel to */ public void editChunk(Vector3i worldPosition, Vector3i voxelPosition, float weight, int type) { terrainEditLock.acquireUninterruptibly(); List worldPositionsToUpdate = new LinkedList(); worldPositionsToUpdate.add(worldPosition); if(voxelPosition.x < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(1,0,0)); if(voxelPosition.y < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(1,1,0)); if(voxelPosition.z < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(1,1,1)); } } if(voxelPosition.z < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(1,0,1)); } } if(voxelPosition.y < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(0,1,0)); if(voxelPosition.z < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(0,1,1)); } } if(voxelPosition.z < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(0,0,1)); } //update all loaded cells for(Vector3i toUpdate : worldPositionsToUpdate){ if( toUpdate.x >= 0 && toUpdate.x < this.serverWorldData.getWorldSizeDiscrete() && toUpdate.y >= 0 && toUpdate.y < this.serverWorldData.getWorldSizeDiscrete() && toUpdate.z >= 0 && toUpdate.z < this.serverWorldData.getWorldSizeDiscrete() ){ //update terrain int localVoxelX = voxelPosition.x + (ServerTerrainChunk.CHUNK_DATA_GENERATOR_SIZE - 1) * (worldPosition.x - toUpdate.x); int localVoxelY = voxelPosition.y + (ServerTerrainChunk.CHUNK_DATA_GENERATOR_SIZE - 1) * (worldPosition.y - toUpdate.y); int localVoxelZ = voxelPosition.z + (ServerTerrainChunk.CHUNK_DATA_GENERATOR_SIZE - 1) * (worldPosition.z - toUpdate.z); serverTerrainManager.deformTerrainAtLocationToValue(toUpdate, new Vector3i(localVoxelX, localVoxelY, localVoxelZ), weight, type); //update anything loaded this.loadedCellsLock.lock(); ServerDataCell cell = groundDataCells.get(this.getServerDataCellKey(toUpdate)); if(cell != null){ //update physics this.createTerrainPhysicsEntities(toUpdate); //broadcast update cell.broadcastNetworkMessage(TerrainMessage.constructUpdateVoxelMessage( toUpdate.x, toUpdate.y, toUpdate.z, localVoxelX, localVoxelY, localVoxelZ, weight, type)); } this.loadedCellsLock.unlock(); } } terrainEditLock.release(); } /** * Gets the world position of a given data cell * @param cell The data cell * @return The world position */ public Vector3i getCellWorldPosition(ServerDataCell cell){ return cellPositionMap.get(cell); } @Override /** * Gets the fluid chunk at a given position */ public ServerFluidChunk getFluidChunkAtPosition(Vector3i worldPosition) { return serverFluidManager.getChunk(worldPosition.x, worldPosition.y, worldPosition.z); } /** * Loads all cells */ public void loadAllCells(){ this.unloadCells = false; for(int x = 0; x < this.serverWorldData.getWorldSizeDiscrete(); x++){ for(int y = 0; y < this.serverWorldData.getWorldSizeDiscrete(); y++){ for(int z = 0; z < this.serverWorldData.getWorldSizeDiscrete(); z++){ this.tryCreateCellAtPoint(new Vector3i(x,y,z)); } } } } @Override public void save(String saveName) { for(ServerDataCell cell : this.groundDataCells.values()){ Long key = this.getServerDataCellKey(this.getCellWorldPosition(cell)); //offload all entities in cell to chunk file serverContentManager.saveContentToDisk(key, cell.getScene().getEntityList()); } } @Override public Vector3d guaranteePositionIsInBounds(Vector3d positionToTest) { Vector3d returnPos = new Vector3d(positionToTest); if(positionToTest.x < 0){ returnPos.x = 0; } if(positionToTest.x >= parent.getServerWorldData().convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete())){ returnPos.x = parent.getServerWorldData().convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete()) - 1; } if(positionToTest.y < 0){ returnPos.y = 0; } if(positionToTest.y >= parent.getServerWorldData().convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete())){ returnPos.y = parent.getServerWorldData().convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete()) - 1; } if(positionToTest.z < 0){ returnPos.z = 0; } if(positionToTest.z >= parent.getServerWorldData().convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete())){ returnPos.z = parent.getServerWorldData().convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete()) - 1; } return returnPos; } /** * Stops the executor service */ public void halt(){ generationService.shutdownNow(); GriddedDataCellLoaderService.ioThreadService.shutdownNow(); } @Override /** * Gets the block chunk data at a given world position */ public BlockChunkData getBlocksAtPosition(Vector3i worldPosition) { return this.serverWorldData.getServerBlockManager().getChunk(worldPosition.x, worldPosition.y, worldPosition.z); } @Override /** * Edits a block chunk in the world */ public void editBlock(Vector3i worldPosition, Vector3i voxelPosition, short type, short metadata) { terrainEditLock.acquireUninterruptibly(); if( worldPosition.x >= 0 && worldPosition.x < this.serverWorldData.getWorldSizeDiscrete() && worldPosition.y >= 0 && worldPosition.y < this.serverWorldData.getWorldSizeDiscrete() && worldPosition.z >= 0 && worldPosition.z < this.serverWorldData.getWorldSizeDiscrete() ){ ServerBlockManager serverBlockManager = this.serverWorldData.getServerBlockManager(); //update terrain int localVoxelX = voxelPosition.x; int localVoxelY = voxelPosition.y; int localVoxelZ = voxelPosition.z; serverBlockManager.editBlockAtLocationToValue(worldPosition, voxelPosition, type, metadata); //update anything loaded this.loadedCellsLock.lock(); ServerDataCell cell = groundDataCells.get(this.getServerDataCellKey(worldPosition)); if(cell != null){ //update physics this.createTerrainPhysicsEntities(worldPosition); //broadcast update cell.broadcastNetworkMessage(TerrainMessage.constructUpdateBlockMessage( worldPosition.x, worldPosition.y, worldPosition.z, localVoxelX, localVoxelY, localVoxelZ, type, metadata )); } this.loadedCellsLock.unlock(); } terrainEditLock.release(); } /** * Gets the set of loaded cells * @return The set of loaded cells */ public Collection getLoadedCells(){ return Collections.unmodifiableCollection(this.groundDataCells.values()); } /** * Gets the number of cells cleaned in the most recent frame * @return The number of cells cleaned */ public int getNumCleaned(){ return this.numCleaned; } /** * Gets the playerless cell->frame count map * @return The playerless cell->frame count map */ public Map getCellPlayerlessFrameMap(){ return cellPlayerlessFrameMap; } }