/*
* This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT).
*
* Copyright (c) 2015 contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package cubicchunks.server;
import net.minecraft.entity.EnumCreatureType;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.gen.ChunkProviderServer;
import org.apache.logging.log4j.Logger;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import javax.annotation.Detainted;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import cubicchunks.CubicChunks;
import cubicchunks.server.chunkio.CubeIO;
import cubicchunks.server.chunkio.async.forge.AsyncWorldIOExecutor;
import cubicchunks.util.CubePos;
import cubicchunks.util.XYZMap;
import cubicchunks.world.ICubeProvider;
import cubicchunks.world.ICubicWorldServer;
import cubicchunks.world.IProviderExtras;
import cubicchunks.world.column.Column;
import cubicchunks.world.cube.Cube;
import cubicchunks.worldgen.generator.ICubeGenerator;
import cubicchunks.worldgen.generator.ICubePrimer;
/**
* This is CubicChunks equivalent of ChunkProviderServer, it loads and unloads Cubes and Columns.
* <p>
* There are a few necessary changes to the way vanilla methods work:
* * Because loading a Chunk (Column) doesn't make much sense with CubicChunks,
* all methods that load Chunks, actually load an empry column with no blocks in it
* (there may be some entities that are not in any Cube yet).
* * dropChunk method is not supported. Columns are unloaded automatically when the last cube is unloaded
*/
public class CubeProviderServer extends ChunkProviderServer implements ICubeProvider, IProviderExtras {
private static final Logger log = CubicChunks.LOGGER;
private ICubicWorldServer worldServer;
private CubeIO cubeIO;
// TODO: Use a better hash map!
private XYZMap<Cube> cubeMap = new XYZMap<>(0.7f, 8000);
private ICubeGenerator cubeGen;
public CubeProviderServer(ICubicWorldServer worldServer, ICubeGenerator cubeGen) {
super((WorldServer) worldServer,
worldServer.getSaveHandler().getChunkLoader(worldServer.getProvider()), // forge uses this in
null); // safe to null out IChunkGenerator (Note: lets hope mods don't touch it, ik its public)
this.cubeGen = cubeGen;
this.worldServer = worldServer;
this.cubeIO = new CubeIO(worldServer);
}
@Override
@Detainted
public void unload(Chunk chunk) {
//ignore, ChunkGc unloads cubes
}
@Override
@Detainted
public void unloadAllChunks() {
//ignore, ChunkGc unloads cubes
}
/**
* Vanilla method, returns a Chunk (Column) only of it's already loaded.
*/
@Override
@Nullable
public Column getLoadedColumn(int columnX, int columnZ) {
return (Column) this.id2ChunkMap.get(ChunkPos.asLong(columnX, columnZ));
}
@Override
@Nullable
public Column getLoadedChunk(int columnX, int columnZ) {
return getLoadedColumn(columnX, columnZ);
}
/**
* Loads Chunk (Column) if it can be loaded from disk, or returns already loaded one.
* Doesn't generate new Columns.
*/
@Override
@Nullable
public Column loadChunk(int columnX, int columnZ) {
return this.loadChunk(columnX, columnZ, null);
}
/**
* Load chunk asynchronously. Currently CubicChunks only loads synchronously.
*/
@Override
@Nullable
public Column loadChunk(int columnX, int columnZ, Runnable runnable) {
// TODO: Set this to LOAD when PlayerCubeMap works
if (runnable == null) {
return getColumn(columnX, columnZ, /*Requirement.LOAD*/Requirement.LIGHT);
}
// TODO here too
asyncGetColumn(columnX, columnZ, Requirement.LIGHT, col -> runnable.run());
return null;
}
/**
* If this Column is already loaded - returns it.
* Loads from disk if possible, otherwise generates new Column.
*/
@Override
public Column provideColumn(int cubeX, int cubeZ) {
return getColumn(cubeX, cubeZ, Requirement.GENERATE);
}
@Override
public Column provideChunk(int cubeX, int cubeZ) {
return provideColumn(cubeX, cubeZ);
}
@Override
public boolean saveChunks(boolean alwaysTrue) {
for (Cube cube : cubeMap) { // save cubes
if (cube.needsSaving()) {
this.cubeIO.saveCube(cube);
}
}
for (Chunk chunk : id2ChunkMap.values()) { // save columns
Column column = (Column) chunk;
// save the column
if (column.needsSaving(alwaysTrue)) {
this.cubeIO.saveColumn(column);
}
}
return true;
}
@Override
public boolean unloadQueuedChunks() {
// NOTE: the return value is completely ignored
// NO-OP, This is called by WorldServer's tick() method every tick
return false;
}
@Override
public String makeString() {
return "CubeProviderServer: " + this.id2ChunkMap.size() + " columns, "
+ this.cubeMap.getSize() + " cubes";
}
@Override
public List<Biome.SpawnListEntry> getPossibleCreatures(@Nonnull final EnumCreatureType type, @Nonnull final BlockPos pos) {
return cubeGen.getPossibleCreatures(type, pos);
}
@Nullable
public BlockPos getStrongholdGen(@Nonnull World worldIn, @Nonnull String name, @Nonnull BlockPos pos) {
return cubeGen.getClosestStructure(name, pos);
}
// getLoadedChunkCount() in ChunkProviderServer is fine - CHECKED: 1.10.2-12.18.1.2092
@Override
public boolean chunkExists(int cubeX, int cubeZ) {
return this.id2ChunkMap.get(ChunkPos.asLong(cubeX, cubeZ)) != null;
}
//==============================
//=====CubicChunks methods======
//==============================
@Override
public Cube getCube(int cubeX, int cubeY, int cubeZ) {
return getCube(cubeX, cubeY, cubeZ, Requirement.GENERATE);
}
@Override
public Cube getCube(CubePos coords) {
return getCube(coords.getX(), coords.getY(), coords.getZ());
}
@Override
public Cube getLoadedCube(int cubeX, int cubeY, int cubeZ) {
return cubeMap.get(cubeX, cubeY, cubeZ);
}
@Override
public Cube getLoadedCube(CubePos coords) {
return getLoadedCube(coords.getX(), coords.getY(), coords.getZ());
}
/**
* Load a cube, asynchronously. The work done to retrieve the column is specified by the
* {@link Requirement} <code>req</code>
*
* @param cubeX Cube x position
* @param cubeY Cube y position
* @param cubeZ Cube z position
* @param req Work done to retrieve the column
* @param callback Callback to be called when the load finishes. Note that <code>null</code> can be passed to the
* callback if the work specified by <code>req</code> is not sufficient to provide a cube
*
* @see #getCube(int, int, int, Requirement) for the synchronous equivalent to this method
*/
public void asyncGetCube(int cubeX, int cubeY, int cubeZ, @Nonnull Requirement req, @Nonnull Consumer<Cube> callback) {
Cube cube = getLoadedCube(cubeX, cubeY, cubeZ);
if (req == Requirement.GET_CACHED || (cube != null && req.compareTo(Requirement.GENERATE) <= 0)) {
callback.accept(cube);
return;
}
if (cube == null) {
AsyncWorldIOExecutor.queueCubeLoad(worldServer, cubeIO, this, cubeX, cubeY, cubeZ, loaded -> {
Column col = getLoadedColumn(cubeX, cubeZ);
if (col != null) {
onCubeLoaded(loaded, col);
}
loaded = postCubeLoadAttempt(cubeX, cubeY, cubeZ, loaded, col, req);
callback.accept(loaded);
});
}
}
@Override
@Nullable
public Cube getCube(int cubeX, int cubeY, int cubeZ, @Nonnull Requirement req) {
Cube cube = getLoadedCube(cubeX, cubeY, cubeZ);
if (req == Requirement.GET_CACHED ||
(cube != null && req.compareTo(Requirement.GENERATE) <= 0)) {
return cube;
}
// try to get the Column
Column column = getColumn(cubeX, cubeZ, req);
if (column == null) {
return cube; // Column did not reach req, so Cube also does not
}
if (cube == null) {
cube = AsyncWorldIOExecutor.syncCubeLoad(worldServer, cubeIO, this, cubeX, cubeY, cubeZ);
onCubeLoaded(cube, column);
}
return postCubeLoadAttempt(cubeX, cubeY, cubeZ, cube, column, req);
}
/**
* After successfully loading a cube, add it to it's column and the lookup table
*
* @param cube The cube that was loaded
* @param column The column of the cube
*/
private void onCubeLoaded(@Nullable Cube cube, @Nonnull Column column) {
if (cube != null) {
cubeMap.put(cube); // cache the Cube
//synchronous loading may cause it to be called twice when async loading has been already queued
//because AsyncWorldIOExecutor only executes one task for one cube and because only saving a cube
//can modify one that is being loaded, it's impossible to end up with 2 versions of the same cube
//This is only to prevents multiple callbacks for the same queued load from adding the same cube twice.
if (!column.getLoadedCubes().contains(cube)) {
column.addCube(cube);
cube.onLoad(); // init the Cube
}
}
}
/**
* Process a recently loaded cube as per the specified effort level.
*
* @param cubeX Cube x position
* @param cubeY Cube y position
* @param cubeZ Cube z positon
* @param cube The loaded cube, if loaded, else <code>null</code>
* @param column The column of the cube
* @param req Work done on the cube
*
* @return The processed cube, or <code>null</code> if the effort level is not sufficient to provide a cube
*/
@Nullable
private Cube postCubeLoadAttempt(int cubeX, int cubeY, int cubeZ, @Nullable Cube cube, @Nonnull Column column, @Nonnull Requirement req) {
// Fast path - Nothing to do here
if (req == Requirement.LOAD) return cube;
if (req == Requirement.GENERATE && cube != null) return cube;
if (cube == null) {
// generate the Cube
cube = generateCube(cubeX, cubeY, cubeZ, column);
if (req == Requirement.GENERATE) {
return cube;
}
}
if (!cube.isFullyPopulated()) {
// forced full population of this cube
populateCube(cube);
if (req == Requirement.POPULATE) {
return cube;
}
}
//TODO: Direct skylight might have changed and even Cubes that have there
// initial light done, there might be work to do for a cube that just loaded
if (!cube.isInitialLightingDone()) {
calculateDiffuseSkylight(cube);
}
return cube;
}
/**
* Generate a cube at the specified position
*
* @param cubeX Cube x position
* @param cubeY Cube y position
* @param cubeZ Cube z position
* @param column Column of the cube
*
* @return The generated cube
*/
@Nonnull
private Cube generateCube(int cubeX, int cubeY, int cubeZ, @Nonnull Column column) {
ICubePrimer primer = cubeGen.generateCube(cubeX, cubeY, cubeZ);
Cube cube = new Cube(column, cubeY, primer);
this.worldServer.getFirstLightProcessor()
.initializeSkylight(cube); // init sky light, (does not require any other cubes, just ServerHeightMap)
onCubeLoaded(cube, column);
return cube;
}
/**
* Populate a cube at the specified position, generating surrounding cubes as necessary
*
* @param cube The cube to populate
*/
private void populateCube(@Nonnull Cube cube) {
int cubeX = cube.getX();
int cubeY = cube.getY();
int cubeZ = cube.getZ();
cubeGen.getPopulationRequirement(cube).forEachPoint((x, y, z) -> {
Cube popcube = getCube(x + cubeX, y + cubeY, z + cubeZ);
if (!popcube.isPopulated()) {
cubeGen.populate(popcube);
popcube.setPopulated(true);
}
});
cube.setFullyPopulated(true);
}
/**
* Initialize skylight for the cube at the specified position, generating surrounding cubes as needed.
*
* @param cube The cube to light up
*/
private void calculateDiffuseSkylight(@Nonnull Cube cube) {
int cubeX = cube.getX();
int cubeY = cube.getY();
int cubeZ = cube.getZ();
for (int x = -2; x <= 2; x++) {
for (int z = -2; z <= 2; z++) {
for (int y = 2; y >= -2; y--) {
if (x != 0 || y != 0 || z != 0) {
getCube(x + cubeX, y + cubeY, z + cubeZ);
}
}
}
}
this.worldServer.getFirstLightProcessor().diffuseSkylight(cube);
}
/**
* Retrieve a column, asynchronously. The work done to retrieve the column is specified by the
* {@link Requirement} <code>req</code>
*
* @param columnX Column x position
* @param columnZ Column z position
* @param req Work done to retrieve the column
* @param callback Callback to be called when the column has finished loading. Note that the returned column is not
* guaranteed to be non-null
*
* @see CubeProviderServer#getColumn(int, int, Requirement) for the synchronous variant of this method
*/
public void asyncGetColumn(int columnX, int columnZ, Requirement req, Consumer<Column> callback) {
Column column = getLoadedColumn(columnX, columnZ);
if (column != null || req == Requirement.GET_CACHED) {
callback.accept(column);
return;
}
AsyncWorldIOExecutor.queueColumnLoad(worldServer, cubeIO, columnX, columnZ, col -> {
col = postProcessColumn(columnX, columnZ, col, req);
callback.accept(col);
});
}
@Override
@Nullable
public Column getColumn(int columnX, int columnZ, Requirement req) {
Column column = getLoadedColumn(columnX, columnZ);
if (column != null || req == Requirement.GET_CACHED) {
return column;
}
column = AsyncWorldIOExecutor.syncColumnLoad(worldServer, cubeIO, columnX, columnZ);
column = postProcessColumn(columnX, columnZ, column, req);
return column;
}
/**
* After loading a column, do work on it, where the work required is specified by <code>req</code>
*
* @param columnX X position of the column
* @param columnZ Z position of the column
* @param column The loaded column, or <code>null</code> if the column couldn't be loaded
* @param req The amount of work to be done on the cube
*
* @return The postprocessed column, or <code>null</code>
*/
@Nullable
private Column postProcessColumn(int columnX, int columnZ, Column column, Requirement req) {
if (column != null) {
id2ChunkMap.put(ChunkPos.asLong(columnX, columnZ), column);
column.setLastSaveTime(this.worldServer.getTotalWorldTime()); // the column was just loaded
column.onChunkLoad();
return column;
} else if (req == Requirement.LOAD) {
return null;
}
column = new Column(this, worldServer, columnX, columnZ);
cubeGen.generateColumn(column);
id2ChunkMap.put(ChunkPos.asLong(columnX, columnZ), column);
column.setLastSaveTime(this.worldServer.getTotalWorldTime()); // the column was just generated
column.onChunkLoad();
return column;
}
public String dumpLoadedCubes() {
StringBuilder sb = new StringBuilder(10000).append("\n");
for (Chunk chunk : this.id2ChunkMap.values()) {
Column column = (Column) chunk;
if (column == null) {
sb.append("column = null\n");
continue;
}
sb.append("Column[").append(column.getX()).append(", ").append(column.getZ()).append("] {");
boolean isFirst = true;
for (Cube cube : column.getLoadedCubes()) {
if (!isFirst) {
sb.append(", ");
}
isFirst = false;
if (cube == null) {
sb.append("cube = null");
continue;
}
sb.append("Cube[").append(cube.getY()).append("]");
}
sb.append("\n");
}
return sb.toString();
}
public void flush() {
this.cubeIO.flush();
}
Iterator<Cube> cubesIterator() {
return cubeMap.iterator();
}
@SuppressWarnings("unchecked")
Iterator<Column> columnsIterator() {
return (Iterator<Column>) (Object) id2ChunkMap.values().iterator();
}
boolean tryUnloadCube(Cube cube) {
if (!cube.getTickets().canUnload()) {
return false; // There are tickets
}
// unload the Cube!
cube.onUnload();
if (cube.needsSaving()) { // save the Cube, if it needs saving
this.cubeIO.saveCube(cube);
}
cube.getColumn().removeCube(cube.getY());
return true;
}
boolean tryUnloadColumn(Column column) {
if (column.hasLoadedCubes()) {
return false; // It has loaded Cubes in it
// (Cubes are to Columns, as tickets are to Cubes... in a way)
}
column.unloaded = true; // flag as unloaded (idk, maybe vanilla uses this somewhere)
// unload the Column!
column.onChunkUnload();
if (column.needsSaving(true)) { // save the Column, if it needs saving
this.cubeIO.saveColumn(column);
}
return true;
}
}