package electrosphere.server.datacell; 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.CopyOnWriteArraySet; import java.util.concurrent.Semaphore; import org.joml.Vector3d; import org.joml.Vector3i; import electrosphere.engine.Globals; import electrosphere.engine.threads.LabeledThread.ThreadLabel; import electrosphere.entity.Entity; import electrosphere.entity.EntityUtils; import electrosphere.game.server.world.ServerWorldData; import electrosphere.logger.LoggerInterface; import electrosphere.net.parser.net.message.TerrainMessage; import electrosphere.net.server.player.Player; import electrosphere.net.server.protocol.TerrainProtocol; import electrosphere.server.content.ServerContentManager; 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.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 = 10; /** * 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 cellPositionMap = new HashMap(); //Map of server data cell to the number of frames said cell has had no players Map cellPlayerlessFrameMap = new HashMap(); //The number of frames without players that must pass before a server data cell is unloaded static final int UNLOAD_FRAME_THRESHOLD = 100; //loaded cells Semaphore loadedCellsLock = new Semaphore(1); Set loadedCells = new CopyOnWriteArraySet(); //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; /** * Map of world position key -> physics cell */ Map posPhysicsMap = new HashMap(); /** * 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( x >= 0 && x < this.serverWorldData.getWorldSizeDiscrete() && y >= 0 && y < this.serverWorldData.getWorldSizeDiscrete() && z >= 0 && z < this.serverWorldData.getWorldSizeDiscrete() ){ Vector3i targetPos = new Vector3i(x,y,z); LoggerInterface.loggerEngine.DEBUG("GriddedDataCellManager: Add player to " + x + " " + y + " " + z); 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 createServerDataCell(targetPos); ///generates physics for the cell in a dedicated thread then finally registers runPhysicsGenerationThread(targetPos); //add to loaded cells loadedCellsLock.acquireUninterruptibly(); loadedCells.add(groundDataCells.get(getServerDataCellKey(targetPos))); cellPlayerlessFrameMap.put(groundDataCells.get(getServerDataCellKey(targetPos)),0); loadedCellsLock.release(); //generate/handle content for new server data cell //add player groundDataCells.get(getServerDataCellKey(targetPos)).addPlayer(player); } } } } } } /** * 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(); Vector3i oldPosition = player.getWorldPos(); player.setWorldPos(newPosition); // System.out.println("=======" + "SET" + newX + " " + newY + " FROM " + oldX + " " + oldY + "========"); // int removals = 0; // int additions = 0; for(int x = oldPosition.x - playerSimulationRadius; x < oldPosition.x + playerSimulationRadius + 1; x++){ for(int y = oldPosition.y - playerSimulationRadius; y < oldPosition.y + playerSimulationRadius + 1; y++){ for(int z = oldPosition.z - playerSimulationRadius; z < oldPosition.z + playerSimulationRadius + 1; z++){ if( x >= 0 && x < this.serverWorldData.getWorldSizeDiscrete() && y >= 0 && y < this.serverWorldData.getWorldSizeDiscrete() && z >= 0 && z < this.serverWorldData.getWorldSizeDiscrete() && ( x < newPosition.x - playerSimulationRadius || x > newPosition.x + playerSimulationRadius || y < newPosition.y - playerSimulationRadius || y > newPosition.y + playerSimulationRadius || z < newPosition.z - playerSimulationRadius || z > newPosition.z + playerSimulationRadius ) ){ Vector3i targetPos = new Vector3i(x,y,z); if(groundDataCells.get(getServerDataCellKey(targetPos)) != null){ if(groundDataCells.get(getServerDataCellKey(targetPos)).containsPlayer(player)){ // removals++; groundDataCells.get(getServerDataCellKey(targetPos)).removePlayer(player); } } } } } } 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( x >= 0 && x < this.serverWorldData.getWorldSizeDiscrete() && y >= 0 && y < this.serverWorldData.getWorldSizeDiscrete() && z >= 0 && z < this.serverWorldData.getWorldSizeDiscrete() && ( x < oldPosition.x - playerSimulationRadius || x > oldPosition.x + playerSimulationRadius || y < oldPosition.y - playerSimulationRadius || y > oldPosition.y + playerSimulationRadius || z < oldPosition.z - playerSimulationRadius || z > oldPosition.z + playerSimulationRadius ) ){ Vector3i targetPos = new Vector3i(x,y,z); // System.out.println("Add player to " + x + " " + y); if(groundDataCells.get(getServerDataCellKey(targetPos)) != null){ groundDataCells.get(getServerDataCellKey(targetPos)).addPlayer(player); } else { //create data cell createServerDataCell(targetPos); //generates physics for the cell in a dedicated thread then finally registers runPhysicsGenerationThread(targetPos); //add to loaded cells loadedCellsLock.acquireUninterruptibly(); loadedCells.add(groundDataCells.get(getServerDataCellKey(targetPos))); cellPlayerlessFrameMap.put(groundDataCells.get(getServerDataCellKey(targetPos)),0); loadedCellsLock.release(); //add player groundDataCells.get(getServerDataCellKey(targetPos)).addPlayer(player); } // additions++; } else { // System.out.println(x + "\t" + (oldX - playerSimulationRadius) + "\t" + (oldX + playerSimulationRadius)); // System.out.println(y + "\t" + (oldY - playerSimulationRadius) + "\t" + (oldY + playerSimulationRadius)); } } } } // System.out.println("removals: " + removals + "\tadditions: " + additions); } /** * Creates physics entities when new data cell being created */ private void createTerrainPhysicsEntities(Vector3i worldPos){ String key = this.getServerDataCellKey(worldPos); if(posPhysicsMap.containsKey(key)){ PhysicsDataCell cell = posPhysicsMap.get(key); cell.retireCell(); cell.generatePhysics(); } else { PhysicsDataCell cell = PhysicsDataCell.createPhysicsCell(parent, worldPos); cell.generatePhysics(); posPhysicsMap.put(key, cell); } } /** * 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(){ 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); if(currentWorldX != player.getWorldPos().x || currentWorldY != player.getWorldPos().y || currentWorldZ != player.getWorldPos().z){ movePlayer(player,new Vector3i(currentWorldX,currentWorldY,currentWorldZ)); playerChangedChunk = true; } } } return playerChangedChunk; } //Used for cleaning server data cells no longer in use from the realm Set toCleanQueue = new HashSet(); /** * Unloads all chunks that haven't had players in them for a set amount of time */ public void unloadPlayerlessChunks(){ if(this.unloadCells){ //TODO: improve to make have less performance impact for(ServerDataCell cell : loadedCells){ loadedCellsLock.acquireUninterruptibly(); if(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); } } loadedCellsLock.release(); } for(ServerDataCell cell : toCleanQueue){ parent.deregisterCell(cell); loadedCells.remove(cell); Vector3i worldPos = getCellWorldPosition(cell); String key = getServerDataCellKey(worldPos); groundDataCells.remove(key); //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()){ EntityUtils.cleanUpEntity(entity); } //save terrain to disk serverTerrainManager.savePositionToDisk(worldPos); } 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: " + 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( //in bounds of array worldPos.x >= 0 && worldPos.x < this.serverWorldData.getWorldSizeDiscrete() && worldPos.y >= 0 && worldPos.y < this.serverWorldData.getWorldSizeDiscrete() && worldPos.z >= 0 && worldPos.z < this.serverWorldData.getWorldSizeDiscrete() && //isn't null groundDataCells.get(getServerDataCellKey(worldPos)) == null ){ //create data cell createServerDataCell(worldPos); //generates physics for the cell in a dedicated thread then finally registers runPhysicsGenerationThread(worldPos); //add to loaded cells loadedCellsLock.acquireUninterruptibly(); loadedCells.add(groundDataCells.get(getServerDataCellKey(worldPos))); cellPlayerlessFrameMap.put(groundDataCells.get(getServerDataCellKey(worldPos)),0); loadedCellsLock.release(); } else { LoggerInterface.loggerEngine.WARNING("Trying to create data cell outside world bounds! " + worldPos); } return groundDataCells.get(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( //in bounds of array position.x >= 0 && position.x < this.serverWorldData.getWorldSizeDiscrete() && position.y >= 0 && position.y < this.serverWorldData.getWorldSizeDiscrete() && position.z >= 0 && position.z < this.serverWorldData.getWorldSizeDiscrete() && //isn't null groundDataCells.get(getServerDataCellKey(position)) != null ){ return groundDataCells.get(getServerDataCellKey(position)); } return null; } /** * Calls the simulate function on all loaded cells */ public void simulate(){ loadedCellsLock.acquireUninterruptibly(); for(ServerDataCell cell : loadedCells){ if(Globals.microSimulation != null && Globals.microSimulation.isReady()){ Globals.microSimulation.simulate(cell); } //simulate fluid Vector3i cellPos = this.getCellWorldPosition(cell); boolean update = this.serverFluidManager.simulate(cellPos.x,cellPos.y,cellPos.z); if(update){ rebroadcastFluidChunk(cellPos); } } loadedCellsLock.release(); updatePlayerPositions(); } /** * 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 void runPhysicsGenerationThread(Vector3i worldPos){ Thread thread = new Thread(new Runnable(){ @Override public void run() { //create physics entities createTerrainPhysicsEntities(worldPos); //set ready groundDataCells.get(getServerDataCellKey(worldPos)).setReady(true); } }); groundDataCells.get(getServerDataCellKey(worldPos)).setReady(false); Globals.threadManager.start(ThreadLabel.ASSET_LOADING, thread); } /** * 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 String getServerDataCellKey(Vector3i worldPos){ return 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){ ServerDataCell rVal = parent.createNewCell(); String cellKey = getServerDataCellKey(worldPos); groundDataCells.put(cellKey,rVal); LoggerInterface.loggerEngine.DEBUG("Create server data cell with key " + cellKey); cellPositionMap.put(rVal,new Vector3i(worldPos)); serverContentManager.generateContentForDataCell(parent, worldPos, rVal, cellKey); 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(); //update terrain serverTerrainManager.deformTerrainAtLocationToValue(worldPosition, voxelPosition, weight, type); 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)); } } else { if(voxelPosition.z < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(1,0,1)); } } } else { 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)); } } else { if(voxelPosition.z < 1){ worldPositionsToUpdate.add(new Vector3i(worldPosition).sub(0,0,1)); } } } //update all loaded cells for(Vector3i toUpdate : worldPositionsToUpdate){ ServerDataCell cell = groundDataCells.get(getServerDataCellKey(toUpdate)); if(cell != null){ this.createTerrainPhysicsEntities(toUpdate); cell.broadcastNetworkMessage(TerrainMessage.constructUpdateVoxelMessage( worldPosition.x, worldPosition.y, worldPosition.z, voxelPosition.x, voxelPosition.y, voxelPosition.z, weight, type)); } } 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)); } } } } /** * Rebroadcasts the fluid cell at a given position * @param worldPosition the world position */ private void rebroadcastFluidChunk(Vector3i worldPosition){ ServerDataCell cell = getCellAtWorldPosition(worldPosition); ServerFluidChunk chunk = getFluidChunkAtPosition(worldPosition); cell.broadcastNetworkMessage( TerrainMessage.constructupdateFluidDataMessage(worldPosition.x, worldPosition.y, worldPosition.z, TerrainProtocol.constructFluidByteBuffer(chunk).array()) ); } @Override public void save(String saveName) { for(ServerDataCell cell : loadedCells){ String 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; } }