blob: 7ef791422692fac9a3bf5c5f5fdeb0edb4e4ce08 [file] [log] [blame] [raw]
package net.glowstone.chunk;
import com.google.common.collect.ConcurrentHashMultiset;
import com.google.common.collect.Multiset;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import lombok.Getter;
import net.glowstone.EventFactory;
import net.glowstone.GlowServer;
import net.glowstone.GlowWorld;
import net.glowstone.chunk.GlowChunk.Key;
import net.glowstone.constants.GlowBiome;
import net.glowstone.generator.GlowChunkData;
import net.glowstone.generator.GlowChunkGenerator;
import net.glowstone.generator.biomegrid.MapLayer;
import net.glowstone.io.ChunkIoService;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkPopulateEvent;
import org.bukkit.generator.BlockPopulator;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.material.MaterialData;
/**
* A class which manages the {@link GlowChunk}s currently loaded in memory.
*
* @author Graham Edgecombe
*/
public final class ChunkManager {
/**
* The world this ChunkManager is managing.
*/
private final GlowWorld world;
/**
* The chunk I/O service used to read chunks from the disk and write them to the disk.
*/
private final ChunkIoService service;
/**
* The chunk generator used to generate new chunks.
*
* @return the chunk generator
*/
@Getter
private final ChunkGenerator generator;
/**
* The biome maps used to fill chunks biome grid and terrain generation.
*/
private final MapLayer[] biomeGrid;
/**
* A map of chunks currently loaded in memory.
*/
private final ConcurrentMap<Key, GlowChunk> chunks = new ConcurrentHashMap<>();
/**
* A set of chunks which are being kept loaded by players or other factors.
*/
private final Multiset<Key> lockSet = ConcurrentHashMultiset.create();
/**
* Creates a new chunk manager with the specified I/O service and world generator.
*
* @param world The chunk manager's world.
* @param service The I/O service.
* @param generator The world generator.
*/
public ChunkManager(GlowWorld world, ChunkIoService service, ChunkGenerator generator) {
this.world = world;
this.service = service;
this.generator = generator;
biomeGrid = MapLayer.initialize(
world.getSeed(), world.getEnvironment(), world.getWorldType());
}
/**
* Gets a chunk object representing the specified coordinates, which might not yet be loaded.
*
* @param x The X coordinate.
* @param z The Z coordinate.
* @return The chunk.
*/
public GlowChunk getChunk(int x, int z) {
Key key = GlowChunk.Key.of(x, z);
if (chunks.containsKey(key)) {
return chunks.get(key);
} else {
// only create chunk if it's not in the map already
GlowChunk chunk = new GlowChunk(world, x, z);
chunks.put(key, chunk);
return chunk;
}
}
/**
* Checks if the Chunk at the specified coordinates is loaded.
*
* @param x The X coordinate.
* @param z The Z coordinate.
* @return true if the chunk is loaded, otherwise false.
*/
public boolean isChunkLoaded(int x, int z) {
Key key = GlowChunk.Key.of(x, z);
return chunks.containsKey(key) && chunks.get(key).isLoaded();
}
/**
* Check whether a chunk has locks on it preventing it from being unloaded.
*
* @param x The X coordinate.
* @param z The Z coordinate.
* @return Whether the chunk is in use.
*/
public boolean isChunkInUse(int x, int z) {
Key key = GlowChunk.Key.of(x, z);
return lockSet.contains(key);
}
/**
* Call the ChunkIoService to load a chunk, optionally generating the chunk.
*
* @param x The X coordinate of the chunk to load.
* @param z The Y coordinate of the chunk to load.
* @param generate Whether to generate the chunk if needed.
* @return True on success, false on failure.
*/
public boolean loadChunk(int x, int z, boolean generate) {
return loadChunk(getChunk(x, z), generate);
}
/**
* Attempts to load a chunk; handles exceptions.
* @param chunk the chunk address
* @param generate if true, generate the chunk if it's new or the saved copy is corrupted
* @return true if the chunk was loaded or generated successfully, false otherwise
*/
public boolean loadChunk(GlowChunk chunk, boolean generate) {
// try to load chunk
try {
if (service.read(chunk)) {
EventFactory.callEvent(new ChunkLoadEvent(chunk, false));
return true;
}
} catch (IOException e) {
GlowServer.logger.log(Level.SEVERE,
"Error while loading chunk (" + chunk.getX() + "," + chunk.getZ() + ")",
e);
// an error in chunk reading may have left the chunk in an invalid state
// (i.e. double initialization errors), so it's forcibly unloaded here
chunk.unload(false, false);
}
// stop here if we can't generate
if (!generate || world.getServer().isGenerationDisabled()) {
return false;
}
// get generating
try {
generateChunk(chunk, chunk.getX(), chunk.getZ());
} catch (Throwable ex) {
GlowServer.logger.log(Level.SEVERE,
"Error while generating chunk (" + chunk.getX() + "," + chunk.getZ() + ")",
ex);
return false;
}
EventFactory.callEvent(new ChunkLoadEvent(chunk, true));
// right now, forcePopulate takes care of populating chunks that players actually see.
/*for (int x2 = x - 1; x2 <= x + 1; ++x2) {
for (int z2 = z - 1; z2 <= z + 1; ++z2) {
populateChunk(x2, z2, false);
}
}*/
return true;
}
/**
* Unload chunks with no locks on them.
*/
public void unloadOldChunks() {
Iterator<Entry<Key, GlowChunk>> chunksEntryIter = chunks.entrySet().iterator();
while (chunksEntryIter.hasNext()) {
Entry<Key, GlowChunk> entry = chunksEntryIter.next();
if (!lockSet.contains(entry.getKey())) {
if (!entry.getValue().unload(true, true)) {
GlowServer.logger.warning(
"Failed to unload chunk " + world.getName() + ":" + entry.getKey());
}
}
if (!entry.getValue().isLoaded()) {
//GlowServer.logger.info("Removing from cache " + entry.getKey());
chunksEntryIter.remove();
lockSet.setCount(entry.getKey(), 0);
}
}
}
/**
* Populate a single chunk if needed.
*/
private void populateChunk(int x, int z, boolean force) {
GlowChunk chunk = getChunk(x, z);
// cancel out if it's already populated
if (chunk.isPopulated()) {
return;
}
// cancel out if the 3x3 around it isn't available
for (int x2 = x - 1; x2 <= x + 1; ++x2) {
for (int z2 = z - 1; z2 <= z + 1; ++z2) {
if (!getChunk(x2, z2).isLoaded() && (!force || !loadChunk(x2, z2, true))) {
return;
}
}
}
// it might have loaded since before, so check again that it's not already populated
if (chunk.isPopulated()) {
return;
}
chunk.setPopulated(true);
Random random = new Random(world.getSeed());
long xrand = (random.nextLong() / 2 << 1) + 1;
long zrand = (random.nextLong() / 2 << 1) + 1;
random.setSeed(x * xrand + z * zrand ^ world.getSeed());
for (BlockPopulator p : world.getPopulators()) {
p.populate(world, random, chunk);
}
EventFactory.callEvent(new ChunkPopulateEvent(chunk));
}
/**
* Force a chunk to be populated by loading the chunks in an area around it. Used when streaming
* chunks to players so that they do not have to watch chunks being populated.
*
* @param x The X coordinate.
* @param z The Z coordinate.
*/
public void forcePopulation(int x, int z) {
try {
populateChunk(x, z, true);
} catch (Throwable ex) {
GlowServer.logger.log(Level.SEVERE,
"Error while populating chunk (" + x + "," + z + ")", ex);
}
}
/**
* Initialize a single chunk from the chunk generator.
*/
private void generateChunk(GlowChunk chunk, int x, int z) {
Random random = new Random(x * 341873128712L + z * 132897987541L);
BiomeGrid biomes = new BiomeGrid();
int[] biomeValues = biomeGrid[0].generateValues(
x * GlowChunk.WIDTH, z * GlowChunk.HEIGHT, GlowChunk.WIDTH, GlowChunk.HEIGHT);
for (int i = 0; i < biomeValues.length; i++) {
biomes.biomes[i] = (byte) biomeValues[i];
}
// extended sections with data
GlowChunkData glowChunkData = null;
if (generator instanceof GlowChunkGenerator) {
glowChunkData = (GlowChunkData)
generator.generateChunkData(world, random, x, z, biomes);
} else {
ChunkGenerator.ChunkData chunkData =
generator.generateChunkData(world, random, x, z, biomes);
if (chunkData != null) {
glowChunkData = new GlowChunkData(world);
for (int i = 0; i < 16; ++i) {
for (int j = 0; j < 16; ++j) {
int maxHeight = chunkData.getMaxHeight();
for (int k = 0; k < maxHeight; ++k) {
MaterialData materialData = chunkData.getTypeAndData(i, k, j);
if (materialData != null) {
glowChunkData.setBlock(i, k, j, materialData);
} else {
glowChunkData.setBlock(i, k, j, new MaterialData(Material.AIR));
}
}
}
}
}
}
if (glowChunkData != null) {
short[][] extSections = glowChunkData.getSections();
if (extSections != null) {
ChunkSection[] sections = new ChunkSection[extSections.length];
for (int i = 0; i < extSections.length; ++i) {
if (extSections[i] != null) {
sections[i] = ChunkSection.fromStateArray(extSections[i]);
}
}
chunk.initializeSections(sections);
chunk.setBiomes(biomes.biomes);
chunk.automaticHeightMap();
return;
}
}
// extended sections
short[][] extSections = generator.generateExtBlockSections(world, random, x, z, biomes);
if (extSections != null) {
ChunkSection[] sections = new ChunkSection[extSections.length];
for (int i = 0; i < extSections.length; ++i) {
if (extSections[i] != null) {
sections[i] = ChunkSection.fromIdArray(extSections[i]);
}
}
chunk.initializeSections(sections);
chunk.setBiomes(biomes.biomes);
chunk.automaticHeightMap();
return;
}
// normal sections
byte[][] blockSections = generator.generateBlockSections(world, random, x, z, biomes);
if (blockSections != null) {
ChunkSection[] sections = new ChunkSection[blockSections.length];
for (int i = 0; i < blockSections.length; ++i) {
if (blockSections[i] != null) {
sections[i] = ChunkSection.fromIdArray(blockSections[i]);
}
}
chunk.initializeSections(sections);
chunk.setBiomes(biomes.biomes);
chunk.automaticHeightMap();
return;
}
// deprecated flat generation
byte[] types = generator.generate(world, random, x, z);
ChunkSection[] sections = new ChunkSection[8];
for (int sy = 0; sy < sections.length; ++sy) {
// We can't use a normal constructor here due to the "interesting"
// choices used for this deprecated API (blocks are in vertical columns)
ChunkSection sec = new ChunkSection();
int by = 16 * sy;
for (int cx = 0; cx < 16; ++cx) {
for (int cz = 0; cz < 16; ++cz) {
for (int cy = by; cy < by + 16; ++cy) {
char type = (char) types[(((cx << 4) + cz) << 7) + cy];
sec.setType(cx, cy, cz, (char) (type << 4));
}
}
}
sections[sy] = sec;
}
chunk.initializeSections(sections);
chunk.setBiomes(biomes.biomes);
chunk.automaticHeightMap();
}
/**
* Forces generation of the given chunk.
*
* @param x The X coordinate.
* @param z The Z coordinate.
* @return Whether the chunk was successfully regenerated.
*/
public boolean forceRegeneration(int x, int z) {
GlowChunk chunk = getChunk(x, z);
if (chunk == null || !chunk.unload(false, false)) {
return false;
}
chunk.setPopulated(false);
try {
generateChunk(chunk, x, z);
populateChunk(x, z, false); // should this be forced?
} catch (Throwable ex) {
GlowServer.logger.log(Level.SEVERE,
"Error while regenerating chunk (" + x + "," + z + ")", ex);
return false;
}
return true;
}
/**
* Gets a list of loaded chunks.
*
* @return The currently loaded chunks.
*/
public GlowChunk[] getLoadedChunks() {
return chunks.values().stream().filter(GlowChunk::isLoaded).toArray(GlowChunk[]::new);
}
/**
* Performs the save for the given chunk using the storage provider.
*
* @param chunk The chunk to save.
* @return True if the save was successful.
*/
public boolean performSave(GlowChunk chunk) {
if (chunk.isLoaded()) {
try {
service.write(chunk);
return true;
} catch (IOException ex) {
GlowServer.logger.log(Level.SEVERE, "Error while saving " + chunk, ex);
return false;
}
}
return false;
}
public int[] getBiomeGridAtLowerRes(int x, int z, int sizeX, int sizeZ) {
return biomeGrid[1].generateValues(x, z, sizeX, sizeZ);
}
public int[] getBiomeGrid(int x, int z, int sizeX, int sizeZ) {
return biomeGrid[0].generateValues(x, z, sizeX, sizeZ);
}
/**
* Indicates that a chunk should be locked. A chunk may be locked multiple times, and will only
* be unloaded when all instances of a lock has been released.
* @param key The chunk's key
*/
private void acquireLock(Key key) {
lockSet.add(key);
}
/**
* Releases one instance of a chunk lock. A chunk may be locked multiple times, and will only be
* unloaded when all instances of a lock has been released.
* @param key The chunk's key
*/
private void releaseLock(Key key) {
lockSet.remove(key);
}
/**
* A group of locks on chunks to prevent them from being unloaded while in use.
*/
public static class ChunkLock implements Iterable<Key> {
private final ChunkManager cm;
private final String desc;
private final Set<Key> keys = new HashSet<>();
public ChunkLock(ChunkManager cm, String desc) {
this.cm = cm;
this.desc = desc;
}
/**
* Acquires a lock on the given chunk key, if it's not already held.
* @param key the key to lock
*/
public void acquire(Key key) {
if (keys.contains(key)) {
return;
}
keys.add(key);
cm.acquireLock(key);
//GlowServer.logger.info(this + " acquires " + key);
}
/**
* Releases a lock on the given chunk key, if it's not already held.
* @param key the key to lock
*/
public void release(Key key) {
if (!keys.contains(key)) {
return;
}
keys.remove(key);
cm.releaseLock(key);
//GlowServer.logger.info(this + " releases " + key);
}
/**
* Release all locks.
*/
public void clear() {
for (Key key : keys) {
cm.releaseLock(key);
//GlowServer.logger.info(this + " clearing " + key);
}
keys.clear();
}
@Override
public String toString() {
return "ChunkLock{" + desc + "}";
}
@Override
public Iterator<Key> iterator() {
return keys.iterator();
}
}
/**
* A BiomeGrid implementation for chunk generation.
*/
private static class BiomeGrid implements ChunkGenerator.BiomeGrid {
private final byte[] biomes = new byte[256];
@Override
public Biome getBiome(int x, int z) {
// upcasting is very important to get extended biomes
return GlowBiome.getBiome(biomes[x | z << 4] & 0xFF);
}
@Override
public void setBiome(int x, int z, Biome bio) {
biomes[x | z << 4] = (byte) GlowBiome.getId(bio);
}
}
}