diff --git a/src/main/java/org/dynmap/MapManager.java b/src/main/java/org/dynmap/MapManager.java index 8e345366..c85c52d5 100644 --- a/src/main/java/org/dynmap/MapManager.java +++ b/src/main/java/org/dynmap/MapManager.java @@ -10,11 +10,17 @@ import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.HashMap; import org.bukkit.Location; import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.util.config.ConfigurationNode; +import org.dynmap.DynmapWorld; +import org.dynmap.MapTile; import org.dynmap.debug.Debug; +import org.bukkit.Chunk; public class MapManager { protected static final Logger log = Logger.getLogger("Minecraft"); @@ -23,6 +29,12 @@ public class MapManager { public Map worlds = new HashMap(); public Map inactiveworlds = new HashMap(); + private BukkitScheduler scheduler; + private DynmapPlugin plug_in; + private boolean do_timesliced_render = false; + private double timeslice_interval = 0.0; + /* Which timesliced renders are active */ + private HashMap active_renders = new HashMap(); /* lock for our data structures */ public static final Object lock = new Object(); @@ -50,17 +62,121 @@ public class MapManager { if (bukkitWorld != null) activateWorld(bukkitWorld); } + do_timesliced_render = configuration.getBoolean("timeslicerender", false); + timeslice_interval = configuration.getDouble("timesliceinterval", 0.5); + + scheduler = plugin.getServer().getScheduler(); + plug_in = plugin; 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()); if (world == null) { 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)..."); + + return; + } World w = world.world; + log.info("Full render starting on world '" + w.getName() + "'..."); for (MapType map : world.maps) { int requiredChunkCount = 200;