Package se.llbit.chunky.world

Source Code of se.llbit.chunky.world.Chunk

/* Copyright (c) 2010-2014 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 java.awt.Color;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import se.llbit.chunky.main.Chunky;
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.MapBuffer;
import se.llbit.chunky.map.SurfaceLayer;
import se.llbit.chunky.map.UnknownLayer;
import se.llbit.nbt.AnyTag;
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;

/**
* This class represents a loaded or not-yet-loaded chunk in the world.
*
* If the chunk is not yet loaded the loadedLayer field is equal to -1.
*
* @author Jesper Öqvist (jesper@llbit.se)
*/
public class Chunk {

  private static final String LEVEL_HEIGHTMAP = ".Level.HeightMap";
  private static final String LEVEL_SECTIONS = ".Level.Sections";
  private static final String LEVEL_BIOMES = ".Level.Biomes";

  /**
   * 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;

  private static final int BLOCK_LAYER = 1<<0;
  private static final int SURFACE_LAYER = 1<<1;
  private static final int CAVE_LAYER = 1<<2;
  private 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;

  /**
   * A chunk renderer
   */
  abstract static public class Renderer {
    /**
     * Render the chunk
     * @param chunk
     * @param rbuff
     * @param cx
     * @param cz
     */
    abstract public void render(Chunk chunk, MapBuffer rbuff, int cx, int cz);

    /**
     * Layers to be loaded for this renderer
     * @param view
     * @return
     */
    abstract public int getLayers(ChunkView view);

    public Set<String> getRequest(ChunkView view) {
      int layers = getLayers(view);
      Set<String> request = new HashSet<String>();
      request.add(LEVEL_SECTIONS);
      if ((layers&BLOCK_LAYER) != 0 || (layers&SURFACE_LAYER) != 0 ||
          (layers&BIOME_LAYER) != 0) {
        request.add(LEVEL_BIOMES);
      }
      if ((layers&SURFACE_LAYER) != 0 || (layers&CAVE_LAYER) != 0) {
        request.add(LEVEL_HEIGHTMAP);
      }
      return request;
    }

    /**
     * @param view
     * @param newView
     * @param buffLayer
     * @param layer
     * @return {@code true} if the render buffer is still valid
     */
    public boolean bufferValid(ChunkView oldView, ChunkView newView,
        int oldLayer, int newLayer) {
      return oldView.chunkScale == newView.chunkScale;
    }
  }

  /**
   * Renders caves and other underground caverns
   */
  public static Renderer caveRenderer = new Renderer() {
    @Override
    public void render(Chunk chunk, MapBuffer rbuff, int cx, int cz) {
      chunk.renderCaves(rbuff, cx, cz);
    }

    @Override
    public int getLayers(ChunkView view) {
      return CAVE_LAYER;
    }
  };

  /**
   * Renders caves and other underground caverns
   */
  public static Renderer biomeRenderer = new Renderer() {
    @Override
    public void render(Chunk chunk, MapBuffer rbuff, int cx, int cz) {
      chunk.renderBiomes(rbuff, cx, cz);
    }

    @Override
    public int getLayers(ChunkView view) {
      return BIOME_LAYER;
    }
  };

  /**
   * Renders the default surface view
   */
  public static Renderer surfaceRenderer = new Renderer() {
    @Override
    public void render(Chunk chunk, MapBuffer rbuff, int cx, int cz) {
      chunk.renderSurface(rbuff, cx, cz);
    }

    @Override
    public int getLayers(ChunkView view) {
      return SURFACE_LAYER;
    }
  };

