Renderer/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java
austin 86344ebfeb
Some checks failed
studiorailgun/Renderer/pipeline/head There was a failure building this commit
small bugfixes
2025-04-03 18:36:15 -04:00

1007 lines
44 KiB
Java

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<Long,ServerDataCell> groundDataCells = new HashMap<Long,ServerDataCell>();
/**
* Map of server cell to its world position
*/
Map<ServerDataCell,Vector3i> cellPositionMap = new HashMap<ServerDataCell,Vector3i>();
/**
* Map of server data cell to the number of frames said cell has had no players
*/
Map<ServerDataCell,Integer> cellPlayerlessFrameMap = new HashMap<ServerDataCell,Integer>();
/**
* 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<ServerDataCell> toCleanQueue = new HashSet<ServerDataCell>();
/**
* Map of world position key -> physics cell
*/
Map<Long,PhysicsDataCell> posPhysicsMap = new HashMap<Long,PhysicsDataCell>();
/**
* 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<Long, PhysicsDataCell> posPhysicsMap,
Map<Long, ServerDataCell> 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<Vector3i> worldPositionsToUpdate = new LinkedList<Vector3i>();
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<ServerDataCell> 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<ServerDataCell,Integer> getCellPlayerlessFrameMap(){
return cellPlayerlessFrameMap;
}
}