/* Copyright (c) 2010-2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.world; import se.llbit.chunky.map.AbstractLayer; import se.llbit.chunky.map.BiomeLayer; import se.llbit.chunky.map.BlockLayer; import se.llbit.chunky.map.CaveLayer; import se.llbit.chunky.map.CorruptLayer; import se.llbit.chunky.map.MapTile; import se.llbit.chunky.map.SurfaceLayer; import se.llbit.chunky.map.UnknownLayer; import se.llbit.chunky.map.WorldMapLoader; import se.llbit.chunky.ui.MapViewMode; import se.llbit.nbt.Tag; import se.llbit.nbt.CompoundTag; import se.llbit.nbt.ErrorTag; import se.llbit.nbt.ListTag; import se.llbit.nbt.NamedTag; import se.llbit.nbt.SpecificTag; import se.llbit.util.NotNull; import java.io.DataInputStream; import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * This class represents a loaded or not-yet-loaded chunk in the world. * <p> * If the chunk is not yet loaded the loadedLayer field is equal to -1. * * @author Jesper Öqvist (jesper@llbit.se) */ public class Chunk { public static final String LEVEL_HEIGHTMAP = ".Level.HeightMap"; public static final String LEVEL_SECTIONS = ".Level.Sections"; public static final String LEVEL_BIOMES = ".Level.Biomes"; private static final String LEVEL_ENTITIES = ".Level.Entities"; private static final String LEVEL_TILEENTITIES = ".Level.TileEntities"; /** Chunk width. */ public static final int X_MAX = 16; /** Chunk height. */ public static final int Y_MAX = 256; /** Chunk depth. */ public static final int Z_MAX = 16; private static final int SECTION_Y_MAX = 16; private static final int SECTION_BYTES = X_MAX * SECTION_Y_MAX * Z_MAX; private static final int SECTION_HALF_NIBBLES = SECTION_BYTES / 2; private static final int CHUNK_BYTES = X_MAX * Y_MAX * Z_MAX; public static final int BLOCK_LAYER = 1 << 0; public static final int SURFACE_LAYER = 1 << 1; public static final int CAVE_LAYER = 1 << 2; public static final int BIOME_LAYER = 1 << 3; private final ChunkPosition position; private int loadedLayer = -1; protected volatile AbstractLayer layer = UnknownLayer.INSTANCE; protected volatile AbstractLayer surface = UnknownLayer.INSTANCE; protected volatile AbstractLayer caves = UnknownLayer.INSTANCE; protected volatile AbstractLayer biomes = UnknownLayer.INSTANCE; private final World world; private int dataTimestamp = 0; private int layerTimestamp = 0; private int surfaceTimestamp = 0; private int cavesTimestamp = 0; private int biomesTimestamp = 0; public Chunk(ChunkPosition pos, World world) { this.world = world; this.position = pos; } public void renderLayer(MapTile tile) { layer.render(tile); } public int layerColor() { return layer.getAvgColor(); } public void renderSurface(MapTile tile) { surface.render(tile); } public int surfaceColor() { return surface.getAvgColor(); } public void renderCaves(MapTile tile) { caves.render(tile); } public int caveColor() { return caves.getAvgColor(); } public void renderBiomes(MapTile tile) { biomes.render(tile); } public int biomeColor() { return biomes.getAvgColor(); } /** * @param request fresh request set * @return loaded data, or null if something went wrong */ private Map<String, Tag> getChunkData(Set<String> request) { Region region = world.getRegion(position.getRegionPosition()); ChunkDataSource data = region.getChunkData(position); dataTimestamp = data.timestamp; if (data.inputStream != null) { try (DataInputStream in = data.inputStream) { Map<String, Tag> result = NamedTag.quickParse(in, request); for (String key : request) { if (!result.containsKey(key)) { result.put(key, new ErrorTag("")); } } return result; } catch (IOException e) { // Ignored. } } return null; } /** * Reset the rendered layers in this chunk. */ public synchronized void reset() { layer = UnknownLayer.INSTANCE; caves = UnknownLayer.INSTANCE; surface = UnknownLayer.INSTANCE; } /** * @return The position of this chunk */ public ChunkPosition getPosition() { return position; } /** * Render block highlight. */ public void renderHighlight(MapTile tile, Block hlBlock, int hlColor) { layer.renderHighlight(tile, hlBlock, hlColor); } /** * Parse the chunk from the region file and render the current * layer, surface and cave maps. */ public synchronized void loadChunk(WorldMapLoader loader) { int requestedLayer = world.currentLayer(); MapViewMode renderer = loader.getChunkRenderer(); ChunkView view = loader.getMapView(); if (!shouldReloadChunk(renderer, view, requestedLayer)) { return; } loadedLayer = requestedLayer; Map<String, Tag> data = getChunkData(renderer.getRequest(view)); int layers = renderer.getLayers(view); if ((layers & BLOCK_LAYER) != 0) { layerTimestamp = dataTimestamp; loadLayer(data, requestedLayer); } if ((layers & SURFACE_LAYER) != 0) { surfaceTimestamp = dataTimestamp; loadSurface(data); } if ((layers & BIOME_LAYER) != 0) { biomesTimestamp = dataTimestamp; loadBiomes(data); } if ((layers & CAVE_LAYER) != 0) { cavesTimestamp = dataTimestamp; loadCaves(data); } world.chunkUpdated(position); } private void loadSurface(Map<String, Tag> data) { if (data == null) { surface = CorruptLayer.INSTANCE; return; } Heightmap heightmap = world.heightmap(); Tag sections = data.get(LEVEL_SECTIONS); if (sections.isList()) { int[] heightmapData = extractHeightmapData(data); byte[] biomeData = extractBiomeData(data); byte[] chunkData = new byte[CHUNK_BYTES]; byte[] blockData = new byte[CHUNK_BYTES]; extractChunkData(data, chunkData, blockData); updateHeightmap(heightmap, position, chunkData, heightmapData); surface = new SurfaceLayer(world.currentDimension(), chunkData, biomeData, blockData); queueTopography(); } else { surface = CorruptLayer.INSTANCE; } } private void loadBiomes(Map<String, Tag> data) { if (data == null) { biomes = CorruptLayer.INSTANCE; } else { biomes = new BiomeLayer(extractBiomeData(data)); } } private void loadLayer(Map<String, Tag> data, int requestedLayer) { if (data == null) { layer = CorruptLayer.INSTANCE; return; } Tag sections = data.get(LEVEL_SECTIONS); if (sections.isList()) { byte[] biomeData = extractBiomeData(data); byte[] chunkData = new byte[CHUNK_BYTES]; extractChunkData(data, chunkData, new byte[CHUNK_BYTES]); layer = new BlockLayer(chunkData, biomeData, requestedLayer); } else { layer = CorruptLayer.INSTANCE; } } private void loadCaves(Map<String, Tag> data) { if (data == null) { caves = CorruptLayer.INSTANCE; return; } Tag sections = data.get(LEVEL_SECTIONS); if (sections.isList()) { int[] heightmapData = extractHeightmapData(data); byte[] chunkData = new byte[CHUNK_BYTES]; extractChunkData(data, chunkData, new byte[CHUNK_BYTES]); caves = new CaveLayer(chunkData, heightmapData); } else { caves = CorruptLayer.INSTANCE; } } private byte[] extractBiomeData(@NotNull Map<String, Tag> data) { Tag biomesTag = data.get(LEVEL_BIOMES); if (biomesTag.isByteArray(X_MAX * Z_MAX)) { return biomesTag.byteArray(); } else { return new byte[X_MAX * Z_MAX]; } } private int[] extractHeightmapData(@NotNull Map<String, Tag> data) { Tag heightmapTag = data.get(LEVEL_HEIGHTMAP); if (heightmapTag.isIntArray(X_MAX * Z_MAX)) { return heightmapTag.intArray(); } else { int[] fallback = new int[X_MAX * Z_MAX]; for (int i = 0; i < fallback.length; ++i) { fallback[i] = Y_MAX - 1; } return fallback; } } private void extractChunkData(@NotNull Map<String, Tag> data, @NotNull byte[] blocks, @NotNull byte[] blockData) { Tag sections = data.get(LEVEL_SECTIONS); if (sections.isList()) { for (SpecificTag section : ((ListTag) sections)) { Tag yTag = section.get("Y"); int yOffset = yTag.byteValue() & 0xFF; Tag blocksTag = section.get("Blocks"); if (blocksTag.isByteArray(SECTION_BYTES)) { System .arraycopy(blocksTag.byteArray(), 0, blocks, SECTION_BYTES * yOffset, SECTION_BYTES); } Tag dataTag = section.get("Data"); if (dataTag.isByteArray(SECTION_HALF_NIBBLES)) { System.arraycopy(dataTag.byteArray(), 0, blockData, SECTION_HALF_NIBBLES * yOffset, SECTION_HALF_NIBBLES); } } } } /** * Load heightmap information from a chunk heightmap array * and insert into a quadtree. */ public static void updateHeightmap(Heightmap heightmap, ChunkPosition pos, byte[] blocksArray, int[] chunkHeightmap) { for (int x = 0; x < 16; ++x) { for (int z = 0; z < 16; ++z) { int y = chunkHeightmap[z * 16 + x]; y = Math.max(1, y - 1); for (; y > 1; --y) { Block block = Block.get(blocksArray[Chunk.chunkIndex(x, y, z)]); if (block != Block.AIR && !block.isWater()) break; } heightmap.set(y, pos.x * 16 + x, pos.z * 16 + z); } } } private boolean shouldReloadChunk(MapViewMode renderer, ChunkView view, int requestedLayer) { int timestamp = Integer.MAX_VALUE; int layers = renderer.getLayers(view); if ((layers & BLOCK_LAYER) != 0) { if (requestedLayer != loadedLayer) { return true; } timestamp = layerTimestamp; } if ((layers & SURFACE_LAYER) != 0) { timestamp = Math.min(timestamp, surfaceTimestamp); } if ((layers & BIOME_LAYER) != 0) { timestamp = Math.min(timestamp, biomesTimestamp); } if ((layers & CAVE_LAYER) != 0) { timestamp = Math.min(timestamp, cavesTimestamp); } if (timestamp == 0) { return true; } Region region = world.getRegion(position.getRegionPosition()); return region.chunkChangedSince(position, timestamp); } private void queueTopography() { for (int x = -1; x <= 1; ++x) { for (int z = -1; z <= 1; ++z) { ChunkPosition pos = ChunkPosition.get(position.x + x, position.z + z); Chunk chunk = world.getChunk(pos); if (!chunk.isEmpty()) { world.chunkTopographyUpdated(chunk); } } } } /** * @return <code>true</code> if this is an empty (non-existing) chunk */ public boolean isEmpty() { return false; } /** * Render the topography of this chunk. */ public synchronized void renderTopography() { surface.renderTopography(position, world.heightmap()); world.chunkUpdated(position); } /** * Load the block data for this chunk. */ public synchronized void getBlockData(byte[] blocks, byte[] blockData, byte[] biomes, Collection<CompoundTag> tileEntities, Collection<CompoundTag> entities) { for (int i = 0; i < CHUNK_BYTES; ++i) { blocks[i] = 0; } for (int i = 0; i < X_MAX * Z_MAX; ++i) { biomes[i] = 0; } for (int i = 0; i < (CHUNK_BYTES) / 2; ++i) { blockData[i] = 0; } Set<String> request = new HashSet<>(); request.add(LEVEL_SECTIONS); request.add(LEVEL_BIOMES); request.add(LEVEL_ENTITIES); request.add(LEVEL_TILEENTITIES); Map<String, Tag> data = getChunkData(request); // TODO: improve error handling here. if (data == null) { return; } Tag sections = data.get(LEVEL_SECTIONS); Tag biomesTag = data.get(LEVEL_BIOMES); Tag entitiesTag = data.get(LEVEL_ENTITIES); Tag tileEntitiesTag = data.get(LEVEL_TILEENTITIES); if (sections.isList() && biomesTag.isByteArray(X_MAX * Z_MAX) && tileEntitiesTag.isList() && entitiesTag.isList()) { byte[] chunkBiomes = extractBiomeData(data); System.arraycopy(chunkBiomes, 0, biomes, 0, chunkBiomes.length); extractChunkData(data, blocks, blockData); ListTag list = (ListTag) entitiesTag; for (SpecificTag tag : list) { if (tag.isCompoundTag()) entities.add((CompoundTag) tag); } list = (ListTag) tileEntitiesTag; for (SpecificTag tag : list) { if (tag.isCompoundTag()) tileEntities.add((CompoundTag) tag); } } } /** * @return Integer index into a chunk YXZ array */ public static int chunkIndex(int x, int y, int z) { return x + Chunk.X_MAX * (z + Chunk.Z_MAX * y); } /** * @return Integer index into a chunk XZ array */ public static int chunkXZIndex(int x, int z) { return x + Chunk.X_MAX * z; } @Override public String toString() { return "Chunk: " + position.toString(); } public String biomeAt(int blockX, int blockZ) { if (biomes instanceof BiomeLayer) { BiomeLayer biomeLayer = (BiomeLayer) biomes; return biomeLayer.biomeAt(blockX, blockZ); } else { return "unknown"; } } }