package org.dynmap.flat; import org.dynmap.DynmapWorld; import static org.dynmap.JSONUtils.a; import static org.dynmap.JSONUtils.s; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.dynmap.Client; import org.dynmap.Color; import org.dynmap.ColorScheme; import org.dynmap.ConfigurationNode; import org.dynmap.DynmapChunk; import org.dynmap.DynmapCore; import org.dynmap.DynmapLocation; import org.dynmap.DynmapCore.CompassMode; import org.dynmap.MapManager; import org.dynmap.TileHashManager; import org.dynmap.MapTile; import org.dynmap.MapType; import org.dynmap.debug.Debug; import org.dynmap.utils.DynmapBufferedImage; import org.dynmap.utils.FileLockManager; import org.dynmap.utils.MapChunkCache; import org.dynmap.utils.MapIterator; import org.dynmap.utils.MapIterator.BlockStep; import org.json.simple.JSONObject; public class FlatMap extends MapType { private ConfigurationNode configuration; private String prefix; private String name; private ColorScheme colorScheme; private int maximumHeight = 127; private int ambientlight = 15;; private int shadowscale[] = null; private boolean night_and_day; /* If true, render both day (prefix+'-day') and night (prefix) tiles */ protected boolean transparency; private enum Texture { NONE, SMOOTH, DITHER }; private Texture textured = Texture.NONE; private boolean isbigmap; public FlatMap(DynmapCore core, ConfigurationNode configuration) { this.configuration = configuration; name = configuration.getString("name", null); prefix = configuration.getString("prefix", name); colorScheme = ColorScheme.getScheme(core, (String) configuration.get("colorscheme")); Object o = configuration.get("maximumheight"); if (o != null) { maximumHeight = Integer.parseInt(String.valueOf(o)); if (maximumHeight > 127) maximumHeight = 127; } o = configuration.get("shadowstrength"); if(o != null) { double shadowweight = Double.parseDouble(String.valueOf(o)); if(shadowweight > 0.0) { shadowscale = new int[16]; shadowscale[15] = 256; /* Normal brightness weight in MC is a 20% relative dropoff per step */ for(int i = 14; i >= 0; i--) { double v = shadowscale[i+1] * (1.0 - (0.2 * shadowweight)); shadowscale[i] = (int)v; if(shadowscale[i] > 256) shadowscale[i] = 256; if(shadowscale[i] < 0) shadowscale[i] = 0; } } } o = configuration.get("ambientlight"); if(o != null) { ambientlight = Integer.parseInt(String.valueOf(o)); } night_and_day = configuration.getBoolean("night-and-day", false); transparency = configuration.getBoolean("transparency", false); /* Default off */ String tex = configuration.getString("textured", "none"); if(tex.equals("none")) textured = Texture.NONE; else if(tex.equals("dither")) textured = Texture.DITHER; else textured = Texture.SMOOTH; isbigmap = configuration.getBoolean("isbigmap", false); } @Override public MapTile[] getTiles(DynmapWorld w, int x, int y, int z) { return new MapTile[] { new FlatMapTile(w, this, x>>7, z>>7, 128) }; } @Override public MapTile[] getTiles(DynmapWorld w, int xmin, int ymin, int zmin, int xmax, int ymax, int zmax) { ArrayList rslt = new ArrayList(); for(int i = xmin; i <= xmax; i++) { for(int j = zmin; j < zmax; j++) { rslt.add(new FlatMapTile(w, this, i, j, 128)); } } return rslt.toArray(new MapTile[rslt.size()]); } @Override public MapTile[] getAdjecentTiles(MapTile tile) { FlatMapTile t = (FlatMapTile) tile; DynmapWorld w = t.getDynmapWorld(); int x = t.x; int y = t.y; int s = t.size; return new MapTile[] { new FlatMapTile(w, this, x, y - 1, s), new FlatMapTile(w, this, x + 1, y, s), new FlatMapTile(w, this, x, y + 1, s), new FlatMapTile(w, this, x - 1, y, s) }; } @Override public List getRequiredChunks(MapTile tile) { FlatMapTile t = (FlatMapTile) tile; int chunksPerTile = t.size / 16; int sx = t.x * chunksPerTile; int sz = t.y * chunksPerTile; ArrayList result = new ArrayList(chunksPerTile * chunksPerTile); for (int x = 0; x < chunksPerTile; x++) for (int z = 0; z < chunksPerTile; z++) { result.add(new DynmapChunk(sx + x, sz + z)); } return result; } public boolean render(MapChunkCache cache, MapTile tile, File outputFile) { FlatMapTile t = (FlatMapTile) tile; boolean isnether = t.getDynmapWorld().isNether() && (maximumHeight == 127); boolean didwrite = false; Color rslt = new Color(); int[] pixel = new int[4]; int[] pixel_day = null; DynmapBufferedImage im = DynmapBufferedImage.allocateBufferedImage(t.size, t.size); int[] argb_buf = im.argb_buf; DynmapBufferedImage im_day = null; int[] argb_buf_day = null; if(night_and_day) { im_day = DynmapBufferedImage.allocateBufferedImage(t.size, t.size); argb_buf_day = im_day.argb_buf; pixel_day = new int[4]; } MapIterator mapiter = cache.getIterator(t.x * t.size, 127, t.y * t.size); for (int x = 0; x < t.size; x++) { mapiter.initialize(t.x * t.size + x, 127, t.y * t.size); for (int y = 0; y < t.size; y++, mapiter.stepPosition(BlockStep.Z_PLUS)) { int blockType; mapiter.setY(127); if(isnether) { while((blockType = mapiter.getBlockTypeID()) != 0) { mapiter.stepPosition(BlockStep.Y_MINUS); if(mapiter.getY() < 0) { /* Solid - use top */ mapiter.setY(127); blockType = mapiter.getBlockTypeID(); break; } } if(blockType == 0) { /* Hit air - now find non-air */ while((blockType = mapiter.getBlockTypeID()) == 0) { mapiter.stepPosition(BlockStep.Y_MINUS); if(mapiter.getY() < 0) { mapiter.setY(0); break; } } } } else { int my = mapiter.getHighestBlockYAt(); if(my > maximumHeight) my = maximumHeight; mapiter.setY(my); blockType = mapiter.getBlockTypeID(); if(blockType == 0) { /* If air, go down one - fixes ice */ my--; if(my < 0) continue; mapiter.setY(my); blockType = mapiter.getBlockTypeID(); } } int data = 0; Color[] colors = colorScheme.colors[blockType]; if(colorScheme.datacolors[blockType] != null) { data = mapiter.getBlockData(); colors = colorScheme.datacolors[blockType][data]; } if (colors == null) continue; Color c; if(textured == Texture.SMOOTH) c = colors[4]; else if((textured == Texture.DITHER) && (((x+y) & 0x01) == 1)) { c = colors[2]; } else { c = colors[0]; } if (c == null) continue; pixel[0] = c.getRed(); pixel[1] = c.getGreen(); pixel[2] = c.getBlue(); pixel[3] = c.getAlpha(); /* If transparency needed, process it */ if(transparency && (pixel[3] < 255)) { process_transparent(pixel, pixel_day, mapiter); } /* If ambient light less than 15, do scaling */ else if((shadowscale != null) && (ambientlight < 15)) { if(mapiter.getY() < 127) mapiter.stepPosition(BlockStep.Y_PLUS); if(night_and_day) { /* Use unscaled color for day (no shadows from above) */ pixel_day[0] = pixel[0]; pixel_day[1] = pixel[1]; pixel_day[2] = pixel[2]; pixel_day[3] = 255; } int light = Math.max(ambientlight, mapiter.getBlockEmittedLight()); pixel[0] = (pixel[0] * shadowscale[light]) >> 8; pixel[1] = (pixel[1] * shadowscale[light]) >> 8; pixel[2] = (pixel[2] * shadowscale[light]) >> 8; pixel[3] = 255; } else { /* Only do height keying if we're not messing with ambient light */ boolean below = mapiter.getY() < 64; // Make height range from 0 - 1 (1 - 0 for below and 0 - 1 above) float height = (below ? 64 - mapiter.getY() : mapiter.getY() - 64) / 64.0f; // Defines the 'step' in coloring. float step = 10 / 128.0f; // The step applied to height. float scale = ((int)(height/step))*step; // Make the smaller values change the color (slightly) more than the higher values. scale = (float)Math.pow(scale, 1.1f); // Don't let the color go fully white or fully black. scale *= 0.8f; if (below) { pixel[0] -= pixel[0] * scale; pixel[1] -= pixel[1] * scale; pixel[2] -= pixel[2] * scale; pixel[3] = 255; } else { pixel[0] += (255-pixel[0]) * scale; pixel[1] += (255-pixel[1]) * scale; pixel[2] += (255-pixel[2]) * scale; pixel[3] = 255; } if(night_and_day) { pixel_day[0] = pixel[0]; pixel_day[1] = pixel[1]; pixel_day[2] = pixel[2]; pixel_day[3] = 255; } } rslt.setRGBA(pixel[0], pixel[1], pixel[2], pixel[3]); argb_buf[(t.size-y-1) + (x*t.size)] = rslt.getARGB(); if(night_and_day) { rslt.setRGBA(pixel_day[0], pixel_day[1], pixel_day[2], pixel[3]); argb_buf_day[(t.size-y-1) + (x*t.size)] = rslt.getARGB(); } } } /* Test to see if we're unchanged from older tile */ TileHashManager hashman = MapManager.mapman.hashman; long crc = hashman.calculateTileHash(argb_buf); boolean tile_update = false; FileLockManager.getWriteLock(outputFile); try { if((!outputFile.exists()) || (crc != hashman.getImageHashCode(tile.getKey(prefix), null, t.x, t.y))) { /* Wrap buffer as buffered image */ Debug.debug("saving image " + outputFile.getPath()); if(!outputFile.getParentFile().exists()) outputFile.getParentFile().mkdirs(); try { FileLockManager.imageIOWrite(im.buf_img, ImageFormat.FORMAT_PNG, outputFile); } catch (IOException e) { Debug.error("Failed to save image: " + outputFile.getPath(), e); } catch (java.lang.NullPointerException e) { Debug.error("Failed to save image (NullPointerException): " + outputFile.getPath(), e); } MapManager.mapman.pushUpdate(tile.getDynmapWorld(), new Client.Tile(tile.getFilename())); hashman.updateHashCode(tile.getKey(prefix), null, t.x, t.y, crc); tile.getDynmapWorld().enqueueZoomOutUpdate(outputFile); tile_update = true; didwrite = true; } else { Debug.debug("skipping image " + outputFile.getPath() + " - hash match"); } } finally { FileLockManager.releaseWriteLock(outputFile); DynmapBufferedImage.freeBufferedImage(im); } MapManager.mapman.updateStatistics(tile, prefix, true, tile_update, true); /* If day too, handle it */ if(night_and_day) { File dayfile = new File(tile.getDynmapWorld().worldtilepath, tile.getDayFilename()); crc = hashman.calculateTileHash(argb_buf_day); FileLockManager.getWriteLock(dayfile); try { if((!dayfile.exists()) || (crc != hashman.getImageHashCode(tile.getKey(prefix), "day", t.x, t.y))) { Debug.debug("saving image " + dayfile.getPath()); if(!dayfile.getParentFile().exists()) dayfile.getParentFile().mkdirs(); try { FileLockManager.imageIOWrite(im_day.buf_img, ImageFormat.FORMAT_PNG, dayfile); } catch (IOException e) { Debug.error("Failed to save image: " + dayfile.getPath(), e); } catch (java.lang.NullPointerException e) { Debug.error("Failed to save image (NullPointerException): " + dayfile.getPath(), e); } MapManager.mapman.pushUpdate(tile.getDynmapWorld(), new Client.Tile(tile.getDayFilename())); hashman.updateHashCode(tile.getKey(prefix), "day", t.x, t.y, crc); tile.getDynmapWorld().enqueueZoomOutUpdate(dayfile); tile_update = true; didwrite = true; } else { Debug.debug("skipping image " + dayfile.getPath() + " - hash match"); tile_update = false; } } finally { FileLockManager.releaseWriteLock(dayfile); DynmapBufferedImage.freeBufferedImage(im_day); } MapManager.mapman.updateStatistics(tile, prefix+"_day", true, tile_update, true); } return didwrite; } private void process_transparent(int[] pixel, int[] pixel_day, MapIterator mapiter) { int r = pixel[0], g = pixel[1], b = pixel[2], a = pixel[3]; int r_day = 0, g_day = 0, b_day = 0, a_day = 0; if(pixel_day != null) { r_day = pixel[0]; g_day = pixel[1]; b_day = pixel[2]; a_day = pixel[3]; } /* Scale alpha to be proportional to iso view (where we go through 4 blocks to go sqrt(6) or 2.45 units of distance */ if(a < 255) a = a_day = 255 - ((255-a)*(255-a) >> 8); /* Handle lighting on cube */ if((shadowscale != null) && (ambientlight < 15)) { boolean did_inc = false; if(mapiter.getY() < 127) { mapiter.stepPosition(BlockStep.Y_PLUS); did_inc = true; } if(night_and_day) { /* Use unscaled color for day (no shadows from above) */ r_day = r; g_day = g; b_day = b; a_day = a; } int light = Math.max(ambientlight, mapiter.getBlockEmittedLight()); r = (r * shadowscale[light]) >> 8; g = (g * shadowscale[light]) >> 8; b = (b * shadowscale[light]) >> 8; if(did_inc) mapiter.stepPosition(BlockStep.Y_MINUS); } if(a < 255) { /* If not opaque */ pixel[0] = pixel[1] = pixel[2] = pixel[3] = 0; if(pixel_day != null) pixel_day[0] = pixel_day[1] = pixel_day[2] = pixel_day[3] = 0; mapiter.stepPosition(BlockStep.Y_MINUS); if(mapiter.getY() >= 0) { int blockType = mapiter.getBlockTypeID(); int data = 0; Color[] colors = colorScheme.colors[blockType]; if(colorScheme.datacolors[blockType] != null) { data = mapiter.getBlockData(); colors = colorScheme.datacolors[blockType][data]; } if (colors != null) { Color c = colors[0]; if (c != null) { pixel[0] = c.getRed(); pixel[1] = c.getGreen(); pixel[2] = c.getBlue(); pixel[3] = c.getAlpha(); } } /* Recurse to resolve color here */ process_transparent(pixel, pixel_day, mapiter); } } /* Blend colors from behind block and block, based on alpha */ r *= a; g *= a; b *= a; int na = 255 - a; pixel[0] = (pixel[0] * na + r) >> 8; pixel[1] = (pixel[1] * na + g) >> 8; pixel[2] = (pixel[2] * na + b) >> 8; pixel[3] = 255; if(pixel_day != null) { r_day *= a_day; g_day *= a_day; b_day *= a_day; na = 255 - a_day; pixel_day[0] = (pixel_day[0] * na + r_day) >> 8; pixel_day[1] = (pixel_day[1] * na + g_day) >> 8; pixel_day[2] = (pixel_day[2] * na + b_day) >> 8; pixel_day[3] = 255; } } public String getName() { return name; } public String getPrefix() { return prefix; } /* Get maps rendered concurrently with this map in this world */ public List getMapsSharingRender(DynmapWorld w) { return Collections.singletonList((MapType)this); } /* Get names of maps rendered concurrently with this map type in this world */ public List getMapNamesSharingRender(DynmapWorld w) { return Collections.singletonList(name); } public List baseZoomFileInfo() { ArrayList s = new ArrayList(); s.add(new ZoomInfo(getPrefix() + "_128", 0)); if(night_and_day) s.add(new ZoomInfo(getPrefix()+"_day_128", 0)); return s; } public int baseZoomFileStepSize() { return 1; } private static final int[] stepseq = { 1, 3, 0, 2 }; public MapStep zoomFileMapStep() { return MapStep.X_PLUS_Y_PLUS; } public int[] zoomFileStepSequence() { return stepseq; } /* How many bits of coordinate are shifted off to make big world directory name */ public int getBigWorldShift() { return 5; } /* Returns true if big world file structure is in effect for this map */ @Override public boolean isBigWorldMap(DynmapWorld w) { return w.bigworld || isbigmap; } public static class FlatMapTile extends MapTile { FlatMap map; public int x; public int y; public int size; private String fname; private String fname_day; public FlatMapTile(DynmapWorld world, FlatMap map, int x, int y, int size) { super(world); this.map = map; this.x = x; this.y = y; this.size = size; } public FlatMapTile(DynmapWorld world, String parm) throws Exception { super(world); String[] parms = parm.split(","); if(parms.length < 4) throw new Exception("wrong parameter count"); this.x = Integer.parseInt(parms[0]); this.y = Integer.parseInt(parms[1]); this.size = Integer.parseInt(parms[2]); for(MapType t : world.maps) { if(t.getName().equals(parms[3]) && (t instanceof FlatMap)) { this.map = (FlatMap)t; break; } } if(this.map == null) throw new Exception("invalid map"); } @Override protected String saveTileData() { return String.format("%d,%d,%d,%s", x, y, size, map.getName()); } @Override public String getFilename() { if(fname == null) { if(world.bigworld) fname = map.prefix + "_" + size + "/" + ((-(y+1))>>5) + "_" + (x>>5) + "/" + -(y+1) + "_" + x + ".png"; else fname = map.prefix + "_" + size + "_" + -(y+1) + "_" + x + ".png"; } return fname; } @Override public String getDayFilename() { if(fname_day == null) { if(world.bigworld) fname_day = map.prefix + "_day_" + size + "/" + ((-(y+1))>>5) + "_" + (x>>5) + "/" + -(y+1) + "_" + x + ".png"; else fname_day = map.prefix + "_day_" + size + "_" + -(y+1) + "_" + x + ".png"; } return fname_day; } public String toString() { return world.getName() + ":" + getFilename(); } @Override public boolean render(MapChunkCache cache, String mapname) { return map.render(cache, this, MapManager.mapman.getTileFile(this)); } @Override public List getRequiredChunks() { return map.getRequiredChunks(this); } @Override public MapTile[] getAdjecentTiles() { return map.getAdjecentTiles(this); } @Override public int hashCode() { return x ^ y ^ size ^ map.getName().hashCode(); } @Override public boolean equals(Object x) { if(x instanceof FlatMapTile) { return equals((FlatMapTile)x); } return false; } public boolean equals(FlatMapTile o) { return (o.x == x) && (o.y == y) && (o.map == map); } @Override public String getKey(String prefix) { return world.getName() + "." + map.getPrefix(); } public boolean isHightestBlockYDataNeeded() { return true; } public boolean isBiomeDataNeeded() { return false; } public boolean isRawBiomeDataNeeded() { return false; } public boolean isBlockTypeDataNeeded() { return true; } public int tileOrdinalX() { return x; } public int tileOrdinalY() { return y; } } @Override public void buildClientConfiguration(JSONObject worldObject, DynmapWorld world) { ConfigurationNode c = configuration; JSONObject o = new JSONObject(); s(o, "type", "FlatMapType"); s(o, "name", c.getString("name")); s(o, "title", c.getString("title")); s(o, "icon", c.getString("icon")); s(o, "prefix", c.getString("prefix")); s(o, "background", c.getString("background")); s(o, "nightandday", c.getBoolean("night-and-day",false)); s(o, "backgroundday", c.getString("backgroundday")); s(o, "backgroundnight", c.getString("backgroundnight")); s(o, "bigmap", this.isBigWorldMap(world)); s(o, "mapzoomin", c.getInteger("mapzoomin", 3)); s(o, "mapzoomout", world.getExtraZoomOutLevels()); if(MapManager.mapman.getCompassMode() != CompassMode.PRE19) s(o, "compassview", "E"); /* Always from east */ else s(o, "compassview", "S"); /* Always from south */ s(o, "image-format", ImageFormat.FORMAT_PNG.getFileExt()); a(worldObject, "maps", o); } }