/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package org.pepsoft.worldpainter; import org.pepsoft.util.MathUtils; import org.pepsoft.util.PerlinNoise; import org.pepsoft.util.ProgressReceiver; import org.pepsoft.util.ProgressReceiver.OperationCancelled; import org.pepsoft.util.undo.UndoManager; import org.pepsoft.worldpainter.biomeschemes.CustomBiome; import org.pepsoft.worldpainter.gardenofeden.Garden; import org.pepsoft.worldpainter.gardenofeden.Seed; import org.pepsoft.worldpainter.layers.*; import org.pepsoft.worldpainter.layers.exporters.ExporterSettings; import org.pepsoft.worldpainter.layers.exporters.ResourcesExporter.ResourcesExporterSettings; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.FileImageInputStream; import javax.imageio.stream.ImageInputStream; import javax.swing.*; import javax.vecmath.Point3i; import java.awt.*; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.*; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.pepsoft.minecraft.Constants.*; import static org.pepsoft.worldpainter.Constants.*; import static org.pepsoft.worldpainter.biomeschemes.Minecraft1_7Biomes.*; /** * * @author pepijn */ public class Dimension extends InstanceKeeper implements TileProvider, Serializable, Tile.Listener, Cloneable { public Dimension(long minecraftSeed, TileFactory tileFactory, int dim, int maxHeight) { this(minecraftSeed, tileFactory, dim, maxHeight, true); } public Dimension(long minecraftSeed, TileFactory tileFactory, int dim, int maxHeight, boolean init) { this.seed = tileFactory.getSeed(); this.minecraftSeed = minecraftSeed; this.tileFactory = tileFactory; this.dim = dim; this.maxHeight = maxHeight; if (init) { if (dim == DIM_NORMAL) { layerSettings.put(Resources.INSTANCE, new ResourcesExporterSettings(maxHeight)); } else if (dim == DIM_NETHER) { layerSettings.put(Resources.INSTANCE, new ResourcesExporterSettings(maxHeight, true)); } topLayerDepthNoise = new PerlinNoise(seed + TOP_LAYER_DEPTH_SEED_OFFSET); } } public World2 getWorld() { return world; } void setWorld(World2 world) { this.world = world; } public int getDim() { return dim; } public String getName() { switch (dim) { case -3: return "End Ceiling"; case -2: return "Nether Ceiling"; case -1: return "Surface Ceiling"; case 0: return "Surface"; case 1: return "Nether"; case 2: return "End"; default: return "Dimension " + dim; } } public boolean isDirty() { return dirty; } public void setDirty(boolean dirty) { this.dirty = dirty; } public long getSeed() { return seed; } public Terrain getSubsurfaceMaterial() { return subsurfaceMaterial; } public void setSubsurfaceMaterial(Terrain subsurfaceMaterial) { if (subsurfaceMaterial != this.subsurfaceMaterial) { Terrain oldSubsurfaceMaterial = this.subsurfaceMaterial; this.subsurfaceMaterial = subsurfaceMaterial; dirty = true; propertyChangeSupport.firePropertyChange("subsurfaceMaterial", oldSubsurfaceMaterial, subsurfaceMaterial); } } public boolean isPopulate() { return populate; } public void setPopulate(boolean populate) { if (populate != this.populate) { this.populate = populate; dirty = true; propertyChangeSupport.firePropertyChange("populate", ! populate, populate); } } public Border getBorder() { return border; } public void setBorder(Border border) { if (border != this.border) { Border oldBorder = this.border; this.border = border; dirty = true; propertyChangeSupport.firePropertyChange("border", oldBorder, border); } } public int getBorderLevel() { return borderLevel; } public void setBorderLevel(int borderLevel) { if (borderLevel != this.borderLevel) { int oldBorderLevel = this.borderLevel; this.borderLevel = borderLevel; dirty = true; propertyChangeSupport.firePropertyChange("borderLevel", oldBorderLevel, borderLevel); } } public int getBorderSize() { return borderSize; } public void setBorderSize(int borderSize) { if (borderSize != this.borderSize) { int oldBorderSize = this.borderSize; this.borderSize = borderSize; dirty = true; propertyChangeSupport.firePropertyChange("borderSize", oldBorderSize, borderSize); } } public boolean isDarkLevel() { return darkLevel; } public void setDarkLevel(boolean darkLevel) { if (darkLevel != this.darkLevel) { this.darkLevel = darkLevel; dirty = true; propertyChangeSupport.firePropertyChange("darkLevel", ! darkLevel, darkLevel); } } public boolean isBedrockWall() { return bedrockWall; } public void setBedrockWall(boolean bedrockWall) { if (bedrockWall != this.bedrockWall) { this.bedrockWall = bedrockWall; dirty = true; propertyChangeSupport.firePropertyChange("bedrockWall", ! bedrockWall, bedrockWall); } } public TileFactory getTileFactory() { return tileFactory; } /** * Determines whether a tile is present in the dimension on specific * coordinates. * * @param x The world X coordinate for which to determine whether a tile is * present. * @param y The world Y coordinate for which to determine whether a tile is * present. * @return <code>true</code> if the dimension contains a tile at the * specified location. */ @Override public synchronized boolean isTilePresent(final int x, final int y) { return tiles.containsKey(new Point(x, y)); } /** * Indicates whether the specified tile is a border tile. * * @param x The X coordinate of the tile for which to check whether it is a * border tile. * @param y The Y coordinate of the tile for which to check whether it is a * border tile. * @return <code>true</code> if it is a border tile. */ public synchronized boolean isBorderTile(int x, int y) { if ((border == null) || ((! border.isEndless()) && ((x < (lowestX - borderSize)) || (x > (highestX + borderSize)) || (y < (lowestY - borderSize)) || (y > (highestY + borderSize))))) { // Couldn't possibly be a border tile return false; } else if (tiles.containsKey(new Point(x, y))) { // There's a tile in the dimension at these coordinates, so not a // border tile return false; } else if (border.isEndless()) { // The border is an endless border, so any tile outside the // dimension is a border tile return true; } else { for (int r = 1; r <= borderSize; r++) { for (int i = 0; i <= (r * 2); i++) { if (tiles.containsKey(new Point(x + i - r, y - r)) || tiles.containsKey(new Point(x + r, y + i - r)) || tiles.containsKey(new Point(x + r - i, y + r)) || tiles.containsKey(new Point(x - r, y - i + r))) { // Found a tile in the dimension <= borderSize tiles // away, so this is a border tile return true; } } } // No tiles in dimension <= borderSize tiles away, so not a border // tile return false; } } /** * Get the tile for a particular set of world or absolute block coordinates. * * @param x The world X coordinate for which to get the tile. * @param y The world Y coordinate for which to get the tile. * @return The tile on which the specified coordinates lie, or * <code>null</code> if there is no tile for those coordinates */ @Override public synchronized Tile getTile(final int x, final int y) { return tiles.get(new Point(x, y)); } public synchronized Tile getTile(final Point coords) { return tiles.get(coords); } /** * Get the tile for a particular set of world or absolute block coordinates with the intention of modifying it. This * is intended to be used in combination with {@link #setEventsInhibited(boolean)}. Whenever * <code>eventsInhibited</code> is <code>true</code>, the dimension will automatically inhibit events on the tile, * mark it as dirty and fire an event for it when <code>eventsInhibited</code> is set to <code>false</code>. * * @param x The world X coordinate for which to get the tile. * @param y The world Y coordinate for which to get the tile. * @return The tile on which the specified coordinates lie, or * <code>null</code> if there is no tile for those coordinates */ public synchronized Tile getTileForEditing(final int x, final int y) { Tile tile = tiles.get(new Point(x, y)); if ((tile != null) && eventsInhibited && (! tile.isEventsInhibited())) { tile.inhibitEvents(); dirtyTiles.add(tile); } return tile; } /** * Get the tile for a particular set of world or absolute block coordinates with the intention of modifying it. This * is intended to be used in combination with {@link #setEventsInhibited(boolean)}. Whenever * <code>eventsInhibited</code> is <code>true</code>, the dimension will automatically inhibit events on the tile, * mark it as dirty and fire an event for it when <code>eventsInhibited</code> is set to <code>false</code>. * * @param coords The world coordinates for which to get the tile. * @return The tile on which the specified coordinates lie, or * <code>null</code> if there is no tile for those coordinates */ public synchronized Tile getTileForEditing(final Point coords) { Tile tile = tiles.get(coords); if ((tile != null) && eventsInhibited && (! tile.isEventsInhibited())) { tile.inhibitEvents(); dirtyTiles.add(tile); } return tile; } @Override public Rectangle getExtent() { return new Rectangle(lowestX, lowestY, (highestX - lowestX) + 1, (highestY - lowestY) + 1); } public int getTileCount() { return tiles.size(); } public Collection<? extends Tile> getTiles() { return Collections.unmodifiableCollection(tiles.values()); } public Set<Point> getTileCoords() { return Collections.unmodifiableSet(tiles.keySet()); } public synchronized void addTile(Tile tile) { if (tile.getMaxHeight() != maxHeight) { throw new IllegalArgumentException("Tile has different max height (" + tile.getMaxHeight() + ") than dimension (" + maxHeight + ")"); } final int x = tile.getX(); final int y = tile.getY(); final Point key = new Point(x, y); if (tiles.containsKey(key)) { throw new IllegalStateException("Tile already set"); } tile.addListener(this); if (undoManager != null) { tile.register(undoManager); } tiles.put(key, tile); // Invalidate all thread local tile caches, as the fact that this tile // didn't exist may be cached somewhere tileCache = new ThreadLocal<TileCache>() { @Override protected TileCache initialValue() { return new TileCache(); } }; if (x < lowestX) { lowestX = x; } if (x > highestX) { highestX = x; } if (y < lowestY) { lowestY = y; } if (y > highestY) { highestY = y; } fireTileAdded(tile); dirty = true; // biomesCalculated = false; } public void removeTile(int tileX, int tileY) { removeTile(new Point(tileX, tileY)); } public void removeTile(Tile tile) { removeTile(tile.getX(), tile.getY()); } public synchronized void removeTile(Point coords) { if (! tiles.containsKey(coords)) { throw new IllegalStateException("Tile not set"); } final Tile tile = tiles.remove(coords); if (undoManager != null) { tile.unregister(); } tile.removeListener(this); // If the tile lies at the edge of the world it's possible the low and // high coordinate marks should change; so recalculate them in that case if ((coords.x == lowestX) || (coords.x == highestX) || (coords.y == lowestY) || (coords.y == highestY)) { lowestX = Integer.MAX_VALUE; highestX = Integer.MIN_VALUE; lowestY = Integer.MAX_VALUE; highestY = Integer.MIN_VALUE; for (Tile myTile: tiles.values()) { int myTileX = myTile.getX(), myTileY = myTile.getY(); if (myTileX < lowestX) { lowestX = myTileX; } if (myTileX > highestX) { highestX = myTileX; } if (myTileY < lowestY) { lowestY = myTileY; } if (myTileY > highestY) { highestY = myTileY; } } } fireTileRemoved(tile); dirty = true; } public int getHighestX() { return highestX; } public int getHighestY() { return highestY; } public int getLowestX() { return lowestX; } public int getLowestY() { return lowestY; } public int getWidth() { if (highestX == Integer.MIN_VALUE) { return 0; } else { return highestX - lowestX + 1; } } public int getHeight() { if (highestY == Integer.MIN_VALUE) { return 0; } else { return highestY - lowestY + 1; } } public int getIntHeightAt(int x, int y) { return getIntHeightAt(x, y, -1); } public int getIntHeightAt(int x, int y, int defaultHeight) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getIntHeight(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return defaultHeight; } } public int getIntHeightAt(Point coords) { return getIntHeightAt(coords.x, coords.y, -1); } public float getHeightAt(int x, int y) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getHeight(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return Float.MIN_VALUE; } } public float getHeightAt(Point coords) { return getHeightAt(coords.x, coords.y); } public void setHeightAt(int x, int y, float height) { Tile tile = getTileForEditing(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { tile.setHeight(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, height); } } public void setHeightAt(Point coords, float height) { setHeightAt(coords.x, coords.y, height); } public int getRawHeightAt(int x, int y) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getRawHeight(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return Integer.MIN_VALUE; } } public int getRawHeightAt(Point coords) { return getRawHeightAt(coords.x, coords.y); } public void setRawHeightAt(int x, int y, int rawHeight) { Tile tile = getTileForEditing(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { tile.setRawHeight(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, rawHeight); } } public void setRawHeightAt(Point coords, int rawHeight) { setRawHeightAt(coords.x, coords.y, rawHeight); } public float getSlope(int x, int y) { final int xInTile = x & TILE_SIZE_MASK, yInTile = y & TILE_SIZE_MASK; if ((xInTile > 0) && (xInTile < (TILE_SIZE - 1)) && (yInTile > 0) && (yInTile < (TILE_SIZE - 1))) { // Inside one tile; delegate to tile Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getSlope(xInTile, yInTile); } else { return 0.0f; } } else { // Spanning tiles; do it ourselves return Math.max(Math.max(Math.abs(getHeightAt(x + 1, y) - getHeightAt(x - 1, y)) / 2, Math.abs(getHeightAt(x + 1, y + 1) - getHeightAt(x - 1, y - 1)) / ROOT_EIGHT), Math.max(Math.abs(getHeightAt(x, y + 1) - getHeightAt(x, y - 1)) / 2, Math.abs(getHeightAt(x - 1, y + 1) - getHeightAt(x + 1, y - 1)) / ROOT_EIGHT)); } } public Terrain getTerrainAt(int x, int y) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getTerrain(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return null; } } public void setTerrainAt(int x, int y, Terrain terrain) { Tile tile = getTileForEditing(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { tile.setTerrain(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, terrain); } } public void setTerrainAt(Point coords, Terrain terrain) { setTerrainAt(coords.x, coords.y, terrain); } public void applyTheme(int x, int y) { Tile tile = getTileForEditing(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { tileFactory.applyTheme(tile, x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } } public int getWaterLevelAt(int x, int y) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getWaterLevel(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return Integer.MIN_VALUE; } } public int getWaterLevelAt(Point coords) { return getWaterLevelAt(coords.x, coords.y); } public void setWaterLevelAt(int x, int y, int waterLevel) { Tile tile = getTileForEditing(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { tile.setWaterLevel(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, waterLevel); } } public int getLayerValueAt(Layer layer, int x, int y) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getLayerValue(layer, x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return layer.getDefaultValue(); } } public int getLayerValueAt(Layer layer, Point coords) { return getLayerValueAt(layer, coords.x, coords.y); } public void setLayerValueAt(Layer layer, int x, int y, int value) { Tile tile = getTileForEditing(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { tile.setLayerValue(layer, x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, value); } } public boolean getBitLayerValueAt(Layer layer, int x, int y) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getBitLayerValue(layer, x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return false; } } /** * Count the number of blocks where the specified bit layer is set in a * square around a particular location * * @param layer The bit layer to count. * @param x The global X coordinate of the location around which to count * the layer. * @param y The global Y coordinate of the location around which to count * the layer. * @param r The radius of the square. * @return The number of blocks in the specified square where the specified * bit layer is set. */ public synchronized int getBitLayerCount(final Layer layer, final int x, final int y, final int r) { final int tileX = x >> TILE_SIZE_BITS, tileY = y >> TILE_SIZE_BITS; if (((x - r) >> TILE_SIZE_BITS == tileX) && ((x + r) >> TILE_SIZE_BITS == tileX) && ((y - r) >> TILE_SIZE_BITS == tileY) && ((y + r) >> TILE_SIZE_BITS == tileY)) { // The requested area is completely contained in one tile, optimise // by delegating to the tile final Tile tile = getTile(tileX, tileY); if (tile != null) { return tile.getBitLayerCount(layer, x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, r); } else { return 0; } } else { // The requested area overlaps tile boundaries; do it the slow way int count = 0; for (int dx = -r; dx <= r; dx++) { for (int dy = -r; dy <= r; dy++) { if (getBitLayerValueAt(layer, x + dx, y + dy)) { count++; } } } return count; } } /** * Gets all layers that are set at the specified location, along with their * intensities. For bit valued layers the intensity is zero for off, one for * on. * * @param x The X location for which to retrieve all layers. * @param y The Y location for which to retrieve all layers. * @return A map with all layers set at the specified location, mapped to * their intensities at that location. May either be <code>null</code> * or an empty map if no layers are present. */ public Map<Layer, Integer> getLayersAt(int x, int y) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return tile.getLayersAt(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return null; } } /** * Count the number of blocks that are flooded in a square around a * particular location * * @param x The global X coordinate of the location around which to count * flooded blocks. * @param y The global Y coordinate of the location around which to count * flooded blocks. * @param r The radius of the square. * @param lava Whether to check for lava (when <code>true</code>) or water * (when <code>false</code>). * @return The number of blocks in the specified square that are flooded. */ public synchronized int getFloodedCount(final int x, final int y, final int r, final boolean lava) { final int tileX = x >> TILE_SIZE_BITS, tileY = y >> TILE_SIZE_BITS; if (((x - r) >> TILE_SIZE_BITS == tileX) && ((x + r) >> TILE_SIZE_BITS == tileX) && ((y - r) >> TILE_SIZE_BITS == tileY) && ((y + r) >> TILE_SIZE_BITS == tileY)) { // The requested area is completely contained in one tile, optimise // by delegating to the tile final Tile tile = getTile(tileX, tileY); if (tile != null) { return tile.getFloodedCount(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, r, lava); } else { return 0; } } else { // The requested area overlaps tile boundaries; do it the slow way int count = 0; for (int dx = -r; dx <= r; dx++) { for (int dy = -r; dy <= r; dy++) { final int xx = x + dx, yy = y + dy; if ((getWaterLevelAt(xx, yy) > getIntHeightAt(xx, yy)) && (lava ? getBitLayerValueAt(FloodWithLava.INSTANCE, xx, yy) : (! getBitLayerValueAt(FloodWithLava.INSTANCE, xx, yy)))) { count++; } } } return count; } } /** * Get the distance from the specified coordinate to the nearest pixel where * the specified layer is <em>not</em> set. * * @param layer The layer for which to find the distance to the nearest * edge. * @param x The X coordinate of the location towards which to determine the distance. * @param y The Y coordinate of the location towards which to determine the distance. * @param maxDistance The maximum distance to return. If the actual distance is further, this value will be returned. * @return The distance from the specified location to the nearest pixel * where the specified layer is not set, or maxDistance, whichever is * smaller. If the layer is not set at the specified coordinates, 0 is * returned. */ public synchronized float getDistanceToEdge(final Layer layer, final int x, final int y, final float maxDistance) { final int r = (int) Math.ceil(maxDistance); final int tileX = x >> TILE_SIZE_BITS, tileY = y >> TILE_SIZE_BITS; if (((x - r) >> TILE_SIZE_BITS == tileX) && ((x + r) >> TILE_SIZE_BITS == tileX) && ((y - r) >> TILE_SIZE_BITS == tileY) && ((y + r) >> TILE_SIZE_BITS == tileY)) { // The requested area is completely contained in one tile, optimise // by delegating to the tile final Tile tile = getTile(tileX, tileY); if (tile != null) { return tile.getDistanceToEdge(layer, x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, maxDistance); } else { return 0; } } else { if (! getBitLayerValueAt(layer, x, y)) { return 0; } float distance = maxDistance; for (int i = 1; i <= r; i++) { if (((! getBitLayerValueAt(layer, x - i, y)) || (! getBitLayerValueAt(layer, x + i, y)) || (! getBitLayerValueAt(layer, x, y - i)) || (! getBitLayerValueAt(layer, x, y + i))) && (i < distance)) { // If we get here there's no possible way a shorter // distance could be found later, so return immediately return i; } for (int d = 1; d <= i; d++) { if ((! getBitLayerValueAt(layer, x - i, y - d)) || (! getBitLayerValueAt(layer, x + d, y - i)) || (! getBitLayerValueAt(layer, x + i, y + d)) || (! getBitLayerValueAt(layer, x - d, y + i)) || ((d < i) && ((! getBitLayerValueAt(layer, x - i, y + d)) || (! getBitLayerValueAt(layer, x - d, y - i)) || (! getBitLayerValueAt(layer, x + i, y - d)) || (! getBitLayerValueAt(layer, x + d, y + i))))) { float tDistance = MathUtils.getDistance(i, d); if (tDistance < distance) { distance = tDistance; } // We won't find a shorter distance this round, so // skip to the next round break; } } } return distance; } } public void setBitLayerValueAt(Layer layer, int x, int y, boolean value) { Tile tile = getTileForEditing(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { tile.setBitLayerValue(layer, x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, value); } } public void clearLayerData(Layer layer) { tiles.values().stream().filter(tile -> tile.hasLayer(layer)).forEach(tile -> { if (eventsInhibited && (!tile.isEventsInhibited())) { tile.inhibitEvents(); dirtyTiles.add(tile); } tile.clearLayerData(layer); }); } /** * Clear all layer data at a particular location (by resetting to the * layer's default value), possibly with the exception of certain layers. * * @param x The X coordinate of the location to clear of layer data. * @param y The Y coordinate of the location to clear of layer data. * @param excludedLayers The layers to exclude, if any. May be * <code>null</code>. */ public void clearLayerData(int x, int y, Set<Layer> excludedLayers) { Tile tile = getTileForEditing(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { tile.clearLayerData(x & TILE_SIZE_MASK, y & TILE_SIZE_MASK, excludedLayers); } } public void setEventsInhibited(boolean eventsInhibited) { if (eventsInhibited != this.eventsInhibited) { this.eventsInhibited = eventsInhibited; if (eventsInhibited == false) { fireTilesAdded(addedTiles); addedTiles.clear(); fireTilesRemoved(removedTiles); removedTiles.clear(); dirtyTiles.forEach(org.pepsoft.worldpainter.Tile::releaseEvents); dirtyTiles.clear(); } } else { throw new IllegalStateException("eventsInhibited already " + eventsInhibited); } } public boolean isEventsInhibited() { return eventsInhibited; } public Map<Layer, ExporterSettings> getAllLayerSettings() { return Collections.unmodifiableMap(layerSettings); } @SuppressWarnings("unchecked") public ExporterSettings getLayerSettings(Layer layer) { return layerSettings.get(layer); } public void setLayerSettings(Layer layer, ExporterSettings settings) { if ((! layerSettings.containsKey(layer)) || (! settings.equals(layerSettings.get(layer)))) { layerSettings.put(layer, settings); dirty = true; } } public long getMinecraftSeed() { return minecraftSeed; } public void setMinecraftSeed(long minecraftSeed) { if (minecraftSeed != this.minecraftSeed) { long oldMinecraftSeed = this.minecraftSeed; this.minecraftSeed = minecraftSeed; dirty = true; propertyChangeSupport.firePropertyChange("minecraftSeed", oldMinecraftSeed, minecraftSeed); } } public File getOverlay() { return overlay; } public void setOverlay(File overlay) { if ((overlay != null) ? (! overlay.equals(this.overlay)) : (this.overlay == null)) { File oldOverlay = this.overlay; this.overlay = overlay; dirty = true; propertyChangeSupport.firePropertyChange("overlay", oldOverlay, overlay); } } public int getOverlayOffsetX() { return overlayOffsetX; } public void setOverlayOffsetX(int overlayOffsetX) { if (overlayOffsetX != this.overlayOffsetX) { int oldOverlayOffsetX = this.overlayOffsetX; this.overlayOffsetX = overlayOffsetX; dirty = true; propertyChangeSupport.firePropertyChange("overlayOffsetX", oldOverlayOffsetX, overlayOffsetX); } } public int getOverlayOffsetY() { return overlayOffsetY; } public void setOverlayOffsetY(int overlayOffsetY) { if (overlayOffsetY != this.overlayOffsetY) { int oldOverlayOffsetY = this.overlayOffsetY; this.overlayOffsetY = overlayOffsetY; dirty = true; propertyChangeSupport.firePropertyChange("overlayOffsetY", oldOverlayOffsetY, overlayOffsetY); } } public float getOverlayScale() { return overlayScale; } public void setOverlayScale(float overlayScale) { if (overlayScale != this.overlayScale) { float oldOverlayScale = this.overlayScale; this.overlayScale = overlayScale; dirty = true; propertyChangeSupport.firePropertyChange("overlayScale", oldOverlayScale, overlayScale); } } public float getOverlayTransparency() { return overlayTransparency; } public void setOverlayTransparency(float overlayTransparency) { if (overlayTransparency != this.overlayTransparency) { float oldOverlayTransparency = this.overlayTransparency; this.overlayTransparency = overlayTransparency; dirty = true; propertyChangeSupport.firePropertyChange("overlayTransparency", oldOverlayTransparency, overlayTransparency); } } public boolean isGridEnabled() { return gridEnabled; } public void setGridEnabled(boolean gridEnabled) { if (gridEnabled != this.gridEnabled) { this.gridEnabled = gridEnabled; dirty = true; propertyChangeSupport.firePropertyChange("gridEnabled", ! gridEnabled, gridEnabled); } } public int getGridSize() { return gridSize; } public void setGridSize(int gridSize) { if (gridSize != this.gridSize) { int oldGridSize = this.gridSize; this.gridSize = gridSize; dirty = true; propertyChangeSupport.firePropertyChange("gridSize", oldGridSize, gridSize); } } public boolean isOverlayEnabled() { return overlayEnabled; } public void setOverlayEnabled(boolean overlayEnabled) { if (overlayEnabled != this.overlayEnabled) { this.overlayEnabled = overlayEnabled; dirty = true; propertyChangeSupport.firePropertyChange("overlayEnabled", ! overlayEnabled, overlayEnabled); } } public int getMaxHeight() { return maxHeight; } public void setMaxHeight(int maxHeight) { if (maxHeight != this.maxHeight) { int oldMaxHeight = this.maxHeight; this.maxHeight = maxHeight; dirty = true; propertyChangeSupport.firePropertyChange("maxHeight", oldMaxHeight, maxHeight); } } public int getContourSeparation() { return contourSeparation; } public void setContourSeparation(int contourSeparation) { if (contourSeparation != this.contourSeparation) { int oldContourSeparation = this.contourSeparation; this.contourSeparation = contourSeparation; dirty = true; propertyChangeSupport.firePropertyChange("contourSeparation", oldContourSeparation, contourSeparation); } } public boolean isContoursEnabled() { return contoursEnabled; } public void setContoursEnabled(boolean contoursEnabled) { if (contoursEnabled != this.contoursEnabled) { this.contoursEnabled = contoursEnabled; dirty = true; propertyChangeSupport.firePropertyChange("contoursEnabled", ! contoursEnabled, contoursEnabled); } } public int getTopLayerMinDepth() { return topLayerMinDepth; } public void setTopLayerMinDepth(int topLayerMinDepth) { if (topLayerMinDepth != this.topLayerMinDepth) { int oldTopLayerMinDepth = this.topLayerMinDepth; this.topLayerMinDepth = topLayerMinDepth; dirty = true; propertyChangeSupport.firePropertyChange("topLayerMinDepth", oldTopLayerMinDepth, topLayerMinDepth); } } public int getTopLayerVariation() { return topLayerVariation; } public void setTopLayerVariation(int topLayerVariation) { if (topLayerVariation != this.topLayerVariation) { int oldTopLayerVariation = this.topLayerVariation; this.topLayerVariation = topLayerVariation; dirty = true; propertyChangeSupport.firePropertyChange("topLayerVariation", oldTopLayerVariation, topLayerVariation); } } public boolean isBottomless() { return bottomless; } public void setBottomless(boolean bottomless) { if (bottomless != this.bottomless) { this.bottomless = bottomless; dirty = true; propertyChangeSupport.firePropertyChange("bottomless", ! bottomless, bottomless); } } public Point getLastViewPosition() { return lastViewPosition; } public void setLastViewPosition(Point lastViewPosition) { if (lastViewPosition == null) { throw new NullPointerException(); } if (! lastViewPosition.equals(this.lastViewPosition)) { Point oldLastViewPosition = this.lastViewPosition; this.lastViewPosition = lastViewPosition; // Don't mark dirty just for changing the view position propertyChangeSupport.firePropertyChange("lastViewPosition", oldLastViewPosition, lastViewPosition); } } public List<CustomBiome> getCustomBiomes() { return customBiomes; } public void setCustomBiomes(List<CustomBiome> customBiomes) { if ((customBiomes != null) ? (! customBiomes.equals(this.customBiomes)) : (this.customBiomes != null)) { List<CustomBiome> oldCustomBiomes = this.customBiomes; this.customBiomes = customBiomes; dirty = true; propertyChangeSupport.firePropertyChange("customBiomes", oldCustomBiomes, customBiomes); } } public boolean isCoverSteepTerrain() { return coverSteepTerrain; } public void setCoverSteepTerrain(boolean coverSteepTerrain) { if (coverSteepTerrain != this.coverSteepTerrain) { this.coverSteepTerrain = coverSteepTerrain; dirty = true; propertyChangeSupport.firePropertyChange("coverSteepTerrain", ! coverSteepTerrain, coverSteepTerrain); } } public boolean isFixOverlayCoords() { return fixOverlayCoords; } public void setFixOverlayCoords(boolean fixOverlayCoords) { this.fixOverlayCoords = fixOverlayCoords; } public Garden getGarden() { return garden; } public List<CustomLayer> getCustomLayers() { return customLayers; } public void setCustomLayers(List<CustomLayer> customLayers) { this.customLayers = customLayers; } /** * Returns the set of all layers currently in use on the world, optionally * including layers that are included in combined layers. * * @param applyCombinedLayers Whether to include layers from combined layers * which are not used independently in the dimension. * @return The set of all layers currently in use on the world. */ public Set<Layer> getAllLayers(boolean applyCombinedLayers) { Set<Layer> allLayers = new HashSet<>(); for (Tile tile: tiles.values()) { allLayers.addAll(tile.getLayers()); } if (applyCombinedLayers) { Set<LayerContainer> containersProcessed = new HashSet<>(); boolean containersFound; do { containersFound = false; for (Layer layer: new HashSet<>(allLayers)) { if ((layer instanceof LayerContainer) && (! containersProcessed.contains(layer))) { allLayers.addAll(((LayerContainer) layer).getLayers()); containersProcessed.add((LayerContainer) layer); containersFound = true; } } } while (containersFound); } return allLayers; } /** * Get the set of layers that has been configured to be applied everywhere. * * @return The set of layers that has been configured to be applied * everywhere. */ public Set<Layer> getMinimumLayers() { Set<Layer> layers = layerSettings.values().stream().filter(ExporterSettings::isApplyEverywhere).map(ExporterSettings::getLayer).collect(Collectors.toSet()); return layers; } public int getCeilingHeight() { return ceilingHeight; } public void setCeilingHeight(int ceilingHeight) { if (ceilingHeight != this.ceilingHeight) { int oldCeilingHeight = this.ceilingHeight; this.ceilingHeight = ceilingHeight; dirty = true; propertyChangeSupport.firePropertyChange("ceilingHeight", oldCeilingHeight, ceilingHeight); } } public void applyTheme(Point coords) { applyTheme(coords.x, coords.y); } public boolean isUndoAvailable() { return undoManager != null; } public void register(UndoManager undoManager) { this.undoManager = undoManager; for (Tile tile: tiles.values()) { tile.register(undoManager); } // garden.register(undoManager); } public boolean undoChanges() { if ((undoManager != null) && undoManager.isDirty()) { return undoManager.undo(); } else { return false; } } public void clearUndo() { if (undoManager != null) { undoManager.clear(); } } public void armSavePoint() { if (undoManager != null) { undoManager.armSavePoint(); } } public void rememberChanges() { if (undoManager != null) { if (undoManager.isDirty()) { undoManager.savePoint(); } else { undoManager.armSavePoint(); } } } public void clearRedo() { if (undoManager != null) { undoManager.clearRedo(); } } public void unregister() { for (Tile tile: tiles.values()) { tile.removeListener(this); tile.unregister(); } undoManager = null; } public final int getAutoBiome(int x, int y) { Tile tile = getTile(x >> TILE_SIZE_BITS, y >> TILE_SIZE_BITS); if (tile != null) { return getAutoBiome(tile, x & TILE_SIZE_MASK, y & TILE_SIZE_MASK); } else { return -1; } } public final int getAutoBiome(Tile tile, int x, int y) { int biome; if (tile.getBitLayerValue(Frost.INSTANCE, x, y)) { if (tile.getBitLayerValue(River.INSTANCE, x, y)) { biome = BIOME_FROZEN_RIVER; } else if ((tile.getLayerValue(DeciduousForest.INSTANCE, x, y) > 0) || (tile.getLayerValue(PineForest.INSTANCE, x, y) > 0) || (tile.getLayerValue(SwampLand.INSTANCE, x, y) > 0) || (tile.getLayerValue(Jungle.INSTANCE, x, y) > 0)) { biome = BIOME_COLD_TAIGA; } else if (tile.getTerrain(x, y) == Terrain.WATER) { biome = BIOME_FROZEN_RIVER; } else { int waterLevel = tile.getWaterLevel(x, y) - tile.getIntHeight(x, y); if ((waterLevel > 0) && (! tile.getBitLayerValue(FloodWithLava.INSTANCE, x, y))) { if (waterLevel <= 5) { biome = BIOME_FROZEN_RIVER; } else { biome = BIOME_FROZEN_OCEAN; } } else { biome = BIOME_ICE_PLAINS; } } } else { if (tile.getBitLayerValue(River.INSTANCE, x, y)) { biome = BIOME_RIVER; } else if (tile.getLayerValue(SwampLand.INSTANCE, x, y) > 0) { biome = BIOME_SWAMPLAND; } else if (tile.getLayerValue(Jungle.INSTANCE, x, y) > 0) { biome = BIOME_JUNGLE; } else { int waterLevel = tile.getWaterLevel(x, y) - tile.getIntHeight(x, y); if ((waterLevel > 0) && (! tile.getBitLayerValue(FloodWithLava.INSTANCE, x, y))) { if (waterLevel <= 5) { biome = BIOME_RIVER; } else if (waterLevel <= 20) { biome = BIOME_OCEAN; } else { biome = BIOME_DEEP_OCEAN; } } else { final Terrain terrain = tile.getTerrain(x, y); // TODO: we have reports from the wild of the custom terrain // returned here somehow not being configured, so check that // even though we don't understand how that could happen final int defaultBiome = terrain.isConfigured() ? terrain.getDefaultBiome() : BIOME_PLAINS; if (((tile.getLayerValue(DeciduousForest.INSTANCE, x, y) > 0) || (tile.getLayerValue(PineForest.INSTANCE, x, y) > 0)) && (defaultBiome != BIOME_DESERT) && (defaultBiome != BIOME_DESERT_HILLS) && (defaultBiome != BIOME_DESERT_M) && (defaultBiome != BIOME_MESA) && (defaultBiome != BIOME_MESA_BRYCE) && (defaultBiome != BIOME_MESA_PLATEAU) && (defaultBiome != BIOME_MESA_PLATEAU_F) && (defaultBiome != BIOME_MESA_PLATEAU_F_M) && (defaultBiome != BIOME_MESA_PLATEAU_M)) { biome = BIOME_FOREST; } else { biome = defaultBiome; } } } } return biome; } /** * Get a snapshot of the current state of this dimension. If you want this * snapshot to be truly static, you must execute a savepoint on the undo * manager after this. * * @return A snapshot of the current state of this dimension. */ public Dimension getSnapshot() { if (undoManager == null) { throw new IllegalStateException("No undo manager installed"); } return new DimensionSnapshot(this, undoManager.getSnapshot()); } public int getTopLayerDepth(int x, int y, int z) { return topLayerMinDepth + Math.round((topLayerDepthNoise.getPerlinNoise(x / SMALL_BLOBS, y / SMALL_BLOBS, z / SMALL_BLOBS) + 0.5f) * topLayerVariation); } void ensureAllReadable() { tiles.values().forEach(org.pepsoft.worldpainter.Tile::ensureAllReadable); } public void addDimensionListener(Listener listener) { listeners.add(listener); } public void removeDimensionListener(Listener listener) { listeners.remove(listener); } public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(propertyName, listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(propertyName, listener); } /** * Transform the tiles of this dimension horizontally, for instance * translating and/or rotating them. If an undo manager is installed this * operation will destroy all undo info. * * @param transform The transform to apply to the tiles and their contents. * @param progressReceiver An optional progress receiver to which progress * of the operation will be reported. * @throws OperationCancelled If the progress receiver threw an * <code>OperationCancelled</code> exception indicating that the user wished * to cancel the operation. */ public void transform(CoordinateTransform transform, ProgressReceiver progressReceiver) throws OperationCancelled { if (progressReceiver != null) { progressReceiver.setMessage("transforming " + getName() + "..."); } eventsInhibited = true; try { addedTiles.clear(); removedTiles.clear(); dirtyTiles.clear(); dirty = true; Rectangle overlayCoords = null; if ((overlay != null) && overlay.canRead()) { try { java.awt.Dimension overlaySize = getImageSize(overlay); overlayCoords = new Rectangle(overlayOffsetX + (lowestX << TILE_SIZE_BITS), overlayOffsetY + (lowestY << TILE_SIZE_BITS), Math.round(overlaySize.width * overlayScale), Math.round(overlaySize.height * overlayScale)); } catch (IOException e) { // Don't bother user with it, just clear the overlay logger.error("I/O error while trying to determine size of " + overlay, e); overlay = null; overlayEnabled = false; overlayOffsetX = 0; overlayOffsetY = 0; overlayScale = 1.0f; } } else { overlay = null; overlayEnabled = false; overlayOffsetX = 0; overlayOffsetY = 0; overlayScale = 1.0f; } // Remove all tiles Set<Tile> removedTiles; synchronized (this) { Map<Point, Tile> oldTiles = tiles; tiles = new HashMap<>(); removedTiles = new HashSet<>(oldTiles.values()); for (Tile removedTile: removedTiles) { removedTile.removeListener(this); removedTile.unregister(); } } clearUndo(); for (Listener listener: listeners) { listener.tilesRemoved(this, removedTiles); } // Add them all back, in their transformed locations lowestX = Integer.MAX_VALUE; highestX = Integer.MIN_VALUE; lowestY = Integer.MAX_VALUE; highestY = Integer.MIN_VALUE; int tileCount = removedTiles.size(), tileNo = 0; for (Iterator<Tile> i = removedTiles.iterator(); i.hasNext(); ) { Tile tile = i.next(); addTile(tile.transform(transform)); i.remove(); // Remove each tile as we're done with it so it can be garbage collected tileNo++; if (progressReceiver != null) { progressReceiver.setProgress((float) tileNo / tileCount); } } if (overlayCoords != null) { overlayCoords = transform.transform(overlayCoords); overlayOffsetX = overlayCoords.x - (lowestX << TILE_SIZE_BITS); overlayOffsetY = overlayCoords.y - (lowestY << TILE_SIZE_BITS); SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(null, "The " + getName() + " dimension has an overlay image!\n" + "The coordinates have been adjusted for you,\n" + "but you need to rotate the actual image yourself\n" + "using a paint program.", "Adjust Overlay Image", JOptionPane.INFORMATION_MESSAGE)); } } finally { eventsInhibited = false; fireTilesAdded(addedTiles); } } public Stream<Tile> streamTiles() { return tiles.values().stream(); } public boolean containsOneOf(Layer... layers) { for (Tile tile: tiles.values()) { if (tile.containsOneOf(layers)) { return true; } } return false; } // Tile.Listener @Override public void heightMapChanged(Tile tile) { dirty = true; } @Override public void terrainChanged(Tile tile) { dirty = true; } @Override public void waterLevelChanged(Tile tile) { dirty = true; } @Override public void seedsChanged(Tile tile) { dirty = true; } @Override public void layerDataChanged(Tile tile, Set<Layer> changedLayers) { dirty = true; } @Override public void allBitLayerDataChanged(Tile tile) { dirty = true; } @Override public void allNonBitlayerDataChanged(Tile tile) { dirty = true; } private void fireTileAdded(Tile tile) { if (eventsInhibited) { addedTiles.add(tile); } else { Set<Tile> tiles = Collections.singleton(tile); for (Listener listener: listeners) { listener.tilesAdded(this, tiles); } } } private void fireTileRemoved(Tile tile) { if (eventsInhibited) { removedTiles.add(tile); } else { Set<Tile> tiles = Collections.singleton(tile); for (Listener listener: listeners) { listener.tilesRemoved(this, tiles); } } } private void fireTilesAdded(Set<Tile> tiles) { if (eventsInhibited) { addedTiles.addAll(tiles); } else { for (Listener listener: listeners) { listener.tilesAdded(this, tiles); } } } private void fireTilesRemoved(Set<Tile> tiles) { if (eventsInhibited) { removedTiles.addAll(tiles); } else { for (Listener listener: listeners) { listener.tilesRemoved(this, tiles); } } } private java.awt.Dimension getImageSize(File image) throws IOException { String filename = image.getName(); int p = filename.lastIndexOf('.'); if (p == -1) { return null; } String suffix = filename.substring(p + 1).toLowerCase(); Iterator<ImageReader> readers = ImageIO.getImageReadersBySuffix(suffix); if (readers.hasNext()) { ImageReader reader = readers.next(); try { try (ImageInputStream in = new FileImageInputStream(image)) { reader.setInput(in); int width = reader.getWidth(reader.getMinIndex()); int height = reader.getHeight(reader.getMinIndex()); return new java.awt.Dimension(width, height); } } finally { reader.dispose(); } } else { return null; } } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); tileCache = new ThreadLocal<TileCache>() { @Override protected TileCache initialValue() { return new TileCache(); } }; init(); } private void init() { listeners = new ArrayList<>(); dirtyTiles = new HashSet<>(); addedTiles = new HashSet<>(); removedTiles = new HashSet<>(); propertyChangeSupport = new PropertyChangeSupport(this); garden = new WPGarden(); topLayerDepthNoise = new PerlinNoise(seed + TOP_LAYER_DEPTH_SEED_OFFSET); for (Tile tile: tiles.values()) { tile.addListener(this); Set<Seed> seeds = tile.getSeeds(); if (seeds != null) { for (Seed gardenSeed: seeds) { gardenSeed.garden = garden; } } } // Legacy support if (wpVersion < 1) { if (borderSize == 0) { borderSize = 2; } if (overlayScale == 0.0f) { overlayScale = 1.0f; } if (overlayTransparency == 0.0f) { overlayTransparency = 0.5f; } if (gridSize == 0) { gridSize = 128; } if (! biomesConverted) { // Convert the nibble sized biomes data from a legacy map (by // deleting it so that it will be recalculated tiles.values().forEach(org.pepsoft.worldpainter.Tile::convertBiomeData); biomesConverted = true; } if (maxHeight == 0) { maxHeight = 128; } if (subsurfaceMaterial == Terrain.RESOURCES) { subsurfaceMaterial = Terrain.STONE; // Load legacy settings ResourcesExporterSettings settings = new ResourcesExporterSettings(maxHeight); settings.setChance(BLK_GOLD_ORE, 1); settings.setChance(BLK_IRON_ORE, 5); settings.setChance(BLK_COAL, 9); settings.setChance(BLK_LAPIS_LAZULI_ORE, 1); settings.setChance(BLK_DIAMOND_ORE, 1); settings.setChance(BLK_REDSTONE_ORE, 6); settings.setChance(BLK_WATER, 1); settings.setChance(BLK_LAVA, 1); settings.setChance(BLK_DIRT, 9); settings.setChance(BLK_GRAVEL, 9); settings.setMaxLevel(BLK_GOLD_ORE, Terrain.GOLD_LEVEL); settings.setMaxLevel(BLK_IRON_ORE, Terrain.IRON_LEVEL); settings.setMaxLevel(BLK_COAL, Terrain.COAL_LEVEL); settings.setMaxLevel(BLK_LAPIS_LAZULI_ORE, Terrain.LAPIS_LAZULI_LEVEL); settings.setMaxLevel(BLK_DIAMOND_ORE, Terrain.DIAMOND_LEVEL); settings.setMaxLevel(BLK_REDSTONE_ORE, Terrain.REDSTONE_LEVEL); settings.setMaxLevel(BLK_WATER, Terrain.WATER_LEVEL); settings.setMaxLevel(BLK_LAVA, Terrain.LAVA_LEVEL); settings.setMaxLevel(BLK_DIRT, Terrain.DIRT_LEVEL); settings.setMaxLevel(BLK_GRAVEL, Terrain.GRAVEL_LEVEL); layerSettings.put(Resources.INSTANCE, settings); } if (contourSeparation == 0) { contourSeparation = 10; } if (topLayerMinDepth == 0) { topLayerMinDepth = 3; topLayerVariation = 4; } if (lastViewPosition == null) { lastViewPosition = new Point(); } if ((customLayers == null) || customLayers.isEmpty()) { // The customLayers.isEmpty() is to fix a bug which escaped in a beta customLayers = new ArrayList<>(); customLayers.addAll(getAllLayers(false).stream().filter(layer -> layer instanceof CustomLayer).map(layer -> (CustomLayer) layer).collect(Collectors.toList())); } } if (wpVersion < 2) { if (overlay != null) { fixOverlayCoords = true; } } if (wpVersion < 3) { ceilingHeight = maxHeight; } wpVersion = CURRENT_WP_VERSION; // Make sure that any custom layers which somehow ended up in the world // are on the custom layer list so they will be added to a palette in // the GUI. TODO: fix this properly // Make sure customLayers isn't some weird read-only list getAllLayers(false).stream() .filter(layer -> (layer instanceof CustomLayer) && (! customLayers.contains(layer))) .forEach(layer -> { if ((! (customLayers instanceof ArrayList)) && (! (customLayers instanceof LinkedList))) { // Make sure customLayers isn't some weird read-only list customLayers = new ArrayList<>(customLayers); } customLayers.add((CustomLayer) layer); }); } private World2 world; private final long seed; private final int dim; private Map<Point, Tile> tiles = new HashMap<>(); private final TileFactory tileFactory; private int lowestX = Integer.MAX_VALUE, highestX = Integer.MIN_VALUE, lowestY = Integer.MAX_VALUE, highestY = Integer.MIN_VALUE; private Terrain subsurfaceMaterial = Terrain.STONE_MIX; private boolean populate; private Border border; private int borderLevel = 62, borderSize = 2; private boolean darkLevel, bedrockWall; private Map<Layer, ExporterSettings> layerSettings = new HashMap<>(); private long minecraftSeed = Long.MIN_VALUE; private File overlay; private float overlayScale = 1.0f, overlayTransparency = 0.5f; private int overlayOffsetX, overlayOffsetY, gridSize = 128; private boolean overlayEnabled, gridEnabled, biomesConverted = true; private int maxHeight = World2.DEFAULT_MAX_HEIGHT, contourSeparation = 10; private boolean contoursEnabled = true; private int topLayerMinDepth = 3, topLayerVariation = 4; private boolean bottomless; private Point lastViewPosition = new Point(); private List<CustomBiome> customBiomes; private boolean coverSteepTerrain = true; private List<CustomLayer> customLayers = new ArrayList<>(); private int wpVersion = CURRENT_WP_VERSION; private boolean fixOverlayCoords; private int ceilingHeight = maxHeight; private transient List<Listener> listeners = new ArrayList<>(); private transient boolean eventsInhibited; private transient Set<Tile> dirtyTiles = new HashSet<>(); private transient Set<Tile> addedTiles = new HashSet<>(); private transient Set<Tile> removedTiles = new HashSet<>(); private transient boolean dirty; private transient UndoManager undoManager; private transient PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); private transient WPGarden garden = new WPGarden(); private transient PerlinNoise topLayerDepthNoise; private transient ThreadLocal<TileCache> tileCache = new ThreadLocal<TileCache>() { @Override protected TileCache initialValue() { return new TileCache(); } }; public static final int[] POSSIBLE_AUTO_BIOMES = {BIOME_PLAINS, BIOME_FOREST, BIOME_SWAMPLAND, BIOME_JUNGLE, BIOME_MESA, BIOME_DESERT, BIOME_BEACH, BIOME_RIVER, BIOME_OCEAN, BIOME_DEEP_OCEAN, BIOME_ICE_PLAINS, BIOME_COLD_TAIGA, BIOME_FROZEN_RIVER, BIOME_FROZEN_OCEAN, BIOME_MUSHROOM_ISLAND, BIOME_HELL, BIOME_SKY}; private static final long TOP_LAYER_DEPTH_SEED_OFFSET = 180728193; private static final float ROOT_EIGHT = (float) Math.sqrt(8.0); private static final int CURRENT_WP_VERSION = 3; private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Dimension.class); private static final long serialVersionUID = 2011062401L; public interface Listener { void tilesAdded(Dimension dimension, Set<Tile> tiles); void tilesRemoved(Dimension dimension, Set<Tile> tiles); } public enum Border { VOID(false), WATER(false), LAVA(false), ENDLESS_VOID(true), ENDLESS_WATER(true), ENDLESS_LAVA(true); Border(boolean endless) { this.endless = endless; } public boolean isEndless() { return endless; } private final boolean endless; } private class WPGarden implements Garden { @Override public void clearLayer(int x, int y, Layer layer, int radius) { for (int dx = -radius; dx <= radius; dx++) { for (int dy = -radius; dy <= radius; dy++) { setLayerValueAt(layer, x + dx, y + dy, 0); } } } @Override public void setCategory(int x, int y, int category) { setLayerValueAt(GardenCategory.INSTANCE, x, y, category); } @Override public int getCategory(int x, int y) { return getLayerValueAt(GardenCategory.INSTANCE, x, y); } @Override public Set<Seed> getSeeds() { Set<Seed> allSeeds = new HashSet<>(); for (Tile tile: tiles.values()) { allSeeds.addAll(tile.getSeeds()); } return allSeeds; } @SuppressWarnings("unchecked") @Override public <T extends Seed> List<T> findSeeds(Class<T> type, int x, int y, int radius) { List<T> seedsFound = new ArrayList<>(); int topLeftTileX = (x - radius) >> 7; int topLeftTileY = (y - radius) >> 7; int bottomRightTileX = (x + radius) >> 7; int bottomRightTileY = (y + radius) >> 7; // System.out.println("Finding seeds of type " + type.getSimpleName() + " " + radius + " blocks around " + x + "," + y + " in " + ((bottomRightTileX - topLeftTileX + 1) * (bottomRightTileY - topLeftTileY + 1)) + " tiles"); for (int tileX = topLeftTileX; tileX <= bottomRightTileX; tileX++) { for (int tileY = topLeftTileY; tileY <= bottomRightTileY; tileY++) { Tile tile = getTile(tileX, tileY); if (tile != null) { Set<Seed> seeds = tile.getSeeds(); if (seeds != null) { seeds.stream() .filter(seed -> seed.getClass() == type) .forEach(seed -> { int distance = (int) MathUtils.getDistance(seed.location.x - x, seed.location.y - y); if (distance <= radius) { seedsFound.add((T) seed); } }); } } } } return seedsFound; } @Override public boolean isOccupied(int x, int y) { return (getLayerValueAt(GardenCategory.INSTANCE, x, y) != 0) || (getWaterLevelAt(x, y) > getIntHeightAt(x, y)); } @Override public boolean isWater(int x, int y) { return (getLayerValueAt(GardenCategory.INSTANCE, x, y) == GardenCategory.CATEGORY_WATER) || ((getWaterLevelAt(x, y) > getIntHeightAt(x, y)) && (! getBitLayerValueAt(FloodWithLava.INSTANCE, x, y))); } @Override public boolean isLava(int x, int y) { return (getWaterLevelAt(x, y) > getIntHeightAt(x, y)) && getBitLayerValueAt(FloodWithLava.INSTANCE, x, y); } @Override public boolean plantSeed(Seed seed) { Point3i location = seed.getLocation(); if ((location.x < lowestX * TILE_SIZE) || (location.x > (highestX + 1) * TILE_SIZE - 1) || (location.y < lowestY * TILE_SIZE) || (location.y > (highestY + 1) * TILE_SIZE - 1)) { return false; } Tile tile = getTileForEditing(location.x >> TILE_SIZE_BITS, location.y >> TILE_SIZE_BITS); if ((tile != null) && tile.plantSeed(seed)) { activeTiles.add(new Point(location.x >> TILE_SIZE_BITS, location.y >> TILE_SIZE_BITS)); return true; } else { return false; } } @Override public void removeSeed(Seed seed) { Point3i location = seed.getLocation(); if ((location.x < lowestX * TILE_SIZE) || (location.x > (highestX + 1) * TILE_SIZE - 1) || (location.y < lowestY * TILE_SIZE) || (location.y > (highestY + 1) * TILE_SIZE - 1)) { return; } Tile tile = getTileForEditing(location.x >> 7, location.y >> 7); if (tile != null) { tile.removeSeed(seed); } } @Override public float getHeight(int x, int y) { return getHeightAt(x, y); } @Override public int getIntHeight(int x, int y) { return getIntHeightAt(x, y); } @Override @SuppressWarnings("unchecked") // Guaranteed by Java public boolean tick() { // Tick all seeds in active tiles. Clone the active tiles set, and // the seed sets from the tiles, because they may change out from // under us for (Point tileCoords: (HashSet<Point>) activeTiles.clone()) { Tile tile = getTile(tileCoords.x, tileCoords.y); if (tile != null) { ((HashSet<Seed>) tile.getSeeds().clone()) .forEach(Seed::tick); } } // Don't cache active seeds, because they might have changed // Groom active tile list, and determine whether all seeds are // finished (have either sprouted or died) boolean finished = true; for (Iterator<Point> i = activeTiles.iterator(); i.hasNext(); ) { Point tileCoords = i.next(); Tile tile = getTile(tileCoords.x, tileCoords.y); boolean tileFinished = true; if (tile != null) { for (Seed seed: tile.getSeeds()) { if (! seed.isFinished()) { tileFinished = false; break; } } } if (tileFinished) { i.remove(); } else { finished = false; } } return finished; } @Override public void neutralise() { for (Point tileCoords: activeTiles) { Tile tile = getTile(tileCoords.x, tileCoords.y); if (tile != null) { tile.getSeeds().stream() .filter(seed -> !seed.isFinished()) .forEach(Seed::neutralise); } } activeTiles.clear(); } private final HashSet<Point> activeTiles = new HashSet<>(); } static class TileCache { int x = Integer.MIN_VALUE, y = Integer.MIN_VALUE; Tile tile; } }