package electrosphere.server.datacell; import java.util.HashMap; import java.util.HashSet; 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.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.saves.SaveUtils; 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 { //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; int discreteWorldSize; //parent realm Realm parent; //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; /** * Constructor * @param parent The gridded data cell manager's parent realm */ public GriddedDataCellManager( Realm parent, ServerTerrainManager serverTerrainManager, ServerFluidManager serverFluidManager, ServerContentManager serverContentManager ) { this.parent = parent; this.serverTerrainManager = serverTerrainManager; this.serverFluidManager = serverFluidManager; this.serverContentManager = serverContentManager; } /** * Initializes the gridded data cell manager * @param data The server world data to back the manager with */ public void init(ServerWorldData data){ discreteWorldSize = data.getWorldSizeDiscrete(); loadedCells = new CopyOnWriteArraySet(); } /** * 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 < discreteWorldSize && y >= 0 && y < discreteWorldSize && z >= 0 && z < discreteWorldSize ){ 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 < discreteWorldSize && y >= 0 && y < discreteWorldSize && z >= 0 && z < discreteWorldSize && ( 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 < discreteWorldSize && y >= 0 && y < discreteWorldSize && z >= 0 && z < discreteWorldSize && ( 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){ PhysicsDataCell cell = PhysicsDataCell.createPhysicsCell(parent, worldPos); cell.generatePhysics(); } /** * 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 = Globals.serverWorldData.convertRealToChunkSpace(position.x); int currentWorldY = Globals.serverWorldData.convertRealToChunkSpace(position.y); int currentWorldZ = Globals.serverWorldData.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(){ //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 = Globals.serverWorldData.convertRealToChunkSpace(point.x); int worldY = Globals.serverWorldData.convertRealToChunkSpace(point.y); int worldZ = Globals.serverWorldData.convertRealToChunkSpace(point.z); Vector3i worldPos = new Vector3i(worldX,worldY,worldZ); if( //in bounds of array worldX >= 0 && worldX < discreteWorldSize && worldY >= 0 && worldY < discreteWorldSize && worldZ >= 0 && worldZ < discreteWorldSize && //isn't null groundDataCells.get(getServerDataCellKey(worldPos)) != null ){ LoggerInterface.loggerEngine.DEBUG("Get server data cell key: " + getServerDataCellKey(worldPos)); rVal = groundDataCells.get(getServerDataCellKey(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 = Globals.serverWorldData.convertRealToChunkSpace(point.x); int worldY = Globals.serverWorldData.convertRealToChunkSpace(point.y); int worldZ = Globals.serverWorldData.convertRealToChunkSpace(point.z); Vector3i worldPos = new Vector3i(worldX,worldY,worldZ); if( //in bounds of array worldX >= 0 && worldX < discreteWorldSize && worldY >= 0 && worldY < discreteWorldSize && worldZ >= 0 && worldZ < discreteWorldSize && //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(); } 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 < discreteWorldSize && position.y >= 0 && position.y < discreteWorldSize && position.z >= 0 && position.z < discreteWorldSize && //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){ Globals.microSimulation.simulate(cell, parent.getHitboxManager()); //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); thread.start(); } /** * 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); //broadcast update to terrain ServerDataCell cell = groundDataCells.get(getServerDataCellKey(worldPosition)); if(cell != null){ 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); } /** * 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 >= Globals.serverWorldData.convertChunkToRealSpace(Globals.serverWorldData.getWorldSizeDiscrete())){ returnPos.x = Globals.serverWorldData.convertChunkToRealSpace(Globals.serverWorldData.getWorldSizeDiscrete()) - 1; } if(positionToTest.y < 0){ returnPos.y = 0; } if(positionToTest.y >= Globals.serverWorldData.convertChunkToRealSpace(Globals.serverWorldData.getWorldSizeDiscrete())){ returnPos.y = Globals.serverWorldData.convertChunkToRealSpace(Globals.serverWorldData.getWorldSizeDiscrete()) - 1; } if(positionToTest.z < 0){ returnPos.z = 0; } if(positionToTest.z >= Globals.serverWorldData.convertChunkToRealSpace(Globals.serverWorldData.getWorldSizeDiscrete())){ returnPos.z = Globals.serverWorldData.convertChunkToRealSpace(Globals.serverWorldData.getWorldSizeDiscrete()) - 1; } return returnPos; } }