diff --git a/configuration.txt b/configuration.txt index 5c1e011e..d92888fa 100644 --- a/configuration.txt +++ b/configuration.txt @@ -6,6 +6,9 @@ display-whitelist: false # How often a tile gets rendered (in seconds). renderinterval: 1 +# Do render on main thread - may generate more server load, but safer and fixes broken tiles +renderonsync: true + render-triggers: # - playermove # - playerjoin diff --git a/src/main/java/org/dynmap/DynmapPlayerChatListener.java b/src/main/java/org/dynmap/DynmapPlayerChatListener.java index c732f855..42ada460 100644 --- a/src/main/java/org/dynmap/DynmapPlayerChatListener.java +++ b/src/main/java/org/dynmap/DynmapPlayerChatListener.java @@ -28,4 +28,4 @@ public class DynmapPlayerChatListener extends PlayerListener { plugin.mapManager.pushUpdate(new Client.PlayerQuitMessage(event.getPlayer().getName())); } -} \ No newline at end of file +} diff --git a/src/main/java/org/dynmap/MapManager.java b/src/main/java/org/dynmap/MapManager.java index 30babd7c..a075a366 100644 --- a/src/main/java/org/dynmap/MapManager.java +++ b/src/main/java/org/dynmap/MapManager.java @@ -33,17 +33,127 @@ public class MapManager { 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(); /* lock for our data structures */ public static final Object lock = new Object(); + private class FullWorldRenderState implements Runnable { + DynmapWorld world; /* Which world are we rendering */ + Location loc; /* Start location */ + int map_index = -1; /* Which map are we on */ + MapType map; + HashSet found = null; + HashSet rendered = null; + LinkedList renderQueue = null; + MapTile tile0 = null; + + /* Full world, all maps render */ + FullWorldRenderState(DynmapWorld dworld, Location l) { + world = dworld; + loc = l; + found = new HashSet(); + rendered = new HashSet(); + renderQueue = new LinkedList(); + } + + /* Single tile render - used for incremental renders */ + FullWorldRenderState(MapTile t) { + world = worlds.get(t.getWorld().getName()); + tile0 = t; + } + + public void run() { + MapTile tile; + + if(tile0 == null) { /* Not single tile render */ + /* If render queue is empty, start next map */ + if(renderQueue.isEmpty()) { + found.clear(); + rendered.clear(); + map_index++; /* Next map */ + if(map_index >= world.maps.size()) { /* Last one done? */ + log.info("Full render finished."); + active_renders.remove(world.world.getName()); + return; + } + map = world.maps.get(map_index); + + /* Now, prime the render queue */ + for (MapTile mt : map.getTiles(loc)) { + if (!found.contains(mt)) { + found.add(mt); + renderQueue.add(mt); + } + } + } + tile = renderQueue.pollFirst(); + } + else { /* Else, single tile render */ + tile = tile0; + } + + DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile); + LinkedList loadedChunks = new LinkedList(); + 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 */ + } + else { + 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); + } + /* 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); + } + if(tile0 == null) { /* fullrender */ + /* Schedule the next tile to be worked */ + scheduler.scheduleSyncDelayedTask(plug_in, this, (int)(timeslice_interval*20)); + } + } + } + public MapManager(DynmapPlugin plugin, ConfigurationNode configuration) { this.tileQueue = new AsynchronousQueue(new Handler() { @Override public void handle(MapTile t) { - render(t); + if(do_sync_render) + scheduler.scheduleSyncDelayedTask(plug_in, + new FullWorldRenderState(t), (int)(timeslice_interval*20)); + else + render(t); } }, (int) (configuration.getDouble("renderinterval", 0.5) * 1000)); @@ -64,6 +174,7 @@ public class MapManager { } do_timesliced_render = configuration.getBoolean("timeslicerender", true); timeslice_interval = configuration.getDouble("timesliceinterval", 0.5); + do_sync_render = configuration.getBoolean("renderonsync", true); scheduler = plugin.getServer().getScheduler(); plug_in = plugin; @@ -71,88 +182,7 @@ public class MapManager { tileQueue.start(); } - private class FullWorldRenderState implements Runnable { - DynmapWorld world; /* Which world are we rendering */ - Location loc; /* Start location */ - int map_index = -1; /* Which map are we on */ - MapType map; - HashSet found = new HashSet(); - HashSet rendered = new HashSet(); - LinkedList renderQueue = new LinkedList(); - - FullWorldRenderState(DynmapWorld dworld, Location l) { - world = dworld; - loc = l; - } - - public void run() { - MapTile tile; - /* If render queue is empty, start next map */ - if(renderQueue.isEmpty()) { - found.clear(); - rendered.clear(); - map_index++; /* Next map */ - if(map_index >= world.maps.size()) { /* Last one done? */ - log.info("Full render finished."); - active_renders.remove(world.world.getName()); - return; - } - map = world.maps.get(map_index); - - /* Now, prime the render queue */ - for (MapTile mt : map.getTiles(loc)) { - if (!found.contains(mt)) { - found.add(mt); - renderQueue.add(mt); - } - } - } - tile = renderQueue.pollFirst(); - - DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile); - LinkedList loadedChunks = new LinkedList(); - 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 (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); - /* 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); - } - /* Schedule the next tile to be worked */ - scheduler.scheduleSyncDelayedTask(plug_in, this, (int)(timeslice_interval*20)); - } - } - + void renderFullWorld(Location l) { DynmapWorld world = worlds.get(l.getWorld().getName());