511 lines
23 KiB
Java
511 lines
23 KiB
Java
package electrosphere.server.datacell;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
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.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<String,ServerDataCell> groundDataCells = new HashMap<String,ServerDataCell>();
|
|
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>();
|
|
//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<ServerDataCell> 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<ServerDataCell>();
|
|
}
|
|
|
|
/**
|
|
* 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<ServerDataCell> toCleanQueue = new HashSet<ServerDataCell>();
|
|
/**
|
|
* 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);
|
|
//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();
|
|
groundDataCells.put(getServerDataCellKey(worldPos),rVal);
|
|
LoggerInterface.loggerEngine.DEBUG("Create server data cell with key " + getServerDataCellKey(worldPos));
|
|
cellPositionMap.put(rVal,new Vector3i(worldPos));
|
|
serverContentManager.generateContentForDataCell(parent, worldPos, rVal);
|
|
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
|
|
groundDataCells.get(getServerDataCellKey(worldPosition)).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())
|
|
);
|
|
}
|
|
|
|
}
|