  /**
   * Switch between surface, layer and biome modes
   */
  public static Renderer autoRenderer = new Renderer() {
    @Override
    public void render(Chunk chunk, MapBuffer rbuff, int cx, int cz) {
      if (rbuff.getView().scale >= 10) {
        chunk.renderSurface(rbuff, cx, cz);
      } else {
        chunk.renderBiomes(rbuff, cx, cz);
      }
    }

    @Override
    public int getLayers(ChunkView view) {
      if (view.scale >= 10) {
        return SURFACE_LAYER | BIOME_LAYER;
      } else {
        return BIOME_LAYER;
      }
    }

    @Override
    public boolean bufferValid(ChunkView oldView, ChunkView newView,
        int oldLayer, int newLayer) {
      return super.bufferValid(oldView, newView, oldLayer, newLayer) &&
          (oldView.scale >= 10 && newView.scale >= 10 ||
          oldView.scale < 10 && newView.scale < 10);
    }
  };

  /**
   * Renders a single layer
   */
  public static Renderer layerRenderer = new Renderer() {
    @Override
    public void render(Chunk chunk, MapBuffer rbuff, int cx, int cz) {
      chunk.renderLayer(rbuff, cx, cz);
    }

    @Override
    public int getLayers(ChunkView view) {
      return BLOCK_LAYER;
    }

    @Override
    public boolean bufferValid(ChunkView oldView, ChunkView newView,
        int oldLayer, int newLayer) {
      return super.bufferValid(oldView, newView, oldLayer, newLayer) &&
          oldLayer == newLayer;
    }
  };

  /**
   * Create a new chunk
   * @param pos
   * @param world
   */
  public Chunk(ChunkPosition pos, World world) {
    this.world = world;
    this.position = pos;
  }

  protected void renderLayer(MapBuffer rbuff, int cx, int cz) {
    layer.render(rbuff, cx, cz);
  }

  protected void renderSurface(MapBuffer rbuff, int cx, int cz) {
    surface.render(rbuff, cx, cz);
  }

  protected void renderCaves(MapBuffer rbuff, int cx, int cz) {
    caves.render(rbuff, cx, cz);
  }

  protected void renderBiomes(MapBuffer rbuff, int cx, int cz) {
    biomes.render(rbuff, cx, cz);
  }

  /**
   * @param request fresh request set
   * @return loaded data, or null if something went wrong
   */
  private Map<String, AnyTag> getChunkData(Set<String> request) {
    Region region = world.getRegion(position.getRegionPosition());
    ChunkData data = region.getChunkData(position);
    if (data == null) {
      return null;
    }
    DataInputStream in = data.inputStream;
    if (in == null) {
      return null;
    }
    dataTimestamp = data.timestamp;
    Map<String, AnyTag> result = NamedTag.quickParse(in, request);
    try {
      in.close();
    } catch (IOException e) {
    }
    for (String key: request) {
      if (!result.containsKey(key)) {
        result.put(key, new ErrorTag());
      }
    }
    return result;
  }

  /**
   * 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
   * @param rbuff
   * @param cx
   * @param cz
   * @param hlBlock
   * @param highlightColor
   */
  public void renderHighlight(MapBuffer rbuff, int cx, int cz,
      Block hlBlock, Color highlightColor) {
    layer.renderHighlight(rbuff, cx, cz, hlBlock, highlightColor);
  }

