package electrosphere.server.terrain.manager; import electrosphere.client.terrain.cache.ChunkData; import electrosphere.engine.Globals; import electrosphere.game.server.world.ServerWorldData; import electrosphere.server.terrain.diskmap.ChunkDiskMap; import electrosphere.server.terrain.generation.TestGenerationChunkGenerator; import electrosphere.server.terrain.generation.continentphase.TerrainGenerator; import electrosphere.server.terrain.generation.interfaces.ChunkGenerator; import electrosphere.server.terrain.models.TerrainModel; import electrosphere.server.terrain.models.TerrainModification; import electrosphere.util.FileUtils; import electrosphere.util.annotation.Exclude; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; import org.joml.Vector3i; /** * Provides an interface for the server to query information about terrain */ public class ServerTerrainManager { /** * The number of threads for chunk generation */ public static final int GENERATION_THREAD_POOL_SIZE = 1; /** * Full world discrete size */ public static final int WORLD_SIZE_DISCRETE = 1000; /** * Vertical interp ratio */ public static final int VERTICAL_INTERPOLATION_RATIO = 50; //the interpolation width of a server terrain manager is hard coded at the moment to make sure it's divisible by the sub chunk calculations public static final int SERVER_TERRAIN_MANAGER_INTERPOLATION_RATIO = 128; /** * The dampening ratio for ground noise */ public static final float SERVER_TERRAIN_MANAGER_DAMPENER = 1.0f; /** * The parent world data */ ServerWorldData parent; //the seed for terrain generation long seed; //The model of the terrain this manager is managing TerrainModel model; /** * The cache of chunks */ @Exclude ServerChunkCache chunkCache = new ServerChunkCache(); //The map of chunk position <-> file on disk containing chunk data ChunkDiskMap chunkDiskMap = null; //The generation algorithm for this terrain manager @Exclude ChunkGenerator chunkGenerator; /** * The threadpool for chunk generation */ @Exclude ExecutorService chunkExecutorService = Executors.newFixedThreadPool(GENERATION_THREAD_POOL_SIZE); /** * Constructor */ public ServerTerrainManager( ServerWorldData parent, long seed, ChunkGenerator chunkGenerator ){ this.parent = parent; this.seed = seed; this.chunkGenerator = chunkGenerator; } ServerTerrainManager(){ } /** * Generates a terrain model for the manager */ public void generate(){ TerrainGenerator terrainGen = new TerrainGenerator(); terrainGen.setInterpolationRatio(parent.getWorldSizeDiscrete()/200); terrainGen.setVerticalInterpolationRatio(parent.getWorldSizeDiscrete()); terrainGen.setRandomSeed(seed); model = terrainGen.generateModel(); this.chunkGenerator.setModel(model); model.setInterpolationRandomDampener(SERVER_TERRAIN_MANAGER_DAMPENER); this.chunkDiskMap = new ChunkDiskMap(); } /** * Saves the terrain model backing this manager to a save file * @param saveName The name of the save */ public void save(String saveName){ if(model != null){ ByteBuffer buffer = ByteBuffer.allocate(model.getElevation().length * model.getElevation()[0].length * 4); FloatBuffer floatView = buffer.asFloatBuffer(); for(int x = 0; x < model.getElevation().length; x++){ floatView.put(model.getElevation()[x]); } if(floatView.position() > 0){ floatView.flip(); } FileUtils.serializeObjectToSavePath(saveName, "./terrain.json", model); FileUtils.saveBinaryToSavePath(saveName, "./terrain.dat", buffer.array()); } //for each chunk, save via disk map for(ServerTerrainChunk chunk : this.chunkCache.getContents()){ chunkDiskMap.saveToDisk(chunk); } //save disk map itself if(chunkDiskMap != null){ chunkDiskMap.save(); } } /** * Loads a terrain manager from a save file * @param saveName The name of the save */ public void load(String saveName){ //load terrain model if(FileUtils.getSaveFile(saveName, "./terrain.json").exists()){ model = FileUtils.loadObjectFromSavePath(saveName, "./terrain.json", TerrainModel.class); chunkGenerator.setModel(model); byte[] data = FileUtils.loadBinaryFromSavePath(saveName, "./terrain.dat"); ByteBuffer buffer = ByteBuffer.wrap(data); FloatBuffer floatView = buffer.asFloatBuffer(); float[][] elevation = new float[parent.getWorldSizeDiscrete()][parent.getWorldSizeDiscrete()]; for(int x = 0; x < parent.getWorldSizeDiscrete(); x++){ for(int y = 0; y < parent.getWorldSizeDiscrete(); y++){ elevation[x][y] = floatView.get(); } } model.setElevationArray(elevation); } //load chunk disk map chunkDiskMap = new ChunkDiskMap(); chunkDiskMap.init(saveName); } /** * Generates a test terrain model * @param chunkGen The chunk generator */ public void genTestData(TestGenerationChunkGenerator chunkGen){ this.model = TerrainModel.generateTestModel(); chunkGen.setModel(model); } /** * Evicts all cached terrain */ public void evictAll(){ this.chunkCache.clear(); } public float[][] getTerrainAtChunk(int x, int y){ return model.getElevationForChunk(x, y); } public double getHeightAtPosition(double x, double y, double z){ return y; } public float getDiscreteValue(int x, int y){ if(model != null){ return model.getElevation()[x][y]; } else { return 0; } } public int getDynamicInterpolationRatio(){ //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING if(model != null){ return model.getDynamicInterpolationRatio(); } else { //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING return 0; } } public float getRandomDampener(){ //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING if(model != null){ return model.getRandomDampener(); } else { //THIS FIRES IF THERE IS AN ARENA WORLD RUNNING return 0.0f; } } /** * Gets the terrain model backing this terrain manager * @return The terrain model */ public TerrainModel getModel() { return model; } /** * Performs logic once a server chunk is available * @param worldX The world x position * @param worldY The world y position * @param worldZ The world z position * @return The ServerTerrainChunk */ public ServerTerrainChunk getChunk(int worldX, int worldY, int worldZ){ Globals.profiler.beginCpuSample("ServerTerrainManager.getChunk"); //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING ServerTerrainChunk returnedChunk = null; if(chunkCache.containsChunk(worldX,worldY,worldZ,ChunkData.NO_STRIDE)){ returnedChunk = chunkCache.get(worldX,worldY,worldZ, ChunkData.NO_STRIDE); } else { //pull from disk if it exists if(chunkDiskMap != null){ if(chunkDiskMap.containsTerrainAtPosition(worldX, worldY, worldZ)){ returnedChunk = chunkDiskMap.getTerrainChunk(worldX, worldY, worldZ); } } //generate if it does not exist if(returnedChunk == null){ returnedChunk = chunkGenerator.generateChunk(worldX, worldY, worldZ, ChunkData.NO_STRIDE); } this.chunkCache.add(worldX, worldY, worldZ, ChunkData.NO_STRIDE, returnedChunk); } Globals.profiler.endCpuSample(); return returnedChunk; } /** * Performs logic once a server chunk is available * @param worldX The world x position * @param worldY The world y position * @param worldZ The world z position * @param stride The stride of the data * @param onLoad The logic to run once the chunk is available */ public void getChunkAsync(int worldX, int worldY, int worldZ, int stride, Consumer onLoad){ Globals.profiler.beginCpuSample("ServerTerrainManager.getChunkAsync"); this.chunkExecutorService.submit(new ChunkGenerationThread(chunkDiskMap, chunkCache, chunkGenerator, worldX, worldY, worldZ, stride, onLoad)); Globals.profiler.endCpuSample(); } /** * Saves a given position's chunk to disk. * Uses the current global save name * @param position The position to save */ public void savePositionToDisk(Vector3i position){ if(chunkDiskMap != null){ chunkDiskMap.saveToDisk(getChunk(position.x, position.y, position.z)); } } /** * Applies a deform to terrain at a given location * @param worldPos The world coordinates of the chunk to modify * @param voxelPos The voxel coordinates of the voxel to modify * @param weight The weight to set it to * @param value The value to set it to */ public void deformTerrainAtLocationToValue(Vector3i worldPos, Vector3i voxelPos, float weight, int value){ TerrainModification modification = new TerrainModification(worldPos,voxelPos,weight,value); //could be null if, for instance, arena mode if(model != null){ model.addModification(modification); } if(chunkCache.containsChunk(worldPos.x,worldPos.y,worldPos.z,ChunkData.NO_STRIDE)){ ServerTerrainChunk chunk = chunkCache.get(worldPos.x,worldPos.y,worldPos.z, ChunkData.NO_STRIDE); chunk.addModification(modification); } } /** * Sets the parent world data of this manager * @param serverWorldData The parent world data */ public void setParent(ServerWorldData serverWorldData){ this.parent = serverWorldData; } /** * Gets the chunk generator of the terrain manager * @return The chunk generator */ public ChunkGenerator getChunkGenerator(){ return chunkGenerator; } /** * Closes the generation threadpool */ public void closeThreads(){ this.chunkExecutorService.shutdownNow(); } }