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.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.joml.Vector3d; import org.joml.Vector3i; import electrosphere.client.block.BlockChunkData; import electrosphere.engine.EngineState; import electrosphere.engine.Globals; import electrosphere.engine.threads.ThreadCounts; import electrosphere.entity.Entity; import electrosphere.entity.EntityCreationUtils; import electrosphere.entity.EntityTags; import electrosphere.entity.EntityUtils; import electrosphere.entity.ServerEntityUtils; import electrosphere.entity.state.server.ServerPlayerViewDirTree; 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.datacell.Realm; import electrosphere.server.datacell.ServerDataCell; import electrosphere.server.datacell.ServerWorldData; import electrosphere.server.datacell.interfaces.DataCellManager; import electrosphere.server.datacell.interfaces.PathfindingManager; import electrosphere.server.datacell.interfaces.VoxelCellManager; import electrosphere.server.datacell.physics.PhysicsDataCell; import electrosphere.server.entity.ServerContentManager; import electrosphere.server.entity.serialization.ContentSerialization; import electrosphere.server.macro.MacroDataUpdater; import electrosphere.server.macro.spatial.MacroObject; import electrosphere.server.pathfinding.recast.PathingProgressiveData; import electrosphere.server.pathfinding.voxel.VoxelPathfinder; import electrosphere.server.physics.block.manager.ServerBlockManager; import electrosphere.server.physics.fluid.manager.ServerFluidChunk; import electrosphere.server.physics.fluid.manager.ServerFluidManager; import electrosphere.server.physics.terrain.manager.ServerChunkCache; import electrosphere.server.physics.terrain.manager.ServerTerrainChunk; import electrosphere.server.physics.terrain.manager.ServerTerrainManager; import electrosphere.server.physics.terrain.models.TerrainModel; import electrosphere.util.math.HashUtils; /** * 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, PathfindingManager { /** * 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 */ private static final int UNLOAD_FRAME_THRESHOLD = 100; /** * The distance at which simulation is queued */ private static final double SIMULATION_DISTANCE_CUTOFF = 5; /** * Big number used when scanning for a data cell to spawn a macro object within */ private static final double MACRO_SCANNING_BIG_NUMBER = 10000; /** * Used for generating physics chunks */ private ExecutorService generationService = null; /** * The service for loading data cells from disk */ private GriddedDataCellLoaderService loaderService; /** * Tracks whether this manager has been flagged to unload cells or not */ private boolean unloadCells = true; /** * These are going to be the natural ground grid of data cells, but we're going to have more than this */ private Map groundDataCells = new HashMap(); /** * Map of server cell to its world position */ private Map cellPositionMap = new HashMap(); /** * Map of server data cell to the number of frames said cell has had no players */ private Map cellPlayerlessFrameMap = new HashMap(); /** * A map of ServerDataCell->GriddedDataCellTrackingData */ private Map cellTrackingMap = new HashMap(); /** * Loaded cells */ private ReentrantLock loadedCellsLock = new ReentrantLock(); /** * Parent realm */ private Realm parent; /** * The world data of the parent */ private ServerWorldData serverWorldData; /** * Manager for terrain for this particular cell manager */ private ServerTerrainManager serverTerrainManager; /** * Manager for fluids for this particular cell manager */ private ServerFluidManager serverFluidManager; /** * Lock for terrain editing */ private Semaphore terrainEditLock = new Semaphore(1); /** * Manager for getting entities to fill in a cell */ private ServerContentManager serverContentManager; /** * Used for cleaning server data cells no longer in use from the realm */ private Set toCleanQueue = new HashSet(); /** * Map of world position key -> physics cell */ private Map posPhysicsMap = new HashMap(); /** * Pooling for physics data cells */ private LinkedList cellPool = new LinkedList(); /** * Number of data cells cleaned up in the most recent frame */ private int numCleaned = 0; /** * Queue of cells that need to have their physics regenerated (ie on block edits) */ private Map physicsQueue = new HashMap(); /** * The pathfinder for the manager */ private VoxelPathfinder pathfinder; /** * Caches lookups for nearby entities between simulate() calls */ private Map> nearbyLookupCache = 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 Error("Tried to create a GriddedDataCellManager with invalid parameters " + this.parent + " " + this.serverWorldData + " " + this.serverTerrainManager + " " + this.serverFluidManager + " " + this.serverContentManager + " " ); } this.pathfinder = new VoxelPathfinder(); this.loaderService = new GriddedDataCellLoaderService(); this.generationService = Globals.engineState.threadManager.requestFixedThreadPool(ThreadCounts.GRIDDED_DATACELL_PHYSICS_GEN_THREADS); } /** * 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.serverState.realmManager.setPlayerRealm(player, parent); int playerSimulationRadius = player.getSimulationRadius(); Vector3i worldPos = player.getWorldPos(); Vector3i tempVec = new Vector3i(); 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++){ tempVec.set(x,y,z); double distance = this.calcDistance(tempVec, worldPos); if(this.canCreateCell(x, y, z) && this.shouldContainPlayer(distance, playerSimulationRadius)){ LoggerInterface.loggerEngine.DEBUG("GriddedDataCellManager: Add player to " + x + " " + y + " " + z); loadedCellsLock.lock(); if(groundDataCells.get(this.getServerDataCellKey(tempVec)) != null){ groundDataCells.get(this.getServerDataCellKey(tempVec)).addPlayer(player); } else { LoggerInterface.loggerEngine.DEBUG("Creating new cell @ " + x + " " + y + " " + z); //create data cell this.createServerDataCell(tempVec); //add player groundDataCells.get(this.getServerDataCellKey(tempVec)).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(this.calcDistance(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(this.calcDistance(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(double distance, int simRadius){ return distance < simRadius; } /** * Calculates the distance from the player to the cell * @param playerPos The player * @param cellWorldPos The cell * @return The distance */ public double calcDistance(Vector3i playerPos, Vector3i cellWorldPos){ return cellWorldPos.distance(playerPos); } /** * 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.destroyEntities(); } 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 ); MacroDataUpdater.update(parent, parent.getMacroData(), realPos); BlockChunkData blockChunkData = parent.getServerWorldData().getServerBlockManager().getChunk(worldPos.x, worldPos.y, worldPos.z); ServerTerrainChunk terrainChunk = parent.getServerWorldData().getServerTerrainManager().getChunk(worldPos.x, worldPos.y, worldPos.z, ServerChunkCache.STRIDE_FULL_RES); //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 = this.getPhysicsDataCell(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 - Reset chunk distances"); loadedCellsLock.lock(); for(ServerDataCell cell : this.groundDataCells.values()){ GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(cell); trackingData.setClosestPlayer(GriddedDataCellTrackingData.REALLY_LARGE_DISTANCE); } loadedCellsLock.unlock(); Globals.profiler.endCpuSample(); Globals.profiler.beginCpuSample("GriddedDataCellManager.updatePlayerPositions - Actually update player positions"); boolean playerChangedChunk = false; for(Player player : Globals.serverState.playerManager.getPlayers()){ Entity playerEntity = player.getPlayerEntity(); if(playerEntity != null && !parent.getLoadingDataCell().containsPlayer(player)){ Vector3d position = EntityUtils.getPosition(playerEntity); int currentWorldX = ServerWorldData.convertRealToChunkSpace(position.x); int currentWorldY = ServerWorldData.convertRealToChunkSpace(position.y); int currentWorldZ = ServerWorldData.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()){ GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(cell); Vector3i cellWorldPos = this.getCellWorldPosition(cell); double distance = this.calcDistance(cellWorldPos, newPosition); if(distance < trackingData.getClosestPlayer()){ trackingData.setClosestPlayer(distance); } if(cell.containsPlayer(player) && !this.shouldContainPlayer(distance, 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"); Vector3i tempVec = new Vector3i(); 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++){ tempVec.set(x,y,z); double distance = this.calcDistance(tempVec, newPosition); if(this.canCreateCell(x,y,z) && this.shouldContainPlayer(distance, playerSimulationRadius)){ if(groundDataCells.get(this.getServerDataCellKey(tempVec)) != null){ loadedCellsLock.lock(); ServerDataCell cell = groundDataCells.get(this.getServerDataCellKey(tempVec)); if(!cell.containsPlayer(player)){ cell.addPlayer(player); } loadedCellsLock.unlock(); } else { loadedCellsLock.lock(); //create data cell this.createServerDataCell(tempVec); //add player groundDataCells.get(this.getServerDataCellKey(tempVec)).addPlayer(player); loadedCellsLock.unlock(); } } } } } Globals.profiler.endCpuSample(); } } Globals.profiler.endCpuSample(); return playerChangedChunk; } /** * Updates the tracking data for each cell */ private void updateTrackingData(){ Globals.profiler.beginCpuSample("GriddedDataCellManager.updateTrackingData"); loadedCellsLock.lock(); for(ServerDataCell cell : this.groundDataCells.values()){ GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(cell); trackingData.setCreatureCount(cell.getScene().getEntitiesWithTag(EntityTags.CREATURE).size()); } loadedCellsLock.unlock(); Globals.profiler.endCpuSample(); } /** * 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.serverState.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; } this.unloadCell(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){ this.unloadCell(cell); } loadedCellsLock.unlock(); this.serverTerrainManager.evictAll(); toCleanQueue.clear(); } /** * Unloads a server data cell * @param cell The cell */ public void unloadCell(ServerDataCell cell){ 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"); this.loaderService.queueLocationBasedOperation(key, () -> { serverContentManager.saveSerializationToDisk(key, serializedEntities); serverTerrainManager.savePositionToDisk(worldPos); }); Globals.profiler.endCpuSample(); //deregister from all tracking structures this.parent.deregisterCell(cell); this.groundDataCells.remove(key); PhysicsDataCell releasedCell = this.posPhysicsMap.remove(key); if(releasedCell != null){ this.cellPool.add(releasedCell); releasedCell.destroyEntities(); } this.cellPositionMap.remove(cell); this.cellTrackingMap.remove(cell); this.cellPlayerlessFrameMap.remove(cell); } /** * Gets a physics data cell from the pool * @return The physics data cell */ private PhysicsDataCell getPhysicsDataCell(Vector3i worldPos, Entity physicsEntity, Entity blockPhysicsEntity){ PhysicsDataCell rVal = null; loadedCellsLock.lock(); if(cellPool.size() > 0){ rVal = this.cellPool.poll(); rVal.reset(worldPos, physicsEntity, blockPhysicsEntity); } else { rVal = PhysicsDataCell.createPhysicsCell(physicsEntity, blockPhysicsEntity); } loadedCellsLock.unlock(); return rVal; } /** * 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 = ServerWorldData.convertRealToChunkSpace(point.x); int worldY = ServerWorldData.convertRealToChunkSpace(point.y); int worldZ = ServerWorldData.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 = ServerWorldData.convertRealToChunkSpace(point.x); int worldY = ServerWorldData.convertRealToChunkSpace(point.y); int worldZ = ServerWorldData.convertRealToChunkSpace(point.z); Vector3i worldPos = new Vector3i(worldX,worldY,worldZ); return this.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(); // //clear nearby entity lookup cache this.nearbyLookupCache.clear(); //regenerate physics where relevant terrainEditLock.acquireUninterruptibly(); if(physicsQueue.size() > 0){ for(Vector3i pos : physicsQueue.values()){ this.createTerrainPhysicsEntities(pos); } physicsQueue.clear(); } terrainEditLock.release(); //micro simulation boolean runMicroSim = Globals.serverState.microSimulation != null && Globals.serverState.microSimulation.isReady(); if(runMicroSim){ List simulationTargets = this.groundDataCells.values().stream().filter((ServerDataCell cell) -> this.shouldSimulate(cell)).collect(Collectors.toList()); for(ServerDataCell cell : simulationTargets){ Globals.serverState.microSimulation.simulate(cell); //queue fluid simulation if(EngineState.EngineFlags.RUN_FLUIDS){ Vector3i cellPos = this.getCellWorldPosition(cell); if(cellPos != null){ this.serverFluidManager.queue(cellPos.x, cellPos.y, cellPos.z); } } } } //simulate fluids if(EngineState.EngineFlags.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(); this.updateTrackingData(); 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){ GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(cell); return //has creature (trackingData.getCreatureCount() > 0) && //has player (cell.getPlayers().size() > 0 && trackingData.getClosestPlayer() < SIMULATION_DISTANCE_CUTOFF) ; } /** * 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, Long key, PhysicsDataCell cell, Map posPhysicsMap, Map groundDataCells, Map cellTrackingMap, 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 = this.getPhysicsDataCell(worldPos, terrainEntity, blockEntity); if(cell == null){ posPhysicsMap.put(key, targetCell); } else { ServerEntityUtils.destroyEntity(terrainEntity); ServerEntityUtils.destroyEntity(blockEntity); } if(parent.getMacroData() != null){ MacroDataUpdater.update(parent, parent.getMacroData(), realPos); } this.generationService.submit(() -> { try { BlockChunkData blockChunkData = realm.getServerWorldData().getServerBlockManager().getChunk(worldPos.x, worldPos.y, worldPos.z); ServerTerrainChunk terrainChunk = realm.getServerWorldData().getServerTerrainManager().getChunk(worldPos.x, worldPos.y, worldPos.z, ServerChunkCache.STRIDE_FULL_RES); while(terrainChunk == null){ TimeUnit.MILLISECONDS.sleep(1); terrainChunk = realm.getServerWorldData().getServerTerrainManager().getChunk(worldPos.x, worldPos.y, worldPos.z, ServerChunkCache.STRIDE_FULL_RES); } while(blockChunkData == null){ TimeUnit.MILLISECONDS.sleep(1); blockChunkData = realm.getServerWorldData().getServerBlockManager().getChunk(worldPos.x, worldPos.y, worldPos.z); } targetCell.setTerrainChunk(terrainChunk); targetCell.setBlockChunk(blockChunkData); //create physics entities if(cell != null){ cell.destroyEntities(); cell.generatePhysics(); } else { targetCell.generatePhysics(); } //set ready dataCell.setReady(true); } catch(Throwable ex){ LoggerInterface.loggerEngine.ERROR(ex); } }); } /** * 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(); Vector3i localWorldPos = new Vector3i(worldPos); Long cellKey = this.getServerDataCellKey(localWorldPos); loadedCellsLock.lock(); if(groundDataCells.containsKey(cellKey)){ throw new Error("Creating server data cell at position that already has a cell! " + localWorldPos); } groundDataCells.put(cellKey,rVal); cellPlayerlessFrameMap.put(rVal,0); LoggerInterface.loggerEngine.DEBUG("Create server data cell with key " + cellKey); cellPositionMap.put(rVal,localWorldPos); GriddedDataCellTrackingData trackingData = new GriddedDataCellTrackingData(); this.cellTrackingMap.put(rVal,trackingData); loadedCellsLock.unlock(); Long key = this.getServerDataCellKey(localWorldPos); //generate content this.loaderService.queueLocationBasedOperation(key, () -> { try { serverContentManager.generateContentForDataCell(parent, localWorldPos, rVal, cellKey); } catch(Error e){ e.printStackTrace(); } catch(Exception e){ e.printStackTrace(); } }); //generates physics for the cell in a dedicated thread then finally registers loadedCellsLock.lock(); PhysicsDataCell cell = posPhysicsMap.get(key); this.runPhysicsGenerationThread(localWorldPos,key,cell,this.posPhysicsMap,this.groundDataCells,this.cellTrackingMap,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, ServerChunkCache.STRIDE_FULL_RES).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, ServerChunkCache.STRIDE_FULL_RES).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 this.getChunkAtPosition(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 >= ServerWorldData.convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete())){ returnPos.x = ServerWorldData.convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete()) - 1; } if(positionToTest.y < 0){ returnPos.y = 0; } if(positionToTest.y >= ServerWorldData.convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete())){ returnPos.y = ServerWorldData.convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete()) - 1; } if(positionToTest.z < 0){ returnPos.z = 0; } if(positionToTest.z >= ServerWorldData.convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete())){ returnPos.z = ServerWorldData.convertChunkToRealSpace(parent.getServerWorldData().getWorldSizeDiscrete()) - 1; } return returnPos; } /** * Stops the executor service */ public void halt(){ this.generationService.shutdownNow(); this.loaderService.haltThreads(); } @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 long key = this.getServerDataCellKey(worldPosition); if(!this.physicsQueue.containsKey(key)){ this.physicsQueue.put(key,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; } /** * Gets the tracking data for a cell * @param cell The cell * @return The tracking data as a string */ public String getCellData(ServerDataCell cell){ String message = "Failed to find position of cell!\n" + "groundDataCells: " + this.groundDataCells.values().contains(cell) + "\n" + "cellPositionMap: " + this.cellPositionMap.keySet().contains(cell) + "\n" + "cellPlayerlessFrameMap: " + this.cellPositionMap.keySet().contains(cell) + "\n" + "cellTrackingMap: " + this.cellTrackingMap.keySet().contains(cell) + "\n" + ""; return message; } @Override public List findPath(Vector3d start, Vector3d end){ Vector3i startChunkPos = ServerWorldData.convertRealToChunkSpace(start); ServerDataCell cell = this.getCellAtWorldPosition(startChunkPos); GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(cell); if(trackingData == null){ throw new Error("Failed to find tracking data for " + start); } Vector3d nearestValidGoal = this.pathfinder.scanNearestWalkable(this, end, VoxelPathfinder.DEFAULT_MAX_TARGET_SCAN_DIST); if(nearestValidGoal == null){ nearestValidGoal = this.pathfinder.scanNearestWalkable(this, end, VoxelPathfinder.DEFAULT_MAX_TARGET_SCAN_DIST); throw new Error("Failed to resolve valid point near " + end.x + "," + end.y + "," + end.z); } List points = this.pathfinder.findPath(this, start, nearestValidGoal, VoxelPathfinder.DEFAULT_MAX_COST); return points; } /** * Prints information about the provided server data cell * @param serverDataCell The cell */ public void printCellInfo(ServerDataCell serverDataCell){ LoggerInterface.loggerEngine.WARNING("Number of players: " + serverDataCell.getPlayers().size()); GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(serverDataCell); if(trackingData == null){ LoggerInterface.loggerEngine.WARNING("No tracking data for this cell in the gridded data cell manager"); } else { LoggerInterface.loggerEngine.WARNING("Closest player distance: " + trackingData.getClosestPlayer()); } LoggerInterface.loggerEngine.WARNING("Should simulate: " + this.shouldSimulate(serverDataCell)); LoggerInterface.loggerEngine.WARNING("Cell is ready: " + serverDataCell.isReady()); } @Override public Collection entityLookup(Vector3d pos, double radius) { Vector3i chunkPos = ServerWorldData.convertRealToChunkSpace(pos); long key = this.getServerDataCellKey(chunkPos); if(this.nearbyLookupCache.containsKey(key)){ return Collections.unmodifiableCollection(this.nearbyLookupCache.get(key)); } List rVal = new LinkedList(); this.loadedCellsLock.lock(); for(ServerDataCell cell : this.groundDataCells.values()){ if(ServerWorldData.convertChunkToRealSpace(this.getCellWorldPosition(cell)).distance(pos) > radius){ continue; } rVal.addAll(cell.getScene().getEntityList()); } this.nearbyLookupCache.put(key,rVal); this.loadedCellsLock.unlock(); return Collections.unmodifiableCollection(rVal); } @Override public ServerTerrainChunk getChunkAtPosition(int worldX, int worldY, int worldZ) { return serverTerrainManager.getChunk(worldX, worldY, worldZ, ServerChunkCache.STRIDE_FULL_RES); } @Override public PathingProgressiveData findPathAsync(Vector3d start, Vector3d end) { return Globals.serverState.aiManager.getPathfindingService().queuePathfinding(start, end, this.pathfinder, this); } @Override public Vector3d getMacroEntryPoint(Vector3d point){ Vector3d rVal = null; //find the closest data cell ServerDataCell closestCell = null; double closestDist = MACRO_SCANNING_BIG_NUMBER; for(ServerDataCell cell : this.groundDataCells.values()){ Vector3i cellChunkPos = this.cellPositionMap.get(cell); Vector3d cellRealPos = ServerWorldData.convertChunkToRealSpace(cellChunkPos); double dist = cellRealPos.distance(point); if(dist < closestDist){ closestCell = cell; closestDist = dist; } } if(closestDist == MACRO_SCANNING_BIG_NUMBER || closestCell == null){ throw new Error("Failed to find closer cell"); } //get surface height at the (1,?,1) point of this cell Vector3i cellChunkPos = this.cellPositionMap.get(closestCell); double elevation = this.serverTerrainManager.getElevation(cellChunkPos.x, cellChunkPos.z, 1, 1); //store as return vec rVal = new Vector3d( ServerWorldData.convertVoxelToRealSpace(1, cellChunkPos.x), elevation, ServerWorldData.convertVoxelToRealSpace(1, cellChunkPos.z) ); return rVal; } @Override public void evaluateMacroObject(MacroObject object){ //figure out if a cell should contain this object Vector3i macroObjectChunkPos = ServerWorldData.convertRealToChunkSpace(object.getPos()); ServerDataCell container = null; for(ServerDataCell serverDataCell : this.groundDataCells.values()){ Vector3i pos = this.getCellWorldPosition(serverDataCell); if(pos.equals(macroObjectChunkPos)){ container = serverDataCell; break; } } //if it is in real space, spawn the object if(container != null){ this.serverContentManager.spawnMacroObject(parent, object); } } @Override public boolean containsCell(ServerDataCell cell) { return this.groundDataCells.values().contains(cell); } @Override public boolean hasBlocksAtPosition(Vector3i worldPosition) { return this.serverWorldData.getServerBlockManager().hasChunk(worldPosition.x, worldPosition.y, worldPosition.z); } }