  /**
   * Parse the chunk from the region file and render the current
   * layer, surface and cave maps.
   * @param chunky
   */
  public synchronized void loadChunk(Chunky chunky) {

    int requestedLayer = world.currentLayer();
    Chunk.Renderer renderer = chunky.getChunkRenderer();
    ChunkView view = chunky.getMapView();

    if (!shouldReloadChunk(renderer, view, requestedLayer)) {
      return;
    }

    loadedLayer = requestedLayer;

    Map<String, AnyTag> 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, AnyTag> data) {
    if (data == null) {
      surface = CorruptLayer.INSTANCE;
      return;
    }

    Heightmap heightmap = world.heightmap();
    AnyTag 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(), position,
          chunkData, biomeData, blockData);
      queueTopography();
    } else {
      surface = CorruptLayer.INSTANCE;
    }
  }

  private void loadBiomes(Map<String, AnyTag> data) {
    if (data == null) {
      biomes = CorruptLayer.INSTANCE;
    } else {
      biomes = new BiomeLayer(extractBiomeData(data));
    }
  }

  private void loadLayer(Map<String, AnyTag> data, int requestedLayer) {
    if (data == null) {
      layer = CorruptLayer.INSTANCE;
      return;
    }

    AnyTag 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, AnyTag> data) {
    if (data == null) {
      caves = CorruptLayer.INSTANCE;
      return;
    }

    AnyTag 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, AnyTag> data) {
    AnyTag 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, AnyTag> data) {
    AnyTag 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, AnyTag> data, @NotNull byte[] blocks, @NotNull byte[] blockData) {
    AnyTag sections = data.get(LEVEL_SECTIONS);
    if (sections.isList()) {
      for (SpecificTag section : ((ListTag) sections).getItemList()) {
        AnyTag yTag = section.get("Y");
        int yOffset = yTag.byteValue() & 0xFF;
        AnyTag blocksTag = section.get("Blocks");
        if (blocksTag.isByteArray(SECTION_BYTES)) {
          System.arraycopy(blocksTag.byteArray(), 0, blocks, SECTION_BYTES*yOffset, SECTION_BYTES);
        }
        AnyTag 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.
   *
   * @param heightmap
   * @param pos
   * @param blocksArray
   * @param chunkHeightmap
   */
  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(Renderer 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);
  }

  /**
   * @return The average color of the surface in this chunk
   */
  public synchronized int avgColor() {
    return surface.getAvgColor();
  }

  /**
   * Load the block data for this chunk
   * @param blocks
   * @param blockData
   * @param biomes
   */
  public synchronized void getBlockData(
      byte[] blocks, byte[] blockData, byte[] biomes) {

    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<String>();
    request.add(LEVEL_SECTIONS);
    request.add(LEVEL_BIOMES);
    Map<String, AnyTag> data = getChunkData(request);
    AnyTag sections = data.get(LEVEL_SECTIONS);
    AnyTag biomesTag = data.get(LEVEL_BIOMES);
    if (sections.isList() && biomesTag.isByteArray(X_MAX*Z_MAX)) {
      byte[] chunkBiomes = extractBiomeData(data);
      System.arraycopy(chunkBiomes, 0, biomes, 0, chunkBiomes.length);
      extractChunkData(data, blocks, blockData);
    }
  }

  /**
   * Write a PNG scanline
   * @param scanline
   * @param out
   * @throws IOException
   */
  public void writePngLine(int scanline, OutputStream out) throws IOException {
    surface.writePngLine(scanline, out);
  }


  /**
   * @param x
   * @param y
   * @param z
   * @return Integer index into a chunk YXZ array
   */
  public static final int chunkIndex(int x, int y, int z) {
    return x + Chunk.X_MAX * ( z + Chunk.Z_MAX * y );
  }

  /**
   * @param x
   * @param z
   * @return Integer index into a chunk XZ array
   */
  public static final int chunkXZIndex(int x, int z) {
    return x + Chunk.X_MAX * z;
  }

  @Override
  public String toString() {
    return "Chunk: " + position.toString();
  }

  /**
   * @return <code>true</code> if this chunk has been parsed
   */
  public synchronized boolean isParsed() {
    return loadedLayer != -1;
  }

  /**
   * @return If the currently visible layer has been parsed
   */
  public synchronized boolean isLayerParsed() {
    return loadedLayer == world.currentLayer();
  }

  public String biomeAt(int blockX, int blockZ) {
    if (biomes instanceof BiomeLayer) {
      BiomeLayer biomeLayer = (BiomeLayer) biomes;
      return biomeLayer.biomeAt(blockX, blockZ);
    } else {
      return "unknown";
    }
  }
}
TOP

Related Classes of se.llbit.chunky.world.Chunk

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.