package org.pepsoft.minecraft; import org.jnbt.CompoundTag; import org.jnbt.NBTInputStream; import org.pepsoft.util.MathUtils; import org.pepsoft.util.swing.TileListener; import org.pepsoft.util.swing.TileProvider; import org.pepsoft.worldpainter.ColourScheme; import org.pepsoft.worldpainter.colourschemes.DynMapColourScheme; import sun.awt.image.IntegerInterleavedRaster; import java.awt.*; import java.awt.image.BufferedImage; import java.io.DataInputStream; import java.io.File; import java.io.IOException; import java.util.*; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.pepsoft.minecraft.Constants.SUPPORTED_VERSION_1; import static org.pepsoft.util.swing.TiledImageViewer.TILE_SIZE; /** * Created by Pepijn Schmitz on 27-10-16. */ public class MinecraftMapTileProvider implements TileProvider { public MinecraftMapTileProvider(File mapDir) throws IOException { this.mapDir = mapDir; // Read the metadata Level level = Level.load(new File(mapDir, "level.dat")); maxHeight = level.getMaxHeight(); version = level.getVersion(); // Scan the region files to determine a rough extent File regionDir = new File(mapDir, "region"); Pattern regionFilePattern = (version == SUPPORTED_VERSION_1) ? Pattern.compile("r\\.(-?\\d+)\\.(-?\\d+)\\.mcr") : Pattern.compile("r\\.(-?\\d+)\\.(-?\\d+)\\.mca"); File[] regionFiles = regionDir.listFiles((dir, name) -> regionFilePattern.matcher(name).matches()); if ((regionFiles != null) && (regionFiles.length > 0)) { int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, minZ = Integer.MAX_VALUE, maxZ = Integer.MIN_VALUE; for (File file: regionFiles) { Matcher matcher = regionFilePattern.matcher(file.getName()); matcher.matches(); int x = Integer.parseInt(matcher.group(1)); int z = Integer.parseInt(matcher.group(2)); MinecraftMapTileProvider.this.fileCache.put(new Point(x, z), file); if (x < minX) { minX = x; } if (x > maxX) { maxX = x; } if (z < minZ) { minZ = z; } if (z > maxZ) { maxZ = z; } } extent = new Rectangle(minX << 2, minZ << 2, (maxX - minX + 1) << 2, (maxZ - minZ + 1) << 2); } else { extent = null; } colourScheme = new DynMapColourScheme("default", true); } @Override public int getTileSize() { return TILE_SIZE; } @Override public boolean isTilePresent(int x, int y) { if (zoom == 0) { Point regionCoords = new Point(x >> 2, y >> 2); return fileCache.containsKey(regionCoords); } else { return true; } } @Override public boolean paintTile(Image tileImage, int x, int y, int dx, int dy) { final BufferedImage image = renderBufferRef.get(); Arrays.fill(((IntegerInterleavedRaster) image.getRaster()).getDataStorage(), 0); int scale = MathUtils.pow(2, -zoom); final int chunkX1 = x * 8 * scale, chunkY1 = y * 8 * scale; final int chunkX2 = chunkX1 + 8 * scale - 1, chunkY2 = chunkY1 + 8 * scale - 1; int previousRegionX = Integer.MIN_VALUE, previousRegionY = Integer.MIN_VALUE; RegionFile previousRegion = null; final int step = Math.max(scale / 16, 1); for (int chunkX = chunkX1; chunkX <= chunkX2; chunkX += step) { for (int chunkY = chunkY1; chunkY <= chunkY2; chunkY += step) { try { int regionX = chunkX >> 5, regionY = chunkY >> 5; RegionFile region; if ((regionX != previousRegionX) || (regionY != previousRegionY)) { region = getRegionFile(regionX, regionY); previousRegion = region; previousRegionX = regionX; previousRegionY = regionY; } else { region = previousRegion; } if (region == null) { continue; } DataInputStream dataIn = region.getChunkDataInputStream(chunkX & 0x1f, chunkY & 0x1f); if (dataIn != null) { Chunk chunk; try (NBTInputStream in = new NBTInputStream(dataIn)) { chunk = (version == Constants.SUPPORTED_VERSION_2) ? new ChunkImpl2((CompoundTag) in.readTag(), maxHeight) : new ChunkImpl((CompoundTag) in.readTag(), maxHeight); } for (int blockX = 0; blockX < 16; blockX += scale) { for (int blockY = 0; blockY < 16; blockY += scale) { image.setRGB((((chunkX - chunkX1) << 4) | blockX) / scale, (((chunkY - chunkY1) << 4) | blockY) / scale, 0xff000000 | getColour(chunk, blockX, blockY)); } } } } catch (IOException e) { throw new RuntimeException("I/O error while reading chunk data", e); } } } Graphics2D g2 = (Graphics2D) tileImage.getGraphics(); try { g2.setComposite(AlphaComposite.Src); g2.drawImage(image, dx, dy, null); } finally { g2.dispose(); } return true; } @Override public int getTilePriority(int x, int y) { return 0; // All tiles have equal priority } @Override public Rectangle getExtent() { return extent; } @Override public void addTileListener(TileListener tileListener) { listeners.add(tileListener); } @Override public void removeTileListener(TileListener tileListener) { listeners.remove(tileListener); } @Override public boolean isZoomSupported() { return true; } @Override public int getZoom() { return zoom; } @Override public void setZoom(int zoom) { if (zoom != this.zoom) { this.zoom = zoom; } } private synchronized RegionFile getRegionFile(int x, int y) throws IOException { Point coords = new Point(x, y); RegionFile regionFile = regionFileCache.get(coords); if (regionFile == null) { if (fileCache.containsKey(coords)) { regionFile = new RegionFile(fileCache.get(coords), true); } else { regionFile = NULL; } regionFileCache.put(coords, regionFile); } if (regionFile == NULL) { return null; } else { return regionFile; } } private int getColour(Chunk chunk, int x, int y) { for (int z = maxHeight - 1; z >= 0; z--) { int blockType = chunk.getBlockType(x, z, y); if (blockType != Constants.BLK_AIR) { return colourScheme.getColour(blockType, chunk.getDataValue(x, z, y)); } } return DEFAULT_VOID_COLOUR; } private final File mapDir; private final int maxHeight, version; private final ColourScheme colourScheme; private final List<TileListener> listeners = new ArrayList<>(); private final Map<Point, File> fileCache = new HashMap<>(); private final Map<Point, RegionFile> regionFileCache = new HashMap<>(); private final Rectangle extent; private int zoom = 0; private static final int DEFAULT_VOID_COLOUR = 0x00FFFF; private static final RegionFile NULL = new RegionFile(); private static final ThreadLocal<BufferedImage> renderBufferRef = new ThreadLocal<BufferedImage>() { @Override protected BufferedImage initialValue() { return new BufferedImage(TILE_SIZE, TILE_SIZE, BufferedImage.TYPE_INT_ARGB); } }; }