package net.glowstone; import lombok.Data; import net.glowstone.block.GlowBlock; import net.glowstone.block.GlowBlockState; import net.glowstone.block.ItemTable; import net.glowstone.block.blocktype.BlockType; import net.glowstone.block.entity.TileEntity; import net.glowstone.entity.GlowEntity; import net.glowstone.net.message.play.game.ChunkDataMessage; import net.glowstone.util.NibbleArray; import org.bukkit.Chunk; import org.bukkit.World; import org.bukkit.entity.Entity; import org.bukkit.event.world.ChunkUnloadEvent; import java.util.*; import java.util.logging.Level; /** * Represents a chunk of the map. * @author Graham Edgecombe */ public final class GlowChunk implements Chunk { /** * A chunk key represents the X and Z coordinates of a chunk in a manner * suitable for use as a key in a hash table or set. */ @Data public static final class Key { /** * The coordinates. */ private final int x, z; } /** * The dimensions of a chunk (width: x, height: z, depth: y). */ public static final int WIDTH = 16, HEIGHT = 16, DEPTH = 256; /** * The Y depth of a single chunk section. */ private static final int SEC_DEPTH = 16; /** * A single cubic section of a chunk, with all data. */ public static final class ChunkSection { private static final int ARRAY_SIZE = WIDTH * HEIGHT * SEC_DEPTH; // these probably should be made non-public public final char[] types; public final NibbleArray skyLight; public final NibbleArray blockLight; public int count; // amount of non-air blocks /** * Create a new, empty ChunkSection. */ public ChunkSection() { types = new char[ARRAY_SIZE]; skyLight = new NibbleArray(ARRAY_SIZE); blockLight = new NibbleArray(ARRAY_SIZE); skyLight.fill((byte) 0xf); } /** * Create a ChunkSection with the specified chunk data. This * ChunkSection assumes ownership of the arrays passed in, and they * should not be further modified. */ public ChunkSection(char[] types, NibbleArray skyLight, NibbleArray blockLight) { if (types.length != ARRAY_SIZE || skyLight.size() != ARRAY_SIZE || blockLight.size() != ARRAY_SIZE) { throw new IllegalArgumentException("An array length was not " + ARRAY_SIZE + ": " + types.length + " " + skyLight.size() + " " + blockLight.size()); } this.types = types; this.skyLight = skyLight; this.blockLight = blockLight; recount(); } /** * Calculate the index into internal arrays for the given coordinates. */ public int index(int x, int y, int z) { if (x < 0 || z < 0 || x >= WIDTH || z >= HEIGHT) { throw new IndexOutOfBoundsException("Coords (x=" + x + ",z=" + z + ") out of section bounds"); } return ((y & 0xf) << 8) | (z << 4) | x; } /** * Recount the amount of non-air blocks in the chunk section. */ public void recount() { count = 0; for (char type : types) { if (type != 0) { count++; } } } /** * Take a snapshot of this section which will not reflect future changes. */ public ChunkSection snapshot() { return new ChunkSection(types.clone(), skyLight.snapshot(), blockLight.snapshot()); } } /** * The world of this chunk. */ private final GlowWorld world; /** * The coordinates of this chunk. */ private final int x, z; /** * The array of chunk sections this chunk contains, or null if it is unloaded. */ private ChunkSection[] sections; /** * The array of biomes this chunk contains, or null if it is unloaded. */ private byte[] biomes; /** * The height map values values of each column, or null if it is unloaded. * The height for a column is one plus the y-index of the highest non-air * block in the column. */ private byte[] heightMap; /** * The tile entities that reside in this chunk. */ private final HashMap<Integer, TileEntity> tileEntities = new HashMap<>(); /** * The entities that reside in this chunk. */ private final Set<GlowEntity> entities = new HashSet<>(4); /** * Whether the chunk has been populated by special features. * Used in map generation. */ private boolean populated = false; /** * Creates a new chunk with a specified X and Z coordinate. * @param x The X coordinate. * @param z The Z coordinate. */ GlowChunk(GlowWorld world, int x, int z) { this.world = world; this.x = x; this.z = z; } @Override public String toString() { return "GlowChunk{world=" + world.getName() + ",x=" + x + ",z=" + z + '}'; } // ======== Basic stuff ======== @Override public GlowWorld getWorld() { return world; } @Override public int getX() { return x; } @Override public int getZ() { return z; } @Override public GlowBlock getBlock(int x, int y, int z) { return new GlowBlock(this, (this.x << 4) | (x & 0xf), y & 0xff, (this.z << 4) | (z & 0xf)); } @Override public Entity[] getEntities() { return entities.toArray(new Entity[entities.size()]); } public Collection<GlowEntity> getRawEntities() { return entities; } @Override public GlowBlockState[] getTileEntities() { List<GlowBlockState> states = new ArrayList<>(tileEntities.size()); for (TileEntity tileEntity : tileEntities.values()) { GlowBlockState state = tileEntity.getState(); if (state != null) { states.add(state); } } return states.toArray(new GlowBlockState[states.size()]); } public Collection<TileEntity> getRawTileEntities() { return Collections.unmodifiableCollection(tileEntities.values()); } @Override public GlowChunkSnapshot getChunkSnapshot() { return getChunkSnapshot(true, false, false); } @Override public GlowChunkSnapshot getChunkSnapshot(boolean includeMaxBlockY, boolean includeBiome, boolean includeBiomeTempRain) { return new GlowChunkSnapshot(x, z, world, sections, includeMaxBlockY ? heightMap.clone() : null, includeBiome ? biomes.clone() : null, includeBiomeTempRain); } /** * Gets whether this chunk has been populated by special features. * @return Population status. */ public boolean isPopulated() { return populated; } /** * Sets the population status of this chunk. * @param populated Population status. */ public void setPopulated(boolean populated) { this.populated = populated; } // ======== Helper Functions ======== @Override public boolean isLoaded() { return sections != null; } @Override public boolean load() { return load(true); } @Override public boolean load(boolean generate) { return isLoaded() || world.getChunkManager().loadChunk(x, z, generate); } @Override public boolean unload() { return unload(true, true); } @Override public boolean unload(boolean save) { return unload(save, true); } @Override public boolean unload(boolean save, boolean safe) { if (!isLoaded()) { return true; } if (safe && world.isChunkInUse(x, z)) { return false; } if (save && !world.getChunkManager().performSave(this)) { return false; } if (EventFactory.callEvent(new ChunkUnloadEvent(this)).isCancelled()) { return false; } sections = null; biomes = null; tileEntities.clear(); return true; } /** * Initialize this chunk from the given sections. * @param initSections The ChunkSections to use. */ public void initializeSections(ChunkSection[] initSections) { if (isLoaded()) { GlowServer.logger.log(Level.SEVERE, "Tried to initialize already loaded chunk (" + x + "," + z + ")", new Throwable()); return; } //GlowServer.logger.log(Level.INFO, "Initializing chunk ({0},{1})", new Object[]{x, z}); sections = new ChunkSection[DEPTH / SEC_DEPTH]; System.arraycopy(initSections, 0, sections, 0, Math.min(sections.length, initSections.length)); biomes = new byte[WIDTH * HEIGHT]; heightMap = new byte[WIDTH * HEIGHT]; // tile entity initialization for (int i = 0; i < sections.length; ++i) { if (sections[i] == null) continue; int by = 16 * i; for (int cx = 0; cx < WIDTH; ++cx) { for (int cz = 0; cz < HEIGHT; ++cz) { for (int cy = by; cy < by + 16; ++cy) { createEntity(cx, cy, cz, getType(cx, cz, cy)); } } } } } /** * If needed, create a new tile entity at the given location. */ private void createEntity(int cx, int cy, int cz, int type) { BlockType blockType = ItemTable.instance().getBlock(type); if (blockType == null) return; try { TileEntity entity = blockType.createTileEntity(this, cx, cy, cz); if (entity == null) return; tileEntities.put(coordToIndex(cx, cz, cy), entity); } catch (Exception ex) { GlowServer.logger.log(Level.SEVERE, "Unable to initialize tile entity for " + type, ex); } } // ======== Data access ======== /** * Attempt to get the ChunkSection at the specified height. * @param y the y value. * @return The ChunkSection, or null if it is empty. */ private ChunkSection getSection(int y) { int idx = y >> 4; if (y < 0 || y >= DEPTH || !load() || idx >= sections.length) { return null; } return sections[idx]; } /** * Get all ChunkSection of this chunk. * @return The chunk sections array. */ public ChunkSection[] getSections() { return sections; } /** * Attempt to get the tile entity located at the given coordinates. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @return A GlowBlockState if the entity exists, or null otherwise. */ public TileEntity getEntity(int x, int y, int z) { if (y >= DEPTH || y < 0) return null; load(); return tileEntities.get(coordToIndex(x, z, y)); } /** * Gets the type of a block within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @return The type. */ public int getType(int x, int z, int y) { ChunkSection section = getSection(y); return section == null ? 0 : (section.types[section.index(x, y, z)] >> 4); } /** * Sets the type of a block within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @param type The type. */ public void setType(int x, int z, int y, int type) { if (type < 0 || type > 0xfff) throw new IllegalArgumentException("Block type out of range: " + type); ChunkSection section = getSection(y); if (section == null) { if (type == 0) { // don't need to create chunk for air return; } else { // create new ChunkSection for this y coordinate int idx = y >> 4; if (y < 0 || y >= DEPTH || idx >= sections.length) { // y is out of range somehow return; } sections[idx] = section = new ChunkSection(); } } // destroy any tile entity there int tileEntityIndex = coordToIndex(x, z, y); if (tileEntities.containsKey(tileEntityIndex)) { tileEntities.remove(tileEntityIndex).destroy(); } // update the air count and height map int index = section.index(x, y, z); int heightIndex = z * WIDTH + x; if (type == 0) { if (section.types[index] != 0) { section.count--; } if (heightMap[heightIndex] == y + 1) { // erased just below old height map -> lower heightMap[heightIndex] = (byte) lowerHeightMap(x, y, z); } } else { if (section.types[index] == 0) { section.count++; } if (heightMap[heightIndex] <= y) { // placed between old height map and top -> raise heightMap[heightIndex] = (byte) Math.min(y + 1, 255); } } // update the type - also sets metadata to 0 section.types[index] = (char) (type << 4); if (type == 0 && section.count == 0) { // destroy the empty section sections[y / SEC_DEPTH] = null; return; } // create a new tile entity if we need createEntity(x, y, z, type); } /** * Scan downwards to determine the new height map value. */ private int lowerHeightMap(int x, int y, int z) { for (--y; y >= 0; --y) { if (getType(x, z, y) != 0) { break; } } return y + 1; } /** * Gets the metadata of a block within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @return The metadata. */ public int getMetaData(int x, int z, int y) { ChunkSection section = getSection(y); return section == null ? 0 : section.types[section.index(x, y, z)] & 0xF; } /** * Sets the metadata of a block within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @param metaData The metadata. */ public void setMetaData(int x, int z, int y, int metaData) { if (metaData < 0 || metaData >= 16) throw new IllegalArgumentException("Metadata out of range: " + metaData); ChunkSection section = getSection(y); if (section == null) return; // can't set metadata on an empty section int index = section.index(x, y, z); int type = section.types[index]; if (type == 0) return; // can't set metadata on air section.types[index] = (char) ((type & 0xfff0) | metaData); } /** * Gets the sky light level of a block within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @return The sky light level. */ public byte getSkyLight(int x, int z, int y) { ChunkSection section = getSection(y); return section == null ? 0 : section.skyLight.get(section.index(x, y, z)); } /** * Sets the sky light level of a block within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @param skyLight The sky light level. */ public void setSkyLight(int x, int z, int y, int skyLight) { ChunkSection section = getSection(y); if (section == null) return; // can't set light on an empty section section.skyLight.set(section.index(x, y, z), (byte) skyLight); } /** * Gets the block light level of a block within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @return The block light level. */ public byte getBlockLight(int x, int z, int y) { ChunkSection section = getSection(y); return section == null ? 0 : section.blockLight.get(section.index(x, y, z)); } /** * Sets the block light level of a block within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @param blockLight The block light level. */ public void setBlockLight(int x, int z, int y, int blockLight) { ChunkSection section = getSection(y); if (section == null) return; // can't set light on an empty section section.blockLight.set(section.index(x, y, z), (byte) blockLight); } /** * Gets the biome of a column within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @return The biome. */ public int getBiome(int x, int z) { if (biomes == null && !load()) return 0; return biomes[z * WIDTH + x] & 0xFF; } /** * Sets the biome of a column within this chunk, * @param x The X coordinate. * @param z The Z coordinate. * @param biome The biome. */ public void setBiome(int x, int z, int biome) { if (biomes == null) return; biomes[z * WIDTH + x] = (byte) biome; } /** * Set the entire biome array of this chunk. * @param newBiomes The biome array. */ public void setBiomes(byte[] newBiomes) { if (biomes == null) { throw new IllegalStateException("Must initialize chunk first"); } if (newBiomes.length != biomes.length) { throw new IllegalArgumentException("Biomes array not of length " + biomes.length); } System.arraycopy(newBiomes, 0, biomes, 0, biomes.length); } /** * Get the height map value of a column within this chunk. * @param x The X coordinate. * @param z The Z coordinate. * @return The height map value. */ public int getHeight(int x, int z) { if (heightMap == null && !load()) return 0; return heightMap[z * WIDTH + x] & 0xff; } /** * Set the entire height map of this chunk. * @param newHeightMap The height map. */ public void setHeightMap(int[] newHeightMap) { if (heightMap == null) { throw new IllegalStateException("Must initialize chunk first"); } if (newHeightMap.length != heightMap.length) { throw new IllegalArgumentException("Height map not of length " + heightMap.length); } for (int i = 0; i < heightMap.length; ++i) { heightMap[i] = (byte) newHeightMap[i]; } } /** * Automatically fill the height map after chunks have been initialized. */ public void automaticHeightMap() { // determine max Y chunk section at a time int sy = sections.length - 1; for (; sy >= 0; --sy) { if (sections[sy] != null) { break; } } int y = (sy + 1) * 16; for (int x = 0; x < WIDTH; ++x) { for (int z = 0; z < HEIGHT; ++z) { heightMap[z * WIDTH + x] = (byte) lowerHeightMap(x, y, z); } } } // ======== Helper functions ======== /** * Converts a three-dimensional coordinate to an index within the * one-dimensional arrays. * @param x The X coordinate. * @param z The Z coordinate. * @param y The Y coordinate. * @return The index within the arrays. */ private int coordToIndex(int x, int z, int y) { if (x < 0 || z < 0 || y < 0 || x >= WIDTH || z >= HEIGHT || y >= DEPTH) throw new IndexOutOfBoundsException("Coords (x=" + x + ",y=" + y + ",z=" + z + ") invalid"); return (y * HEIGHT + z) * WIDTH + x; } /** * Creates a new {@link ChunkDataMessage} which can be sent to a client to stream * this entire chunk to them. * @return The {@link ChunkDataMessage}. */ public ChunkDataMessage toMessage() { // this may need to be changed to "true" depending on resolution of // some inconsistencies on the wiki return toMessage(world.getEnvironment() == World.Environment.NORMAL); } /** * Creates a new {@link ChunkDataMessage} which can be sent to a client to stream * this entire chunk to them. * @param skylight Whether to include skylight data. * @return The {@link ChunkDataMessage}. */ public ChunkDataMessage toMessage(boolean skylight) { return toMessage(skylight, true, 0); } /** * Creates a new {@link ChunkDataMessage} which can be sent to a client to stream * parts of this chunk to them. * @return The {@link ChunkDataMessage}. */ public ChunkDataMessage toMessage(boolean skylight, boolean entireChunk, int sectionBitmask) { load(); // filter sectionBitmask based on actual chunk contents int sectionCount; if (sections == null) { sectionBitmask = 0; sectionCount = 0; } else { final int maxBitmask = (1 << sections.length) - 1; if (entireChunk) { sectionBitmask = maxBitmask; sectionCount = sections.length; } else { sectionBitmask &= maxBitmask; sectionCount = countBits(sectionBitmask); } for (int i = 0; i < sections.length; ++i) { if (sections[i] == null || sections[i].count == 0) { // remove empty sections from bitmask sectionBitmask &= ~(1 << i); sectionCount--; } } } // calculate how big the data will need to be int byteSize = 0; if (sections != null) { final int numBlocks = WIDTH * HEIGHT * SEC_DEPTH; int sectionSize = numBlocks * 5 / 2; // (data and metadata combo) * 2 + blockLight/2 if (skylight) { sectionSize += numBlocks / 2; // + skyLight/2 } byteSize += sectionCount * sectionSize; } if (entireChunk) { byteSize += 256; // + biomes } byte[] tileData = new byte[byteSize]; int pos = 0; if (sections != null) { // get the list of sections ChunkSection[] sendSections = new ChunkSection[sectionCount]; for (int i = 0, j = 0, mask = 1; i < sections.length; ++i, mask <<= 1) { if ((sectionBitmask & mask) != 0) { sendSections[j++] = sections[i]; } } for (ChunkSection sec : sendSections) { for (char t : sec.types) { tileData[pos++] = (byte) (t & 0xff); tileData[pos++] = (byte) (t >> 8); } } for (ChunkSection sec : sendSections) { byte[] blockLight = sec.blockLight.getRawData(); System.arraycopy(blockLight, 0, tileData, pos, blockLight.length); pos += blockLight.length; } if (skylight) { for (ChunkSection sec : sendSections) { byte[] skyLight = sec.skyLight.getRawData(); System.arraycopy(skyLight, 0, tileData, pos, skyLight.length); pos += skyLight.length; } } } // biomes if (entireChunk) { for (int i = 0; i < 256; ++i) { tileData[pos++] = biomes[i]; } } if (pos != byteSize) { throw new IllegalStateException("only wrote " + pos + " out of expected " + byteSize + " bytes"); } return new ChunkDataMessage(x, z, entireChunk, sectionBitmask, tileData); } private int countBits(int v) { // http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan int c; for (c = 0; v > 0; c++) { v &= v - 1; } return c; } }