/*
* 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.world.column;
import com.google.common.base.Predicate;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.Entity;
import net.minecraft.network.PacketBuffer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.ClassInheritanceMultiMap;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.EnumSkyBlock;
import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.IChunkGenerator;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.world.ChunkEvent;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import cubicchunks.lighting.LightingManager;
import cubicchunks.util.Coords;
import cubicchunks.util.MathUtil;
import cubicchunks.world.ClientHeightMap;
import cubicchunks.world.ICubeProvider;
import cubicchunks.world.ICubicWorld;
import cubicchunks.world.IHeightMap;
import cubicchunks.world.ServerHeightMap;
import cubicchunks.world.cube.Cube;
/**
* A quasi-chunk, representing an infinitely tall section of the world.
*/
public class Column extends Chunk {
private CubeMap cubeMap;
private IHeightMap opacityIndex;
private ICubeProvider provider;
private ICubicWorld world;
public Column(ICubeProvider provider, ICubicWorld world, int x, int z) {
// NOTE: this constructor is called by the chunk loader
super((World) world, x, z);
this.provider = provider;
this.world = world;
init();
}
//=================================================
//===============VANILLA METHODS===================
//=================================================
/**
* Return Y position of the block directly above the top non-transparent block, or {@link Coords#NO_HEIGHT} + 1 if
* there are no non-transparent blocks
*/
@Override
public int getHeight(BlockPos pos) {
return this.getHeightValue(
Coords.blockToLocal(pos.getX()),
Coords.blockToLocal(pos.getZ()));
}
/**
* Return Y position of the block directly above the top non-transparent block, or {@link Coords#NO_HEIGHT} + 1 if
* there are no non-transparent blocks
*/
@Override
public int getHeightValue(int localX, int localZ) {
// NOTE: the "height value" here is the height of the transparent block
// on top of the highest non-transparent block
return opacityIndex.getTopBlockY(localX, localZ) + 1;
}
@Override
@Deprecated
//TODO: stop this method form being used by vanilla (any algorithms in vanilla that use it are be broken any way)
// don't use this! It's only here because vanilla needs it
public int getTopFilledSegment() {
//NOTE: this method actually returns block Y coords
// PANIC!
// this column doesn't have any blocks in it that aren't air!
// but we can't return null here because vanilla code expects there to be a surface down there somewhere
// we don't actually know where the surface is yet, because maybe it hasn't been generated
// but we do know that the surface has to be at least at sea level,
// so let's go with that for now and hope for the best
// old solution
// return this.getCubicWorld().provider.getAverageGroundLevel();
int blockY = Coords.NO_HEIGHT;
for (int localX = 0; localX < Cube.SIZE; localX++) {
for (int localZ = 0; localZ < Cube.SIZE; localZ++) {
int y = this.opacityIndex.getTopBlockY(localX, localZ);
if (y > blockY) {
blockY = y;
}
}
}
return Coords.cubeToMinBlock(Coords.blockToCube(blockY)); // return the lowest block in the Cube (kinda weird I know)
}
@Override
@Deprecated // Vanilla can safely use this for block ticking, but just try to avoid it!
public ExtendedBlockStorage[] getBlockStorageArray() {
return cubeMap.getStoragesToTick();
}
@SideOnly(Side.CLIENT)
protected void generateHeightMap() {
//this method reduces to no-op with CubicChunks, heightmap is generated in real time
}
@Override
@Deprecated
public void generateSkylightMap() {
throw new UnsupportedOperationException("Functionality of this method is replaced with LightingManager");
}
/**
* Retrieve the block state at the specified location
*
* @param pos target location
*
* @return The block state
*
* @see Column#getBlockState(int, int, int)
* @see Cube#getBlockState(BlockPos)
*/
@Override
public IBlockState getBlockState(BlockPos pos) {
//forward to cube
return this.getCube(pos).getBlockState(pos);
}
/**
* Retrieve the block state at the specified location
*
* @param blockX block x position
* @param blockY block y position
* @param blockZ block z position
*
* @return The block state
*
* @see Column#getBlockState(BlockPos)
* @see Cube#getBlockState(int, int, int)
*/
@Override
public IBlockState getBlockState(final int blockX, final int blockY, final int blockZ) {
//forward to cube
return this.getCube(Coords.blockToLocal(blockY)).getBlockState(blockX, blockY, blockZ);
}
/**
* Set the block state at the specified location
*
* @param pos target location
* @param newstate target state of the block at that position
*
* @return The the old state of the block at the position, or null if there was no change
*/
@Override
public IBlockState setBlockState(BlockPos pos, @Nonnull IBlockState newstate) {
Cube cube = getCube(Coords.blockToCube(pos.getY()));
IBlockState oldstate = cube.getBlockState(pos);
// get the old opacity for use when updating the heightmap
int oldOpacity = oldstate.getLightOpacity(this.getWorld(), pos);
oldstate = cube.setBlockStateDirect(pos, newstate); // forward to cube
if (oldstate == null) {
// Nothing changed
return null;
}
this.doOnBlockSetLightUpdates(pos, newstate, oldOpacity);
return oldstate;
}
/**
* Update lighting at the specified location after a block was changed
*
* @param pos target location
* @param newBlockState the new state of that block
* @param oldOpacity the original opacity of that block
*/
//TODO: This looks ugly idk
private void doOnBlockSetLightUpdates(BlockPos pos, IBlockState newBlockState, int oldOpacity) {
int newOpacity = newBlockState.getLightOpacity(this.getWorld(), pos);
if (oldOpacity == newOpacity || (oldOpacity >= 15 && newOpacity >= 15)) {
//nothing to update, this will frequently happen in ore generation
return;
}
int localX = Coords.blockToLocal(pos.getX());
int localZ = Coords.blockToLocal(pos.getZ());
// did the top non-transparent block change?
int oldSkylightY = getHeightValue(localX, localZ);
this.opacityIndex.onOpacityChange(localX, pos.getY(), localZ, newOpacity);
setModified(true);
int newSkylightY = oldSkylightY;
if (!getWorld().isRemote) {
newSkylightY = getHeightValue(localX, localZ);
//if oldSkylightY == null and newOpacity == 0 then we didn't change anything
} else if (!(oldSkylightY < world.getMinHeight() && newOpacity == 0)) {
int oldSkylightActual = oldSkylightY - 1;
//to avoid unnecessary delay when breaking blocks we need to hack it clientside
if ((pos.getY() > oldSkylightActual - 1) && newOpacity != 0) {
//we added block, so we can be sure it's correct. Server update will be ignored
newSkylightY = pos.getY() + 1;
} else if (newOpacity == 0 && pos.getY() == oldSkylightY - 1) {
//we changed block to something transparent. Heightmap can change only if we break top block
//we don't know by how much we changed heightmap, and we could have changed it by any value
//but for client code any value higher than render distance means the same
//we need to update it enough not to be unresponsive, and then wait for information from server
//so only scan 64 blocks down. If we updated more - we would need to wait for renderer updates anyway
int newTop = oldSkylightActual - 1;
while (getBlockLightOpacity(new BlockPos(localX, newTop, localZ)) == 0 &&
newTop > oldSkylightActual - 65) {
newTop--;
}
newSkylightY = newTop;
} else {
// no change
newSkylightY = oldSkylightActual;
}
//update the heightmap. If out update it not accurate - it will be corrected when server sends block update
((ClientHeightMap) opacityIndex).setHeight(localX, localZ, newSkylightY);
}
int minY = MathUtil.minInteger(oldSkylightY, newSkylightY);
int maxY = MathUtil.maxInteger(oldSkylightY, newSkylightY);
if (minY > maxY) {
int t = minY;
minY = maxY;
maxY = t;
}
LightingManager lightManager = this.world.getLightingManager();
lightManager.columnSkylightUpdate(LightingManager.UpdateType.IMMEDIATE, this, localX, minY, maxY, localZ);
}
/**
* Retrieve the raw light level at the specified location
*
* @param type The type of light (sky or block light)
* @param pos The position at which light should be checked
*
* @return the light level
*
* @see Cube#getLightFor(EnumSkyBlock, BlockPos)
*/
@Override
public int getLightFor(@Nonnull EnumSkyBlock type, BlockPos pos) {
//forward to cube
return getCube(pos).getLightFor(type, pos);
}
/**
* Set the raw light level at the specified location
*
* @param type The type of light (sky or block light)
* @param pos The position at which light should be updated
* @param value the light level
*
* @see Cube#setLightFor(EnumSkyBlock, BlockPos, int)
*/
@Override
public void setLightFor(EnumSkyBlock type, BlockPos pos, int value) {
//forward to cube
getCube(pos).setLightFor(type, pos, value);
}
/**
* Retrieve actual light level at the specified location. This is the brightest of all types of light affecting this
* block
*
* @param pos the target position
* @param amount skylight falloff factor
*
* @return actual light level at this location
*
* @see Cube#getLightSubtracted(BlockPos, int)
*/
@Override
public int getLightSubtracted(BlockPos pos, int amount) {
//forward to cube
return getCube(pos).getLightSubtracted(pos, amount);
}
/**
* Add an entity to this column
*
* @param entity entity to add
*
* @see Cube#addEntity(Entity)
*/
@Override
public void addEntity(Entity entity) {
//forward to cube
getCube(Coords.getCubeYForEntity(entity)).addEntity(entity);
}
/**
* Remove an entity from this column
*
* @param entityIn The entity to remove
*
* @see Column#removeEntityAtIndex(Entity, int)
* @see Cube#removeEntity(Entity)
*/
@Override
public void removeEntity(Entity entityIn) {
this.removeEntityAtIndex(entityIn, entityIn.chunkCoordY);
}
/**
* Remove an entity from the cube at the specified location
*
* @param entity The entity to remove
* @param cubeY cube y location
*
* @see Cube#removeEntity(Entity)
*/
@Override
public void removeEntityAtIndex(@Nonnull Entity entity, int cubeY) {
//forward to cube
getCube(cubeY).removeEntity(entity);
}
/**
* Check whether the block at the specified location has a clear line of view towards the sky
*
* @param pos target location
*
* @return <code>true</code> if there is no block between this block and the sky, <code>false</code> otherwise
*/
@Override
public boolean canSeeSky(BlockPos pos) {
int height = this.getHeightValue(
Coords.blockToLocal(pos.getX()),
Coords.blockToLocal(pos.getZ()));
return pos.getY() >= height;
}
/**
* Retrieve the tile entity at the specified location
*
* @param pos target location
* @param createType how fast the tile entity is needed
*
* @return the tile entity at the specified location, or <code>null</code> if there is no entity and
* <code>createType</code> was not {@link net.minecraft.world.chunk.Chunk.EnumCreateEntityType#IMMEDIATE}
*
* @see Cube#getTileEntity(BlockPos, EnumCreateEntityType)
*/
@Override
public TileEntity getTileEntity(@Nonnull BlockPos pos, Chunk.EnumCreateEntityType createType) {
//forward to cube
return getCube(pos).getTileEntity(pos, createType);
}
/**
* Add a tile entity to this column
*
* @param tileEntity The tile entity to add
*
* @see Cube#addTileEntity(TileEntity)
*/
@Override
public void addTileEntity(TileEntity tileEntity) {
// pass off to the cube
getCube(tileEntity.getPos()).addTileEntity(tileEntity);
}
/**
* Add a tile entity to this column at the specified location
*
* @param pos The target location
* @param blockEntity The tile entity to add
*
* @see Cube#addTileEntity(BlockPos, TileEntity)
*/
@Override
public void addTileEntity(@Nonnull BlockPos pos, TileEntity blockEntity) {
// pass off to the cube
getCube(pos).addTileEntity(pos, blockEntity);
}
/**
* Remove the tile entity at the specified location
*
* @param pos target location
*/
@Override
public void removeTileEntity(@Nonnull BlockPos pos) {
//forward to cube
this.getCube(pos).removeTileEntity(pos);
}
/**
* Called when this column is finished loading
*/
@Override
public void onChunkLoad() {
this.isChunkLoaded = true;
MinecraftForge.EVENT_BUS.post(new ChunkEvent.Load(this));
}
/**
* Called when this column is being unloaded
*/
@Override
public void onChunkUnload() {
this.isChunkLoaded = false;
MinecraftForge.EVENT_BUS.post(new ChunkEvent.Unload(this));
}
//setChunkModified() goes here, it's unchanged
/**
* Retrieve all matching entities within a specific area of the world
*
* @param exclude don't include this entity in the results
* @param queryBox section of the world being checked
* @param out list to which found entities should be added
* @param predicate filter to match entities against
*/
@Override
public void getEntitiesWithinAABBForEntity(Entity exclude, AxisAlignedBB queryBox, @Nonnull List<Entity> out, Predicate<? super Entity> predicate) {
// get a y-range that 2 blocks wider than the box for safety
int minCubeY = Coords.blockToCube(MathHelper.floor(queryBox.minY - World.MAX_ENTITY_RADIUS));
int maxCubeY = Coords.blockToCube(MathHelper.floor(queryBox.maxY + World.MAX_ENTITY_RADIUS));
for (int cubeY = minCubeY; cubeY <= maxCubeY; cubeY++) {
Cube cube = getCube(cubeY);
cube.getEntitiesWithinAABBForEntity(exclude, queryBox, out, predicate);
}
}
/**
* Retrieve all matching entities of the specified type within a specific area of the world
*
* @param entityType the type of entity to retrieve
* @param queryBox section of the world being checked
* @param out list to which found entities should be added
* @param predicate filter to match entities against
* @param <T> type parameter for the class of entities being searched for
*/
@Override
public <T extends Entity> void getEntitiesOfTypeWithinAAAB(@Nonnull Class<? extends T> entityType, AxisAlignedBB queryBox, @Nonnull List<T> out, Predicate<? super T> predicate) {
// get a y-range that 2 blocks wider than the box for safety
int minCubeY = Coords.blockToCube(MathHelper.floor(queryBox.minY - World.MAX_ENTITY_RADIUS));
int maxCubeY = Coords.blockToCube(MathHelper.floor(queryBox.maxY + World.MAX_ENTITY_RADIUS));
for (int cubeY = minCubeY; cubeY < maxCubeY + 1; cubeY++) {
Cube cube = getCube(cubeY);
cube.getEntitiesOfTypeWithinAAAB(entityType, queryBox, out, predicate);
}
}
/**
* Check whether this column needs to be written back to disk for persistence
*
* @param flag unused
*
* @return <code>true</code> if there were modifications since the time this column was loaded from disk,
* <code>false</code> otherwise
*/
@Override
public boolean needsSaving(boolean flag) {
return this.isModified;
}
//getRandomWithSeed(seed) doesn't need changes
//isEmpty() doesn't need changes
@Override
@Deprecated
public void populateChunk(IChunkProvider chunkProvider, @Nonnull IChunkGenerator chunkGenerator) {
throw new UnsupportedOperationException("This method is incompatible with CubicChunks");
}
/**
* Retrieve lowest block still affected by rain and snow
*
* @param pos The target block column to check
*
* @return The lowest block at the same x and z coordinates that is still hit by rain and snow
*/
@Override
//TODO: Actual precipitation heightmap, currently skylight heightmap is used which triggers an old MC alpha bug
public BlockPos getPrecipitationHeight(BlockPos pos) {
return new BlockPos(pos.getX(), this.getHeight(pos), pos.getZ());
}
/**
* Tick this column
*
* @param tryToTickFaster Whether costly calculations should be skipped in order to catch up with ticks
*/
@Override
public void onTick(boolean tryToTickFaster) {
this.chunkTicked = true;
cubeMap.forEach((c) -> c.tickCube(tryToTickFaster));
}
@Override
@Deprecated
public boolean isPopulated() {
return true; //stub... its broken and only used in World.markAndNotifyBlock()
}
//isCHunkTicked() doesn't need changes
//getChunkCoordIntPair doesn't need changes
/**
* See if there is any blocks in the specified section of the world. Note that while parameters are block
* coordinates, the check is actually aligned to cubes.
*
* @param minBlockY Lower end of the section being checked
* @param maxBlockY Upper end of the section being checked
*
* @return <code>true</code> if there is only air blocks in the checked cubes, <code>false</code> otherwise
*/
@Override
// used for by ChunkCache, and that is used for rendering to see
// if there are any blocks, or is there just air
public boolean getAreLevelsEmpty(int minBlockY, int maxBlockY) {
int minCubeY = Coords.blockToCube(minBlockY);
int maxCubeY = Coords.blockToCube(maxBlockY);
for (int cubeY = minCubeY; cubeY <= maxCubeY; cubeY++) {
Cube cube = getCube(cubeY); // yes, load/generate a chunk if there is none...
// even if its not loaded there is still technical something there
if (!cube.isEmpty()) {
return false;
}
}
return true;
}
@Override
@Deprecated
public void setStorageArrays(ExtendedBlockStorage[] newArray) {
throw new UnsupportedOperationException("This method is incompatible with Cubic Chunks");
}
@Override
@Deprecated
@SideOnly(Side.CLIENT)
public void fillChunk(@Nonnull PacketBuffer buf, int p_186033_2_, boolean p_186033_3_) {
throw new UnsupportedOperationException("This method is incompatible with Cubic Chunks");
}
//getBiome doesn't need changes
//getBiomeArray doesn't need changes
//setBiomeArray doesn't need changes
// TODO: lighting should not be done in Column
@Override
@Deprecated
public void resetRelightChecks() {
throw new UnsupportedOperationException("This method is incompatible with Cubic Chunks");
}
//TODO: enqueueRelightChecks() must die! (it should be in its own lighting system or at least only in Cube)
@Override
@Deprecated
public void enqueueRelightChecks() {
// stub
}
@Override
@Deprecated
public void checkLight() {
//no-op on cubic chunks
}
//isLoaded doesn't need changes
//getWorld doesn't need changes
@Override
@Deprecated // TODO: only used by IONbtReader and IONbtWriter ... and those are super broken
// add throw new UnsupportedOperationException();
public int[] getHeightMap() {
return this.opacityIndex.getHeightmap();
}
@Override
@Deprecated
public void setHeightMap(int[] newHeightMap) {
throw new UnsupportedOperationException();
}
/**
* Retrieve a map of all tile entities in this column and their locations
*
* @return A map, mapping positions to tile entities at that position
*/
@Override
public Map<BlockPos, TileEntity> getTileEntityMap() {
//TODO: Important: Fix getTileEntityMap. Need to implement special Map that accesses tile entities from cubeMap
return super.getTileEntityMap();
}
/**
* Retrieve a list of all entities in this column
*
* @return the list of entities
*/
@Override
public ClassInheritanceMultiMap<Entity>[] getEntityLists() {
//TODO: need to make it returns something that contains correct data
//Forge needs it and editing Forge classes with ASM is a bad idea
return super.getEntityLists();
}
@Override
@Deprecated //TODO: only used in PlayerCubeMap.getChunkIterator() (see todo's in that)
// add throw new UnsupportedOperationException();
public boolean isTerrainPopulated() {
//with cubic chunks the whole column is never fully generated,
//this method is currently used to determine list of chunks to be ticked
//and PlayerCubeMap :(
return true; //TODO: stub, replace with new UnsupportedOperationException();
}
@Override
@Deprecated
public boolean isLightPopulated() {
//with cubic chunks light is never generated in the whole column
//this method is currently used to determine list of chunks to be ticked
//and PlayerCubeMap :(
return true; //TODO: stub, replace with new UnsupportedOperationException();
}
/**
* Check if this column needs to be ticked
*
* @return <code>true</code> if any cube in this column needs to be ticked, <code>false</code> otherwise
*/
public boolean shouldTick() {
for (Cube cube : cubeMap) {
if (cube.getTickets().shouldTick()) {
return true;
}
}
return false;
}
@Override
@Deprecated
public void removeInvalidTileEntity(@Nonnull BlockPos pos) {
throw new UnsupportedOperationException("Not implemented because not used");
}
//===========================================
//===========CubicChunks methods=============
//===========================================
private void init() {
this.cubeMap = new CubeMap();
//clientside we don't really need that much data. we actually only need top and bottom block Y positions
if (this.getWorld().isRemote) {
this.opacityIndex = new ClientHeightMap(this);
} else {
this.opacityIndex = new ServerHeightMap();
}
// make sure no one's using data structures that have been replaced
// also saves memory
/*
* TODO: setting these vars to null would save memory, also... make sure we're actually
* not using them
*/
// this.chunkSections = null;
// this.heightMap = null;
// this.skylightUpdateMap = null;
Arrays.fill(super.getBiomeArray(), (byte) -1);
}
/**
* @return x position of this column
*/
public int getX() {
return this.xPosition;
}
/**
* @return z position of this column
*/
public int getZ() {
return this.zPosition;
}
/**
* @return the height map of this column
*/
public IHeightMap getOpacityIndex() {
return this.opacityIndex;
}
/**
* Retrieve all cubes in this column that are currently loaded
*
* @return the cubes
*/
public Collection<Cube> getLoadedCubes() {
return this.cubeMap.all();
}
// =========================================
// =======Mini CubeCache like methods=======
// =========================================
/**
* Iterate over all loaded cubes in this column in order. If <code>startY < endY</code>, order is bottom to top,
* otherwise order is top to bottom.
*
* @param startY initial cube y position
* @param endY last cube y position
*
* @return an iterator over all loaded cubes between <code>startY</code> and <code>endY</code>
*/
public Iterable<Cube> getLoadedCubes(int startY, int endY) {
return this.cubeMap.cubes(startY, endY);
}
/**
* Retrieve the cube at the specified location if it is loaded.
*
* @param cubeY cube y position
*
* @return the cube at that position, or <code>null</code> if it is not loaded
*/
@Nullable
public Cube getLoadedCube(int cubeY) {
return provider.getLoadedCube(getX(), cubeY, getZ());
}
/**
* Retrieve the cube at the specified location
*
* @param cubeY cube y position
*
* @return the cube at that position
*/
public Cube getCube(int cubeY) {
return provider.getCube(getX(), cubeY, getZ());
}
/**
* Retrieve the cube containing the specified block
*
* @param pos the target block position
*
* @return the cube containing that block
*/
private Cube getCube(BlockPos pos) {
return getCube(Coords.blockToCube(pos.getY()));
}
/**
* Add a cube to this column
*
* @param cube the cube being added
*/
public void addCube(Cube cube) {
this.cubeMap.put(cube);
}
/**
* Remove the cube at the specified height
*
* @param cubeY cube y position
*
* @return the removed cube if it existed, otherwise <code>null</code>
*/
public Cube removeCube(int cubeY) {
return this.cubeMap.remove(cubeY);
}
/**
* Check if there are any loaded cube in this column
*
* @return <code>true</code> if there is at least on loaded cube in this column, <code>false</code> otherwise
*/
public boolean hasLoadedCubes() {
return !this.cubeMap.isEmpty();
}
// ======= end cube cache like methods =======
// ===========================================
/**
* Notify this column that it has been saved
*/
public void markSaved() {
this.setModified(false);
}
/**
* Among all top blocks in this column, return the height of the lowest one
*
* @return the height of the lowest top block
*/
@Override
public int getLowestHeight() {
return opacityIndex.getLowestTopBlockY();
}
/**
* Retrieve the world to which this column belongs
*
* @return the world
*/
public ICubicWorld getCubicWorld() {
return world;
}
}