diff --git a/configuration.txt b/configuration.txt index c765f162..6924b988 100644 --- a/configuration.txt +++ b/configuration.txt @@ -59,6 +59,8 @@ worlds: prefix: t maximumheight: 127 colorscheme: default + # Add shadows to world (based on top-down shadows from chunk data) + # shadowstrength: 1.0 #- class: org.dynmap.kzedmap.HighlightTileRenderer # prefix: ht # maximumheight: 127 diff --git a/src/main/java/org/dynmap/CraftChunkSnapshot.java b/src/main/java/org/dynmap/CraftChunkSnapshot.java new file mode 100644 index 00000000..8ab42381 --- /dev/null +++ b/src/main/java/org/dynmap/CraftChunkSnapshot.java @@ -0,0 +1,106 @@ +package org.dynmap; + +/** + * Represents a static, thread-safe snapshot of chunk of blocks + * Purpose is to allow clean, efficient copy of a chunk data to be made, and then handed off for processing in another thread (e.g. map rendering) + */ +public class CraftChunkSnapshot { + private final int x, z; + private final byte[] buf; /* Flat buffer in uncompressed chunk file format */ + + private static final int BLOCKDATA_OFF = 32768; + private static final int BLOCKLIGHT_OFF = BLOCKDATA_OFF + 16384; + private static final int SKYLIGHT_OFF = BLOCKLIGHT_OFF + 16384; + + /** + * Constructor + */ + CraftChunkSnapshot(int x, int z, byte[] buf) { + this.x = x; + this.z = z; + this.buf = buf; + } + + /** + * Gets the X-coordinate of this chunk + * + * @return X-coordinate + */ + public int getX() { + return x; + } + + /** + * Gets the Z-coordinate of this chunk + * + * @return Z-coordinate + */ + public int getZ() { + return z; + } + + /** + * Get block type for block at corresponding coordinate in the chunk + * + * @param x 0-15 + * @param y 0-127 + * @param z 0-15 + * @return 0-255 + */ + public int getBlockTypeId(int x, int y, int z) { + return buf[x << 11 | z << 7 | y] & 255; + } + + /** + * Get block data for block at corresponding coordinate in the chunk + * + * @param x 0-15 + * @param y 0-127 + * @param z 0-15 + * @return 0-15 + */ + public int getBlockData(int x, int y, int z) { + int off = ((x << 10) | (z << 6) | (y >> 1)) + BLOCKDATA_OFF; + + return ((y & 1) == 0) ? (buf[off] & 0xF) : ((buf[off] >> 4) & 0xF); + } + + /** + * Get sky light level for block at corresponding coordinate in the chunk + * + * @param x 0-15 + * @param y 0-127 + * @param z 0-15 + * @return 0-15 + */ + public int getBlockSkyLight(int x, int y, int z) { + int off = ((x << 10) | (z << 6) | (y >> 1)) + SKYLIGHT_OFF; + + return ((y & 1) == 0) ? (buf[off] & 0xF) : ((buf[off] >> 4) & 0xF); + } + + /** + * Get light level emitted by block at corresponding coordinate in the chunk + * + * @param x 0-15 + * @param y 0-127 + * @param z 0-15 + * @return 0-15 + */ + public int getBlockEmittedLight(int x, int y, int z) { + int off = ((x << 10) | (z << 6) | (y >> 1)) + BLOCKLIGHT_OFF; + + return ((y & 1) == 0) ? (buf[off] & 0xF) : ((buf[off] >> 4) & 0xF); + } + + public int getHighestBlockYAt(int x, int z) { + int off = x << 11 | z << 7 | 126; + int i; + for(i = 127; (i >= 2); i--, off--) { + if(buf[off] != 0) { + break; + } + } + return i; + } +} diff --git a/src/main/java/org/dynmap/MapChunkCache.java b/src/main/java/org/dynmap/MapChunkCache.java new file mode 100644 index 00000000..bd4efb10 --- /dev/null +++ b/src/main/java/org/dynmap/MapChunkCache.java @@ -0,0 +1,213 @@ +package org.dynmap; + +import java.lang.reflect.Method; +import java.util.LinkedList; +import org.bukkit.World; +import org.bukkit.Chunk; +import org.bukkit.entity.Entity; + +/** + * Container for managing chunks, as well as abstracting the different methods we may + * handle chunk data (existing chunk loading, versus upcoming chunk snapshots) + * + */ +public class MapChunkCache { + private World w; + private static Method getchunkdata = null; + private static Method gethandle = null; + private static boolean initialized = false; + + private int x_min, x_max, z_min, z_max; + private int x_dim; + + private CraftChunkSnapshot[] snaparray; /* Index = (x-x_min) + ((z-z_min)*x_dim) */ + private LinkedList loadedChunks = new LinkedList(); + + /** + * Create chunk cache container + * @param w - world + * @param x_min - minimum chunk x coordinate + * @param z_min - minimum chunk z coordinate + * @param x_max - maximum chunk x coordinate + * @param z_max - maximum chunk z coordinate + */ + @SuppressWarnings({ "unchecked" }) + public MapChunkCache(World w, DynmapChunk[] chunks) { + /* Compute range */ + if(chunks.length == 0) { + this.x_min = 0; + this.x_max = 0; + this.z_min = 0; + this.z_max = 0; + x_dim = 1; + } + else { + x_min = x_max = chunks[0].x; + z_min = z_max = chunks[0].z; + for(int i = 1; i < chunks.length; i++) { + if(chunks[i].x > x_max) + x_max = chunks[i].x; + if(chunks[i].x < x_min) + x_min = chunks[i].x; + if(chunks[i].z > z_max) + z_max = chunks[i].z; + if(chunks[i].z < z_min) + z_min = chunks[i].z; + } + x_dim = x_max - x_min + 1; + } + this.w = w; + + if(!initialized) { + try { + Class c = Class.forName("net.minecraft.server.Chunk"); + getchunkdata = c.getDeclaredMethod("a", new Class[] { byte[].class, int.class, + int.class, int.class, int.class, int.class, int.class, int.class }); + c = Class.forName("org.bukkit.craftbukkit.CraftChunk"); + gethandle = c.getDeclaredMethod("getHandle", new Class[0]); + } catch (ClassNotFoundException cnfx) { + } catch (NoSuchMethodException nsmx) { + } + initialized = true; + if(gethandle != null) + Log.info("Chunk snapshot support enabled"); + else + Log.info("Chunk snapshot support disabled"); + } + if(gethandle != null) { /* We can use caching */ + snaparray = new CraftChunkSnapshot[x_dim * (z_max-z_min+1)]; + } + if(snaparray != null) { + // Load the required chunks. + for (DynmapChunk chunk : chunks) { + boolean wasLoaded = w.isChunkLoaded(chunk.x, chunk.z); + boolean didload = w.loadChunk(chunk.x, chunk.z, false); + /* If it did load, make cache of it */ + if(didload) { + Chunk c = w.getChunkAt(chunk.x, chunk.z); + try { + Object cc = gethandle.invoke(c); + byte[] buf = new byte[32768 + 16384 + 16384 + 16384]; /* Get big enough buffer for whole chunk */ + getchunkdata.invoke(cc, buf, 0, 0, 0, 16, 128, 16, 0); + CraftChunkSnapshot ss = new CraftChunkSnapshot(chunk.x, chunk.z, buf); + snaparray[(chunk.x-x_min) + (chunk.z - z_min)*x_dim] = ss; + } catch (Exception x) { + } + } + if ((!wasLoaded) && didload) { + /* It looks like bukkit "leaks" entities - they don't get removed from the world-level table + * when chunks are unloaded but not saved - removing them seems to do the trick */ + Chunk cc = w.getChunkAt(chunk.x, chunk.z); + if(cc != null) { + for(Entity e: cc.getEntities()) + e.remove(); + } + /* Since we only remember ones we loaded, and we're synchronous, no player has + * moved, so it must be safe (also prevent chunk leak, which appears to happen + * because isChunkInUse defined "in use" as being within 256 blocks of a player, + * while the actual in-use chunk area for a player where the chunks are managed + * by the MC base server is 21x21 (or about a 160 block radius) */ + w.unloadChunk(chunk.x, chunk.z, false, false); + } + } + } + else { /* Else, load and keep them loaded for now */ + // Load the required chunks. + for (DynmapChunk chunk : chunks) { + boolean wasLoaded = w.isChunkLoaded(chunk.x, chunk.z); + boolean didload = w.loadChunk(chunk.x, chunk.z, false); + if ((!wasLoaded) && didload) + loadedChunks.add(chunk); + } + } + } + /** + * Unload chunks + */ + public void unloadChunks() { + if(snaparray != null) { + for(int i = 0; i < snaparray.length; i++) { + snaparray[i] = null; + } + } + else { + while (!loadedChunks.isEmpty()) { + DynmapChunk c = loadedChunks.pollFirst(); + /* It looks like bukkit "leaks" entities - they don't get removed from the world-level table + * when chunks are unloaded but not saved - removing them seems to do the trick */ + Chunk cc = w.getChunkAt(c.x, c.z); + if(cc != null) { + for(Entity e: cc.getEntities()) + e.remove(); + } + /* Since we only remember ones we loaded, and we're synchronous, no player has + * moved, so it must be safe (also prevent chunk leak, which appears to happen + * because isChunkInUse defined "in use" as being within 256 blocks of a player, + * while the actual in-use chunk area for a player where the chunks are managed + * by the MC base server is 21x21 (or about a 160 block radius) */ + w.unloadChunk(c.x, c.z, false, false); + } + } + } + /** + * Get block ID at coordinates + */ + public int getBlockTypeID(int x, int y, int z) { + if(snaparray != null) { + CraftChunkSnapshot ss = snaparray[((x>>4) - x_min) + ((z>>4) - z_min) * x_dim]; + if(ss == null) + return 0; + else + return ss.getBlockTypeId(x & 0xF, y, z & 0xF); + } + else { + return w.getBlockTypeIdAt(x, y, z); + } + } + /** + * Get block data at coordiates + */ + public byte getBlockData(int x, int y, int z) { + if(snaparray != null) { + CraftChunkSnapshot ss = snaparray[((x>>4) - x_min) + ((z>>4) - z_min) * x_dim]; + if(ss == null) + return 0; + else + return (byte)ss.getBlockData(x & 0xF, y, z & 0xF); + } + else { + return w.getBlockAt(x, y, z).getData(); + } + } + /* Get highest block Y + * + */ + public int getHighestBlockYAt(int x, int z) { + if(snaparray != null) { + CraftChunkSnapshot ss = snaparray[((x>>4) - x_min) + ((z>>4) - z_min) * x_dim]; + if(ss == null) { + return 1; + } + else + return ss.getHighestBlockYAt(x & 0xF, z & 0xF); + } + else { + return w.getHighestBlockYAt(x, z); + } + } + /* Get sky light level + */ + public int getBlockSkyLight(int x, int y, int z) { + if(snaparray != null) { + CraftChunkSnapshot ss = snaparray[((x>>4) - x_min) + ((z>>4) - z_min) * x_dim]; + if(ss == null) { + return 15; + } + else + return ss.getBlockSkyLight(x & 0xF, y, z & 0xF); + } + else { + return 15; + } + } +} diff --git a/src/main/java/org/dynmap/MapManager.java b/src/main/java/org/dynmap/MapManager.java index 413dbbc3..0a0496d5 100644 --- a/src/main/java/org/dynmap/MapManager.java +++ b/src/main/java/org/dynmap/MapManager.java @@ -21,9 +21,7 @@ public class MapManager { public Map inactiveworlds = new HashMap(); private BukkitScheduler scheduler; private DynmapPlugin plug_in; - private boolean do_timesliced_render = false; private double timeslice_interval = 0.0; - private boolean do_sync_render = false; /* Do incremental renders on sync thread too */ /* Which timesliced renders are active */ private HashMap active_renders = new HashMap(); @@ -96,22 +94,14 @@ public class MapManager { else { /* Else, single tile render */ tile = tile0; } - DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile); - LinkedList loadedChunks = new LinkedList(); + MapChunkCache cache = new MapChunkCache(world.world, requiredChunks); World w = world.world; - // Load the required chunks. - for (DynmapChunk chunk : requiredChunks) { - boolean wasLoaded = w.isChunkLoaded(chunk.x, chunk.z); - boolean didload = w.loadChunk(chunk.x, chunk.z, false); - if ((!wasLoaded) && didload) - loadedChunks.add(chunk); - } if(tile0 != null) { /* Single tile? */ - render(tile); /* Just render */ + render(cache, tile); /* Just render */ } else { - if (render(tile)) { + if (render(cache, tile)) { found.remove(tile); rendered.add(tile); for (MapTile adjTile : map.getAdjecentTiles(tile)) { @@ -129,22 +119,7 @@ public class MapManager { } } /* And unload what we loaded */ - while (!loadedChunks.isEmpty()) { - DynmapChunk c = loadedChunks.pollFirst(); - /* It looks like bukkit "leaks" entities - they don't get removed from the world-level table - * when chunks are unloaded but not saved - removing them seems to do the trick */ - Chunk cc = w.getChunkAt(c.x, c.z); - if(cc != null) { - for(Entity e: cc.getEntities()) - e.remove(); - } - /* Since we only remember ones we loaded, and we're synchronous, no player has - * moved, so it must be safe (also prevent chunk leak, which appears to happen - * because isChunkInUse defined "in use" as being within 256 blocks of a player, - * while the actual in-use chunk area for a player where the chunks are managed - * by the MC base server is 21x21 (or about a 160 block radius) */ - w.unloadChunk(c.x, c.z, false, false); - } + cache.unloadChunks(); if(tile0 == null) { /* fullrender */ /* Schedule the next tile to be worked */ scheduler.scheduleSyncDelayedTask(plug_in, this, (int)(timeslice_interval*20)); @@ -159,11 +134,8 @@ public class MapManager { this.tileQueue = new AsynchronousQueue(new Handler() { @Override public void handle(MapTile t) { - if(do_sync_render) - scheduler.scheduleSyncDelayedTask(plug_in, - new FullWorldRenderState(t), 1); - else - render(t); + scheduler.scheduleSyncDelayedTask(plug_in, + new FullWorldRenderState(t), 1); } }, (int) (configuration.getDouble("renderinterval", 0.5) * 1000)); @@ -175,9 +147,7 @@ public class MapManager { } }, 10); - do_timesliced_render = configuration.getBoolean("timeslicerender", true); timeslice_interval = configuration.getDouble("timesliceinterval", 0.5); - do_sync_render = configuration.getBoolean("renderonsync", true); for(ConfigurationNode worldConfiguration : configuration.getNodes("worlds")) { String worldName = worldConfiguration.getString("name"); @@ -219,78 +189,17 @@ public class MapManager { Log.severe("Could not render: world '" + l.getWorld().getName() + "' not defined in configuration."); return; } - if(do_timesliced_render) { - String wname = l.getWorld().getName(); - FullWorldRenderState rndr = active_renders.get(wname); - if(rndr != null) { - Log.info("Full world render of world '" + wname + "' already active."); - return; - } - rndr = new FullWorldRenderState(world,l); /* Make new activation record */ - active_renders.put(wname, rndr); /* Add to active table */ - /* Schedule first tile to be worked */ - scheduler.scheduleSyncDelayedTask(plug_in, rndr, (int)(timeslice_interval*20)); - Log.info("Full render starting on world '" + wname + "' (timesliced)..."); - + String wname = l.getWorld().getName(); + FullWorldRenderState rndr = active_renders.get(wname); + if(rndr != null) { + Log.info("Full world render of world '" + wname + "' already active."); return; } - World w = world.world; - - Log.info("Full render starting on world '" + w.getName() + "'..."); - for (MapType map : world.maps) { - int requiredChunkCount = 200; - HashSet found = new HashSet(); - HashSet rendered = new HashSet(); - LinkedList renderQueue = new LinkedList(); - LinkedList loadedChunks = new LinkedList(); - - for (MapTile tile : map.getTiles(l)) { - if (!found.contains(tile)) { - found.add(tile); - renderQueue.add(tile); - } - } - while (!renderQueue.isEmpty()) { - MapTile tile = renderQueue.pollFirst(); - - DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile); - - if (requiredChunks.length > requiredChunkCount) - requiredChunkCount = requiredChunks.length; - // Unload old chunks. - while (loadedChunks.size() >= requiredChunkCount - requiredChunks.length) { - DynmapChunk c = loadedChunks.pollFirst(); - w.unloadChunk(c.x, c.z, false, true); - } - - // Load the required chunks. - for (DynmapChunk chunk : requiredChunks) { - boolean wasLoaded = w.isChunkLoaded(chunk.x, chunk.z); - w.loadChunk(chunk.x, chunk.z, false); - if (!wasLoaded) - loadedChunks.add(chunk); - } - - if (render(tile)) { - found.remove(tile); - rendered.add(tile); - for (MapTile adjTile : map.getAdjecentTiles(tile)) { - if (!found.contains(adjTile) && !rendered.contains(adjTile)) { - found.add(adjTile); - renderQueue.add(adjTile); - } - } - } - found.remove(tile); - } - - // Unload remaining chunks to clean-up. - while (!loadedChunks.isEmpty()) { - DynmapChunk c = loadedChunks.pollFirst(); - w.unloadChunk(c.x, c.z, false, true); - } - } - Log.info("Full render finished."); + rndr = new FullWorldRenderState(world,l); /* Make new activation record */ + active_renders.put(wname, rndr); /* Add to active table */ + /* Schedule first tile to be worked */ + scheduler.scheduleSyncDelayedTask(plug_in, rndr, (int)(timeslice_interval*20)); + Log.info("Full render starting on world '" + wname + "' (timesliced)..."); } public void activateWorld(World w) { @@ -337,8 +246,8 @@ public class MapManager { writeQueue.stop(); } - public boolean render(MapTile tile) { - boolean result = tile.getMap().render(tile, getTileFile(tile)); + public boolean render(MapChunkCache cache, MapTile tile) { + boolean result = tile.getMap().render(cache, tile, getTileFile(tile)); //Do update after async file write return result; @@ -387,6 +296,6 @@ public class MapManager { } public boolean doSyncRender() { - return do_sync_render; + return true; } } diff --git a/src/main/java/org/dynmap/MapType.java b/src/main/java/org/dynmap/MapType.java index 006d1846..5aa5a35e 100644 --- a/src/main/java/org/dynmap/MapType.java +++ b/src/main/java/org/dynmap/MapType.java @@ -13,5 +13,5 @@ public abstract class MapType { public abstract DynmapChunk[] getRequiredChunks(MapTile tile); - public abstract boolean render(MapTile tile, File outputFile); + public abstract boolean render(MapChunkCache cache, MapTile tile, File outputFile); } diff --git a/src/main/java/org/dynmap/flat/FlatMap.java b/src/main/java/org/dynmap/flat/FlatMap.java index 59f30ae2..0cb51f43 100644 --- a/src/main/java/org/dynmap/flat/FlatMap.java +++ b/src/main/java/org/dynmap/flat/FlatMap.java @@ -19,6 +19,8 @@ import org.dynmap.MapManager; import org.dynmap.MapTile; import org.dynmap.MapType; import org.dynmap.debug.Debug; +import org.dynmap.kzedmap.KzedMap; +import org.dynmap.MapChunkCache; public class FlatMap extends MapType { private String prefix; @@ -73,13 +75,13 @@ public class FlatMap extends MapType { } @Override - public boolean render(MapTile tile, File outputFile) { + public boolean render(MapChunkCache cache, MapTile tile, File outputFile) { FlatMapTile t = (FlatMapTile) tile; World w = t.getWorld(); boolean isnether = (w.getEnvironment() == Environment.NETHER) && (maximumHeight == 127); boolean rendered = false; - BufferedImage im = new BufferedImage(t.size, t.size, BufferedImage.TYPE_INT_RGB); + BufferedImage im = KzedMap.allocateBufferedImage(t.size, t.size); WritableRaster raster = im.getRaster(); int[] pixel = new int[4]; @@ -93,16 +95,16 @@ public class FlatMap extends MapType { if(isnether) { /* Scan until we hit air */ my = 127; - while((blockType = w.getBlockTypeIdAt(mx, my, mz)) != 0) { + while((blockType = cache.getBlockTypeID(mx, my, mz)) != 0) { my--; if(my < 0) { /* Solid - use top */ my = 127; - blockType = w.getBlockTypeIdAt(mx, my, mz); + blockType = cache.getBlockTypeID(mx, my, mz); break; } } if(blockType == 0) { /* Hit air - now find non-air */ - while((blockType = w.getBlockTypeIdAt(mx, my, mz)) == 0) { + while((blockType = cache.getBlockTypeID(mx, my, mz)) == 0) { my--; if(my < 0) { my = 0; @@ -112,14 +114,14 @@ public class FlatMap extends MapType { } } else { - my = w.getHighestBlockYAt(mx, mz) - 1; + my = cache.getHighestBlockYAt(mx, mz) - 1; if(my > maximumHeight) my = maximumHeight; - blockType = w.getBlockTypeIdAt(mx, my, mz); + blockType = cache.getBlockTypeID(mx, my, mz); } byte data = 0; Color[] colors = colorScheme.colors[blockType]; if(colorScheme.datacolors[blockType] != null) { - data = w.getBlockAt(mx, my, mz).getData(); + data = cache.getBlockData(mx, my, mz); colors = colorScheme.datacolors[blockType][data]; } if (colors == null) @@ -176,7 +178,7 @@ public class FlatMap extends MapType { } catch (java.lang.NullPointerException e) { Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e); } - img.flush(); + KzedMap.freeBufferedImage(img); MapManager.mapman.pushUpdate(mtile.getWorld(), new Client.Tile(mtile.getFilename())); } diff --git a/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java b/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java index d07f5713..59041e16 100644 --- a/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java @@ -1,6 +1,7 @@ package org.dynmap.kzedmap; import org.bukkit.World; +import org.dynmap.MapChunkCache; import org.dynmap.Color; import org.dynmap.ConfigurationNode; @@ -11,14 +12,15 @@ public class CaveTileRenderer extends DefaultTileRenderer { } @Override - protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { + protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result, + MapChunkCache cache) { boolean air = true; result.setTransparent(); for (;;) { if (y < 0) return; - int id = world.getBlockTypeIdAt(x, y, z); + int id = cache.getBlockTypeID(x, y, z); if(isnether) { /* Make ceiling into air in nether */ if(id != 0) id = 0; diff --git a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java index 963c5d39..c3526ccf 100644 --- a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java @@ -1,7 +1,5 @@ package org.dynmap.kzedmap; -import java.awt.Graphics2D; -import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.io.File; @@ -18,6 +16,7 @@ import org.dynmap.ColorScheme; import org.dynmap.ConfigurationNode; import org.dynmap.MapManager; import org.dynmap.debug.Debug; +import org.dynmap.MapChunkCache; public class DefaultTileRenderer implements MapTileRenderer { protected static final Color translucent = new Color(0, 0, 0, 0); @@ -28,6 +27,7 @@ public class DefaultTileRenderer implements MapTileRenderer { protected HashSet highlightBlocks = new HashSet(); protected Color highlightColor = new Color(255, 0, 0); + protected int shadowscale[]; /* index=skylight level, value = 256 * scaling value */ @Override public String getName() { return name; @@ -41,15 +41,29 @@ public class DefaultTileRenderer implements MapTileRenderer { 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]; + for(int i = 0; i < 16; i++) { + double v = 256.0 * (1.0 - (shadowweight * (15-i) / 15.0)); + shadowscale[i] = (int)v; + if(shadowscale[i] > 256) shadowscale[i] = 256; + if(shadowscale[i] < 0) shadowscale[i] = 0; + } + } + } colorScheme = ColorScheme.getScheme((String)configuration.get("colorscheme")); } - public boolean render(KzedMapTile tile, File outputFile) { + public boolean render(MapChunkCache cache, KzedMapTile tile, File outputFile) { World world = tile.getWorld(); boolean isnether = (world.getEnvironment() == Environment.NETHER); - BufferedImage im = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); - + BufferedImage im = KzedMap.allocateBufferedImage(KzedMap.tileWidth, KzedMap.tileHeight); + BufferedImage zim = KzedMap.allocateBufferedImage(KzedMap.tileWidth/2, KzedMap.tileHeight/2); WritableRaster r = im.getRaster(); + WritableRaster zr = zim.getRaster(); boolean isempty = true; int ix = KzedMap.anchorx + tile.px / 2 + tile.py / 2 - ((127-maximumHeight)/2); @@ -66,30 +80,37 @@ public class DefaultTileRenderer implements MapTileRenderer { Color c1 = new Color(); Color c2 = new Color(); - int[] rgb = new int[3]; + int[] rgb = new int[3*KzedMap.tileWidth]; + int[] zrgb = new int[3*KzedMap.tileWidth/2]; /* draw the map */ for (y = 0; y < KzedMap.tileHeight;) { jx = ix; jz = iz; for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) { - scan(world, jx, iy, jz, 0, isnether, c1); - scan(world, jx, iy, jz, 2, isnether, c2); - if(c1.isTransparent() == false) { - rgb[0] = c1.getRed(); rgb[1] = c1.getGreen(); rgb[2] = c1.getBlue(); - r.setPixel(x, y, rgb); - isempty = false; - } - if(c2.isTransparent() == false) { - rgb[0] = c2.getRed(); rgb[1] = c2.getGreen(); rgb[2] = c2.getBlue(); - r.setPixel(x - 1, y, rgb); - isempty = false; - } + scan(world, jx, iy, jz, 0, isnether, c1, cache); + scan(world, jx, iy, jz, 2, isnether, c2, cache); + rgb[3*x] = c1.getRed(); + rgb[3*x+1] = c1.getGreen(); + rgb[3*x+2] = c1.getBlue(); + rgb[3*x-3] = c2.getRed(); + rgb[3*x-2] = c2.getGreen(); + rgb[3*x-1] = c2.getBlue(); + + isempty = isempty && c1.isTransparent() && c2.isTransparent(); + jx++; jz++; } + r.setPixels(0, y, KzedMap.tileWidth, 1, rgb); + /* Sum up zoomed pixels - bilinar filter */ + for(x = 0; x < KzedMap.tileWidth / 2; x++) { + zrgb[3*x] = rgb[6*x] + rgb[6*x+3]; + zrgb[3*x+1] = rgb[6*x+1] + rgb[6*x+4]; + zrgb[3*x+2] = rgb[6*x+2] + rgb[6*x+5]; + } y++; @@ -97,23 +118,29 @@ public class DefaultTileRenderer implements MapTileRenderer { jz = iz - 1; for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) { - scan(world, jx, iy, jz, 2, isnether, c1); + scan(world, jx, iy, jz, 2, isnether, c1, cache); jx++; jz++; - scan(world, jx, iy, jz, 0, isnether, c2); - if(c1.isTransparent() == false) { - rgb[0] = c1.getRed(); rgb[1] = c1.getGreen(); rgb[2] = c1.getBlue(); - r.setPixel(x, y, rgb); - isempty = false; - } - if(c2.isTransparent() == false) { - rgb[0] = c2.getRed(); rgb[1] = c2.getGreen(); rgb[2] = c2.getBlue(); + scan(world, jx, iy, jz, 0, isnether, c2, cache); - r.setPixel(x - 1, y, rgb); - isempty = false; - } + rgb[3*x] = c1.getRed(); + rgb[3*x+1] = c1.getGreen(); + rgb[3*x+2] = c1.getBlue(); + rgb[3*x-3] = c2.getRed(); + rgb[3*x-2] = c2.getGreen(); + rgb[3*x-1] = c2.getBlue(); + + isempty = isempty && c1.isTransparent() && c2.isTransparent(); } - + r.setPixels(0, y, KzedMap.tileWidth, 1, rgb); + /* Finish summing values for zoomed pixels */ + for(x = 0; x < KzedMap.tileWidth / 2; x++) { + zrgb[3*x] = (zrgb[3*x] + rgb[6*x] + rgb[6*x+3]) >> 2; + zrgb[3*x+1] = (zrgb[3*x+1] + rgb[6*x+1] + rgb[6*x+4]) >> 2; + zrgb[3*x+2] = (zrgb[3*x+2] + rgb[6*x+2] + rgb[6*x+5]) >> 2; + } + zr.setPixels(0, y/2, KzedMap.tileWidth/2, 1, zrgb); + y++; ix++; @@ -124,13 +151,14 @@ public class DefaultTileRenderer implements MapTileRenderer { final File fname = outputFile; final KzedMapTile mtile = tile; final BufferedImage img = im; + final BufferedImage zimg = zim; final KzedZoomedMapTile zmtile = new KzedZoomedMapTile(mtile.getWorld(), (KzedMap) mtile.getMap(), mtile); final File zoomFile = MapManager.mapman.getTileFile(zmtile); MapManager.mapman.enqueueImageWrite(new Runnable() { public void run() { - doFileWrites(fname, mtile, img, zmtile, zoomFile); + doFileWrites(fname, mtile, img, zmtile, zoomFile, zimg); } }); @@ -138,7 +166,8 @@ public class DefaultTileRenderer implements MapTileRenderer { } private void doFileWrites(final File fname, final KzedMapTile mtile, - final BufferedImage img, final KzedZoomedMapTile zmtile, final File zoomFile) { + final BufferedImage img, final KzedZoomedMapTile zmtile, final File zoomFile, + final BufferedImage zimg) { Debug.debug("saving image " + fname.getPath()); try { ImageIO.write(img, "png", fname); @@ -147,6 +176,8 @@ public class DefaultTileRenderer implements MapTileRenderer { } catch (java.lang.NullPointerException e) { Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e); } + img.flush(); + mtile.file = fname; // Since we've already got the new tile, and we're on an async thread, just // make the zoomed tile here @@ -175,21 +206,20 @@ public class DefaultTileRenderer implements MapTileRenderer { } catch (IndexOutOfBoundsException e) { } + boolean zIm_allocated = false; if (zIm == null) { /* create new one */ - zIm = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); + zIm = KzedMap.allocateBufferedImage(KzedMap.tileWidth, KzedMap.tileHeight); + zIm_allocated = true; Debug.debug("New zoom-out tile created " + zmtile.getFilename()); } else { Debug.debug("Loaded zoom-out tile from " + zmtile.getFilename()); } /* blit scaled rendered tile onto zoom-out tile */ - Graphics2D g2 = zIm.createGraphics(); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.drawImage(img, ox, oy, scw, sch, null); - g2.dispose(); /* Supposed to speed up non-heap memory recovery */ - - img.flush(); + WritableRaster zim = zIm.getRaster(); + zim.setRect(ox, oy, zimg.getRaster()); + KzedMap.freeBufferedImage(zimg); /* save zoom-out tile */ @@ -201,7 +231,10 @@ public class DefaultTileRenderer implements MapTileRenderer { } catch (java.lang.NullPointerException e) { Debug.error("Failed to save zoom-out tile (NullPointerException): " + zoomFile.getName(), e); } - zIm.flush(); + if(zIm_allocated) + KzedMap.freeBufferedImage(zIm); + else + zIm.flush(); /* Push updates for both files.*/ MapManager.mapman.pushUpdate(mtile.getWorld(), new Client.Tile(mtile.getFilename())); @@ -209,14 +242,15 @@ public class DefaultTileRenderer implements MapTileRenderer { new Client.Tile(zmtile.getFilename())); } - - protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { + protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result, + MapChunkCache cache) { + int lightlevel = 15; result.setTransparent(); for (;;) { if (y < 0) { return; } - int id = world.getBlockTypeIdAt(x, y, z); + int id = cache.getBlockTypeID(x, y, z); byte data = 0; if(isnether) { /* Make bedrock ceiling into air in nether */ if(id != 0) { @@ -229,9 +263,29 @@ public class DefaultTileRenderer implements MapTileRenderer { else isnether = false; } - if(colorScheme.datacolors[id] != null) { /* If data colored */ - data = world.getBlockAt(x, y, z).getData(); + if(id != 0) { /* No update needed for air */ + if(colorScheme.datacolors[id] != null) { /* If data colored */ + data = cache.getBlockData(x, y, z); + } + if((shadowscale != null) && (y < 127)) { + /* Find light level of previous chunk */ + switch(seq) { + case 0: + lightlevel = cache.getBlockSkyLight(x, y+1, z); + break; + case 1: + lightlevel = cache.getBlockSkyLight(x+1, y, z); + break; + case 2: + lightlevel = cache.getBlockSkyLight(x, y+1, z); + break; + case 3: + lightlevel = cache.getBlockSkyLight(x, y, z-1); + break; + } + } } + switch (seq) { case 0: x--; @@ -266,16 +320,25 @@ public class DefaultTileRenderer implements MapTileRenderer { if (c.getAlpha() == 255) { /* it's opaque - the ray ends here */ result.setColor(c); + if(lightlevel < 15) { /* Not full light? */ + shadowColor(result, lightlevel); + } return; } /* this block is transparent, so recurse */ - scan(world, x, y, z, seq, isnether, result); + scan(world, x, y, z, seq, isnether, result, cache); int cr = c.getRed(); int cg = c.getGreen(); int cb = c.getBlue(); int ca = c.getAlpha(); + if(lightlevel < 15) { + int scale = shadowscale[lightlevel]; + cr = (cr * scale) >> 8; + cg = (cg * scale) >> 8; + cb = (cb * scale) >> 8; + } cr *= ca; cg *= ca; cb *= ca; @@ -287,4 +350,10 @@ public class DefaultTileRenderer implements MapTileRenderer { } } } + private final void shadowColor(Color c, int lightlevel) { + int scale = shadowscale[lightlevel]; + if(scale < 256) + c.setRGBA((c.getRed() * scale) >> 8, (c.getGreen() * scale) >> 8, + (c.getBlue() * scale) >> 8, c.getAlpha()); + } } diff --git a/src/main/java/org/dynmap/kzedmap/HighlightTileRenderer.java b/src/main/java/org/dynmap/kzedmap/HighlightTileRenderer.java index d5314e54..2c379878 100644 --- a/src/main/java/org/dynmap/kzedmap/HighlightTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/HighlightTileRenderer.java @@ -1,6 +1,7 @@ package org.dynmap.kzedmap; import java.util.HashSet; +import org.dynmap.MapChunkCache; import java.util.List; import org.bukkit.World; @@ -19,14 +20,15 @@ public class HighlightTileRenderer extends DefaultTileRenderer { } @Override - protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { + protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result, + MapChunkCache cache) { result.setTransparent(); for (;;) { if (y < 0) { break; } - int id = world.getBlockTypeIdAt(x, y, z); + int id = cache.getBlockTypeID(x, y, z); if(isnether) { /* Make bedrock ceiling into air in nether */ if(id != 0) { /* Remember first color we see, in case we wind up solid */ @@ -40,7 +42,7 @@ public class HighlightTileRenderer extends DefaultTileRenderer { } byte data = 0; if(colorScheme.datacolors[id] != null) { /* If data colored */ - data = world.getBlockAt(x, y, z).getData(); + data = cache.getBlockData(x, y, z); } switch (seq) { diff --git a/src/main/java/org/dynmap/kzedmap/KzedMap.java b/src/main/java/org/dynmap/kzedmap/KzedMap.java index ab395aa7..44b8f024 100644 --- a/src/main/java/org/dynmap/kzedmap/KzedMap.java +++ b/src/main/java/org/dynmap/kzedmap/KzedMap.java @@ -1,7 +1,10 @@ package org.dynmap.kzedmap; +import java.awt.image.BufferedImage; import java.io.File; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.logging.Logger; @@ -12,6 +15,7 @@ import org.dynmap.DynmapChunk; import org.dynmap.Log; import org.dynmap.MapTile; import org.dynmap.MapType; +import org.dynmap.MapChunkCache; public class KzedMap extends MapType { protected static final Logger log = Logger.getLogger("Minecraft"); @@ -32,10 +36,17 @@ public class KzedMap extends MapType { public static final int anchorx = 0; public static final int anchory = 127; public static final int anchorz = 0; - + MapTileRenderer[] renderers; ZoomedTileRenderer zoomrenderer; + /* BufferedImage cache - we use the same things a lot... */ + private static Object lock = new Object(); + private static HashMap> imgcache = + new HashMap>(); /* Indexed by resolution - X<<32+Y */ + private static int[] zerobuf = new int[128]; + private static final int CACHE_LIMIT = 10; + public KzedMap(ConfigurationNode configuration) { Log.info("Loading renderers for map '" + getClass().toString() + "'..."); List renderers = configuration.createInstances("renderers", new Class[0], new Object[0]); @@ -203,12 +214,12 @@ public class KzedMap extends MapType { } @Override - public boolean render(MapTile tile, File outputFile) { + public boolean render(MapChunkCache cache, MapTile tile, File outputFile) { if (tile instanceof KzedZoomedMapTile) { - zoomrenderer.render((KzedZoomedMapTile) tile, outputFile); + zoomrenderer.render(cache, (KzedZoomedMapTile) tile, outputFile); return true; } else if (tile instanceof KzedMapTile) { - return ((KzedMapTile) tile).renderer.render((KzedMapTile) tile, outputFile); + return ((KzedMapTile) tile).renderer.render(cache, (KzedMapTile) tile, outputFile); } return false; } @@ -245,4 +256,48 @@ public class KzedMap extends MapType { else return y - (y % zTileHeight); } + + /** + * Allocate buffered image from pool, if possible + * @param x - x dimension + * @param y - y dimension + */ + public static BufferedImage allocateBufferedImage(int x, int y) { + BufferedImage img = null; + synchronized(lock) { + long k = (x<<16) + y; + LinkedList ll = imgcache.get(k); + if(ll != null) { + img = ll.poll(); + } + } + if(img != null) { /* Got it - reset it for use */ + if(zerobuf.length < x) + zerobuf = new int[x]; + img.setRGB(0, 0, x, y, zerobuf, 0, 0); + } + else { + img = new BufferedImage(x, y, BufferedImage.TYPE_INT_RGB); + } + return img; + } + + /** + * Return buffered image to pool + */ + public static void freeBufferedImage(BufferedImage img) { + img.flush(); + synchronized(lock) { + long k = (img.getWidth()<<16) + img.getHeight(); + LinkedList ll = imgcache.get(k); + if(ll == null) { + ll = new LinkedList(); + imgcache.put(k, ll); + } + if(ll.size() < CACHE_LIMIT) { + ll.add(img); + img = null; + } + } + } } diff --git a/src/main/java/org/dynmap/kzedmap/MapTileRenderer.java b/src/main/java/org/dynmap/kzedmap/MapTileRenderer.java index 7674990f..7ad8dd83 100644 --- a/src/main/java/org/dynmap/kzedmap/MapTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/MapTileRenderer.java @@ -1,9 +1,10 @@ package org.dynmap.kzedmap; import java.io.File; +import org.dynmap.MapChunkCache; public interface MapTileRenderer { String getName(); - boolean render(KzedMapTile tile, File outputFile); + boolean render(MapChunkCache cache, KzedMapTile tile, File outputFile); } diff --git a/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java b/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java index a0de1520..7e1d5d09 100644 --- a/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java @@ -2,12 +2,12 @@ package org.dynmap.kzedmap; import java.io.File; import java.util.Map; - +import org.dynmap.MapChunkCache; public class ZoomedTileRenderer { public ZoomedTileRenderer(Map configuration) { } - public void render(final KzedZoomedMapTile zt, final File outputPath) { + public void render(MapChunkCache cache, final KzedZoomedMapTile zt, final File outputPath) { return; /* Doing this in Default render, since image already loaded */ } } diff --git a/src/main/java/org/dynmap/web/HttpServer.java b/src/main/java/org/dynmap/web/HttpServer.java index 8ba67232..8ada63e0 100644 --- a/src/main/java/org/dynmap/web/HttpServer.java +++ b/src/main/java/org/dynmap/web/HttpServer.java @@ -29,7 +29,7 @@ public class HttpServer extends Thread { } public void startServer() throws IOException { - sock = new ServerSocket(port, 5, bindAddress); + sock = new ServerSocket(port, 50, bindAddress); /* 5 too low - more than a couple users during render will get connect errors on some tile loads */ listeningThread = this; start(); Log.info("Dynmap WebServer started on " + bindAddress + ":" + port);