diff --git a/configuration.txt b/configuration.txt index c8190d3d..d567b2c2 100755 --- a/configuration.txt +++ b/configuration.txt @@ -1,58 +1,73 @@ -# All paths in this configuration file are relative to Dynmap's data-folder: minecraft_server/plugins/dynmap/ - -# How often a tile gets rendered (in seconds). -renderinterval: 1 - -# The path where the tile-files are placed. -tilepath: web/tiles - -# The path where the web-files are located. -webpath: web - -# The network-interface the webserver will bind to (0.0.0.0 for all interfaces, 127.0.0.1 for only local access). -webserver-bindaddress: 0.0.0.0 - -# The TCP-port the webserver will listen on. -webserver-port: 8123 - -# Disables Webserver portion of Dynmap (Advanced users only) -disable-webserver: true - -# Writes JSON to file in the webpath -jsonfile: true - -# How often the json file gets written to(in seconds) -jsonfile-interval: 1000 - -disabledcommands: - - fullrender - -# The maptypes Dynmap will use to render. -maps: - - class: org.dynmap.kzedmap.KzedMap - renderers: - - class: org.dynmap.kzedmap.DefaultTileRenderer - prefix: t - - class: org.dynmap.kzedmap.CaveTileRenderer - prefix: ct - -web: - # Interval the browser should poll for updates. - updaterate: 2000 - - showchatballoons: true - showplayerfacesonmap: true - showplayerfacesinmenu: true - focuschatballoons: false - - # The name of the map shown when opening Dynmap's page (must be in menu). - defaultmap: defaultmap - - # The maps shown in the menu. - shownmaps: - - type: KzedMapType - name: defaultmap - prefix: t - - type: KzedMapType - name: cavemap - prefix: ct +# All paths in this configuration file are relative to Dynmap's data-folder: minecraft_server/plugins/dynmap/ + +# How often a tile gets rendered (in seconds). +renderinterval: 1 + +# The path where the tile-files are placed. +tilespath: web/tiles + +# The path where the web-files are located. +webpath: web + +# The network-interface the webserver will bind to (0.0.0.0 for all interfaces, 127.0.0.1 for only local access). +webserver-bindaddress: 0.0.0.0 + +# The TCP-port the webserver will listen on. +webserver-port: 8123 + +# Disables Webserver portion of Dynmap (Advanced users only) +disable-webserver: true + +# Writes JSON to file in the webpath +jsonfile: true + +# How often the json file gets written to(in seconds) +jsonfile-interval: 1000 + +disabledcommands: + - fullrender + +# The maptypes Dynmap will use to render. +maps: + - class: org.dynmap.kzedmap.KzedMap + renderers: + - class: org.dynmap.kzedmap.DefaultTileRenderer + prefix: t + maximumheight: 127 + - class: org.dynmap.kzedmap.CaveTileRenderer + prefix: ct + maximumheight: 127 + +web: + # Interval the browser should poll for updates. + updaterate: 2000 + + showchatballoons: true + showplayerfacesonmap: true + showplayerfacesinmenu: true + focuschatballoons: false + + # The clock that is shown alongside the map. + clock: timeofday + #clock: digital + + # The name of the map shown when opening Dynmap's page (must be in menu). + defaultmap: defaultmap + + # The maps shown in the menu. + shownmaps: + - type: KzedMapType + name: defaultmap + prefix: t + - type: KzedMapType + name: cavemap + prefix: ct + + # The name of the world shown when opening Dynmap's page. + defaultworld: world + + # The worlds shown in the menu. + shownworlds: + - world + - nether + - world_bad diff --git a/src/main/java/org/dynmap/AsynchronousQueue.java b/src/main/java/org/dynmap/AsynchronousQueue.java new file mode 100644 index 00000000..7eacca1b --- /dev/null +++ b/src/main/java/org/dynmap/AsynchronousQueue.java @@ -0,0 +1,108 @@ +package org.dynmap; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AsynchronousQueue { + protected static final Logger log = Logger.getLogger("Minecraft"); + + private Object lock = new Object(); + private Thread thread; + private LinkedList queue = new LinkedList(); + private Set set = new HashSet(); + private Handler handler; + private int dequeueTime; + + public AsynchronousQueue(Handler handler, int dequeueTime) { + this.handler = handler; + this.dequeueTime = dequeueTime; + } + + public boolean push(T t) { + synchronized (lock) { + if (set.add(t)) { + queue.addLast(t); + return true; + } + return false; + } + } + + private T pop() { + synchronized (lock) { + try { + T t = queue.removeFirst(); + if (!set.remove(t)) { + // This should never happen. + } + return t; + } catch (NoSuchElementException e) { + return null; + } + } + } + + public int size() { + return set.size(); + } + + public void start() { + synchronized (lock) { + thread = new Thread(new Runnable() { + @Override + public void run() { + running(); + } + }); + thread.start(); + try { + thread.setPriority(Thread.MIN_PRIORITY); + } catch (SecurityException e) { + log.info("Failed to set minimum priority for worker thread!"); + } + } + } + + public void stop() { + synchronized (lock) { + if (thread == null) + return; + Thread oldThread = thread; + thread = null; + + log.info("Stopping map renderer..."); + + try { + oldThread.join(); + } catch (InterruptedException e) { + log.info("Waiting for map renderer to stop is interrupted"); + } + } + } + + private void running() { + try { + while (Thread.currentThread() == thread) { + T t = pop(); + if (t != null) { + handler.handle(t); + } + sleep(dequeueTime); + } + + } catch (Exception ex) { + log.log(Level.SEVERE, "Exception on rendering-thread", ex); + } + } + + private void sleep(int time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + } + } +} diff --git a/src/main/java/org/dynmap/Client.java b/src/main/java/org/dynmap/Client.java index e18f582c..d27f3e0b 100644 --- a/src/main/java/org/dynmap/Client.java +++ b/src/main/java/org/dynmap/Client.java @@ -11,10 +11,12 @@ public class Client { public static class Player { public String type = "player"; public String name; + public String world; public double x, y, z; - public Player(String name, double x, double y, double z) { + public Player(String name, String world, double x, double y, double z) { this.name = name; + this.world = world; this.x = x; this.y = y; this.z = z; diff --git a/src/main/java/org/dynmap/DynmapBlockListener.java b/src/main/java/org/dynmap/DynmapBlockListener.java index f1cf4367..41a8b61c 100644 --- a/src/main/java/org/dynmap/DynmapBlockListener.java +++ b/src/main/java/org/dynmap/DynmapBlockListener.java @@ -16,13 +16,13 @@ public class DynmapBlockListener extends BlockListener { @Override public void onBlockPlace(BlockPlaceEvent event) { Block blockPlaced = event.getBlockPlaced(); - mgr.touch(blockPlaced.getX(), blockPlaced.getY(), blockPlaced.getZ()); + mgr.touch(blockPlaced.getLocation()); } public void onBlockDamage(BlockDamageEvent event) { if (event.getDamageLevel() == BlockDamageLevel.BROKEN) { Block blockBroken = event.getBlock(); - mgr.touch(blockBroken.getX(), blockBroken.getY(), blockBroken.getZ()); + mgr.touch(blockBroken.getLocation()); } } } diff --git a/src/main/java/org/dynmap/DynmapPlayerListener.java b/src/main/java/org/dynmap/DynmapPlayerListener.java index 7c98da1b..7f30456a 100644 --- a/src/main/java/org/dynmap/DynmapPlayerListener.java +++ b/src/main/java/org/dynmap/DynmapPlayerListener.java @@ -31,7 +31,7 @@ public class DynmapPlayerListener extends PlayerListener { if (split[1].equals("render")) { Player player = event.getPlayer(); - mgr.touch(player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ()); + mgr.touch(player.getLocation()); event.setCancelled(true); } else if (split[1].equals("hide")) { if (split.length == 2) { @@ -54,6 +54,7 @@ public class DynmapPlayerListener extends PlayerListener { } else if (split[1].equals("fullrender")) { Player player = event.getPlayer(); mgr.renderFullWorld(player.getLocation()); + event.setCancelled(true); } } } @@ -66,6 +67,6 @@ public class DynmapPlayerListener extends PlayerListener { * Relevant event details */ public void onPlayerChat(PlayerChatEvent event) { - mgr.updateQueue.pushUpdate(new Client.ChatMessage(event.getPlayer().getName(), event.getMessage())); + mgr.pushUpdate(new Client.ChatMessage(event.getPlayer().getName(), event.getMessage())); } } \ No newline at end of file diff --git a/src/main/java/org/dynmap/DynmapPlugin.java b/src/main/java/org/dynmap/DynmapPlugin.java index 5c907ebc..bd5f84ef 100644 --- a/src/main/java/org/dynmap/DynmapPlugin.java +++ b/src/main/java/org/dynmap/DynmapPlugin.java @@ -20,11 +20,15 @@ import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginLoader; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.util.config.Configuration; -import org.dynmap.debug.BukkitPlayerDebugger; +import org.dynmap.Event.Listener; +import org.dynmap.debug.Debug; +import org.dynmap.debug.LogDebugger; import org.dynmap.web.HttpServer; import org.dynmap.web.handlers.ClientConfigurationHandler; import org.dynmap.web.handlers.ClientUpdateHandler; import org.dynmap.web.handlers.FilesystemHandler; +import org.dynmap.web.handlers.SendMessageHandler; +import org.dynmap.web.handlers.SendMessageHandler.Message; import org.dynmap.web.Json; public class DynmapPlugin extends JavaPlugin { @@ -36,10 +40,8 @@ public class DynmapPlugin extends JavaPlugin { private PlayerList playerList; private Configuration configuration; + public static File tilesDirectory; private Timer timer; - - private BukkitPlayerDebugger debugger = new BukkitPlayerDebugger(this); - public static File dataRoot; public DynmapPlugin(PluginLoader pluginLoader, Server instance, PluginDescriptionFile desc, File folder, File plugin, ClassLoader cLoader) { @@ -60,15 +62,19 @@ public class DynmapPlugin extends JavaPlugin { } public void onEnable() { + Debug.addDebugger(new LogDebugger()); + configuration = new Configuration(new File(this.getDataFolder(), "configuration.txt")); configuration.load(); - debugger.enable(); + tilesDirectory = getFile(configuration.getString("tilespath", "web/tiles")); + tilesDirectory.mkdirs(); + playerList = new PlayerList(getServer()); playerList.load(); - mapManager = new MapManager(getWorld(), debugger, configuration); - mapManager.startManager(); + mapManager = new MapManager(configuration); + mapManager.startRendering(); if(!configuration.getBoolean("disable-webserver", true)) { InetAddress bindAddress; @@ -85,11 +91,21 @@ public class DynmapPlugin extends JavaPlugin { int port = configuration.getInt("webserver-port", 8123); webServer = new HttpServer(bindAddress, port); - webServer.handlers.put("/", new FilesystemHandler(mapManager.webDirectory)); - webServer.handlers.put("/tiles/", new FilesystemHandler(mapManager.tileDirectory)); - webServer.handlers.put("/up/", new ClientUpdateHandler(mapManager, playerList, getWorld())); + webServer.handlers.put("/", new FilesystemHandler(getFile(configuration.getString("webpath", "web")))); + webServer.handlers.put("/tiles/", new FilesystemHandler(tilesDirectory)); + webServer.handlers.put("/up/", new ClientUpdateHandler(mapManager, playerList, getServer())); webServer.handlers.put("/up/configuration", new ClientConfigurationHandler((Map) configuration.getProperty("web"))); + SendMessageHandler messageHandler = new SendMessageHandler(); + messageHandler.onMessageReceived.addListener(new Listener() { + @Override + public void triggered(Message t) { + log.info("[WEB] " + t.name + ": " + t.message); + getServer().broadcastMessage("[WEB] " + t.name + ": " + t.message); + } + }); + webServer.handlers.put("/up/sendmessage", messageHandler); + try { webServer.startServer(); } catch (IOException e) { @@ -108,25 +124,39 @@ public class DynmapPlugin extends JavaPlugin { } public void onDisable() { - mapManager.stopManager(); + mapManager.stopRendering(); if (webServer != null) { webServer.shutdown(); webServer = null; } - debugger.disable(); + Debug.clearDebuggers(); } public void registerEvents() { BlockListener blockListener = new DynmapBlockListener(mapManager); - getServer().getPluginManager().registerEvent(Event.Type.BLOCK_PLACED, blockListener, Priority.Normal, this); - getServer().getPluginManager().registerEvent(Event.Type.BLOCK_DAMAGED, blockListener, Priority.Normal, this); + getServer().getPluginManager().registerEvent(Event.Type.BLOCK_PLACED, blockListener, Priority.Monitor, this); + getServer().getPluginManager().registerEvent(Event.Type.BLOCK_DAMAGED, blockListener, Priority.Monitor, this); PlayerListener playerListener = new DynmapPlayerListener(mapManager, playerList, configuration); getServer().getPluginManager().registerEvent(Event.Type.PLAYER_COMMAND, playerListener, Priority.Normal, this); getServer().getPluginManager().registerEvent(Event.Type.PLAYER_CHAT, playerListener, Priority.Normal, this); } + private static File combinePaths(File parent, String path) { + return combinePaths(parent, new File(path)); + } + + private static File combinePaths(File parent, File path) { + if (path.isAbsolute()) + return path; + return new File(parent, path.getPath()); + } + + public File getFile(String path) { + return combinePaths(DynmapPlugin.dataRoot, path); + } + private void jsonConfig() { File outputFile; diff --git a/src/main/java/org/dynmap/Event.java b/src/main/java/org/dynmap/Event.java new file mode 100644 index 00000000..9cba4e97 --- /dev/null +++ b/src/main/java/org/dynmap/Event.java @@ -0,0 +1,26 @@ +package org.dynmap; + +import java.util.LinkedList; +import java.util.List; + +public class Event { + private List> listeners = new LinkedList>(); + + public synchronized void addListener(Listener l) { + listeners.add(l); + } + + public synchronized void removeListener(Listener l) { + listeners.remove(l); + } + + public synchronized void trigger(T t) { + for (Listener l : listeners) { + l.triggered(t); + } + } + + public interface Listener { + void triggered(T t); + } +} diff --git a/src/main/java/org/dynmap/Handler.java b/src/main/java/org/dynmap/Handler.java new file mode 100644 index 00000000..2a72acd4 --- /dev/null +++ b/src/main/java/org/dynmap/Handler.java @@ -0,0 +1,5 @@ +package org.dynmap; + +public interface Handler { + void handle(T t); +} diff --git a/src/main/java/org/dynmap/MapLocation.java b/src/main/java/org/dynmap/MapLocation.java deleted file mode 100644 index 854ae5c0..00000000 --- a/src/main/java/org/dynmap/MapLocation.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.dynmap; - -public class MapLocation { - public float x; - public float y; -} diff --git a/src/main/java/org/dynmap/MapManager.java b/src/main/java/org/dynmap/MapManager.java index 7a926377..4c172e1d 100644 --- a/src/main/java/org/dynmap/MapManager.java +++ b/src/main/java/org/dynmap/MapManager.java @@ -3,84 +3,50 @@ package org.dynmap; import java.io.File; import java.lang.reflect.Constructor; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.logging.Level; import java.util.logging.Logger; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.util.config.ConfigurationNode; -import org.dynmap.debug.Debugger; +import org.dynmap.debug.Debug; -public class MapManager extends Thread { +public class MapManager { protected static final Logger log = Logger.getLogger("Minecraft"); - private World world; - private Debugger debugger; - private MapType[] maps; - public StaleQueue staleQueue; - public UpdateQueue updateQueue; + private MapType[] mapTypes; + public AsynchronousQueue tileQueue; + + public Map worldUpdateQueues = new HashMap(); + public ArrayList worlds = new ArrayList(); + public PlayerList playerList; /* lock for our data structures */ public static final Object lock = new Object(); - /* whether the worker thread should be running now */ - private boolean running = false; - - /* path to image tile directory */ - public File tileDirectory; - - /* web files location */ - public File webDirectory; - - /* bind web server to ip-address */ - public String bindaddress = "0.0.0.0"; - - /* port to run web server on */ - public int serverport = 8123; - - /* time to pause between rendering tiles (ms) */ - public int renderWait = 500; - - public boolean loadChunks = true; - - public void debug(String msg) { - debugger.debug(msg); - } - - private static File combinePaths(File parent, String path) { - return combinePaths(parent, new File(path)); - } - - private static File combinePaths(File parent, File path) { - if (path.isAbsolute()) - return path; - return new File(parent, path.getPath()); - } - - public MapManager(World world, Debugger debugger, ConfigurationNode configuration) { - this.world = world; - this.debugger = debugger; - this.staleQueue = new StaleQueue(); - this.updateQueue = new UpdateQueue(); - - tileDirectory = combinePaths(DynmapPlugin.dataRoot, configuration.getString("tilespath", "web/tiles")); - webDirectory = combinePaths(DynmapPlugin.dataRoot, configuration.getString("webpath", "web")); - renderWait = (int) (configuration.getDouble("renderinterval", 0.5) * 1000); - loadChunks = configuration.getBoolean("loadchunks", true); - - if (!tileDirectory.isDirectory()) - tileDirectory.mkdirs(); - - maps = loadMapTypes(configuration); + public MapManager(ConfigurationNode configuration) { + this.tileQueue = new AsynchronousQueue(new Handler() { + @Override + public void handle(MapTile t) { + render(t); + } + }, (int) (configuration.getDouble("renderinterval", 0.5) * 1000)); + + mapTypes = loadMapTypes(configuration); + + tileQueue.start(); } void renderFullWorld(Location l) { - debugger.debug("Full render starting..."); - for (MapType map : maps) { + World world = l.getWorld(); + log.info("Full render starting..."); + for (MapType map : mapTypes) { int requiredChunkCount = 200; HashSet found = new HashSet(); HashSet rendered = new HashSet(); @@ -114,10 +80,9 @@ public class MapManager extends Thread { loadedChunks.add(chunk); } - if (map.render(tile)) { + if (render(tile)) { found.remove(tile); rendered.add(tile); - updateQueue.pushUpdate(new Client.Tile(tile.getName())); for (MapTile adjTile : map.getAdjecentTiles(tile)) { if (!found.contains(adjTile) && !rendered.contains(adjTile)) { found.add(adjTile); @@ -135,10 +100,17 @@ public class MapManager extends Thread { world.unloadChunk(c.x, c.z, false, true); } } - debugger.debug("Full render finished."); + log.info("Full render finished."); } private MapType[] loadMapTypes(ConfigurationNode configuration) { + Event.Listener invalitateListener = new Event.Listener() { + @Override + public void triggered(MapTile t) { + invalidateTile(t); + } + }; + List configuredMaps = (List) configuration.getProperty("maps"); ArrayList mapTypes = new ArrayList(); for (Object configuredMapObj : configuredMaps) { @@ -148,83 +120,24 @@ public class MapManager extends Thread { String typeName = (String) configuredMap.get("class"); log.info("Loading map '" + typeName.toString() + "'..."); Class mapTypeClass = Class.forName(typeName); - Constructor constructor = mapTypeClass.getConstructor(MapManager.class, World.class, Debugger.class, Map.class); - MapType mapType = (MapType) constructor.newInstance(this, world, debugger, configuredMap); + Constructor constructor = mapTypeClass.getConstructor(Map.class); + MapType mapType = (MapType) constructor.newInstance(configuredMap); + mapType.onTileInvalidated.addListener(invalitateListener); mapTypes.add(mapType); } catch (Exception e) { - debugger.error("Error loading map", e); + log.log(Level.SEVERE, "Error loading maptype", e); + e.printStackTrace(); } } MapType[] result = new MapType[mapTypes.size()]; mapTypes.toArray(result); return result; } - - /* initialize and start map manager */ - public void startManager() { - synchronized (lock) { - running = true; - this.start(); - try { - this.setPriority(MIN_PRIORITY); - log.info("Set minimum priority for worker thread"); - } catch (SecurityException e) { - log.info("Failed to set minimum priority for worker thread!"); - } - } - } - - /* stop map manager */ - public void stopManager() { - synchronized (lock) { - if (!running) - return; - - log.info("Stopping map renderer..."); - running = false; - - try { - this.join(); - } catch (InterruptedException e) { - log.info("Waiting for map renderer to stop is interrupted"); - } - } - } - - /* the worker/renderer thread */ - public void run() { - try { - log.info("Map renderer has started."); - - while (running) { - MapTile t = staleQueue.popStaleTile(); - if (t != null) { - debugger.debug("Rendering tile " + t + "..."); - boolean isNonEmptyTile = t.getMap().render(t); - updateQueue.pushUpdate(new Client.Tile(t.getName())); - - try { - Thread.sleep(renderWait); - } catch (InterruptedException e) { - } - } else { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - } - } - } - - log.info("Map renderer has stopped."); - } catch (Exception ex) { - debugger.error("Exception on rendering-thread: " + ex.toString()); - ex.printStackTrace(); - } - } - - public void touch(int x, int y, int z) { - for (int i = 0; i < maps.length; i++) { - MapTile[] tiles = maps[i].getTiles(new Location(world, x, y, z)); + + public void touch(Location l) { + Debug.debug("Touched " + l.toString()); + for (int i = 0; i < mapTypes.length; i++) { + MapTile[] tiles = mapTypes[i].getTiles(l); for (int j = 0; j < tiles.length; j++) { invalidateTile(tiles[j]); } @@ -232,7 +145,61 @@ public class MapManager extends Thread { } public void invalidateTile(MapTile tile) { - debugger.debug("Invalidating tile " + tile.getName()); - staleQueue.pushStaleTile(tile); + Debug.debug("Invalidating tile " + tile.getFilename()); + tileQueue.push(tile); + } + + public void startRendering() { + tileQueue.start(); + } + + public void stopRendering() { + tileQueue.stop(); + } + + public boolean render(MapTile tile) { + boolean result = tile.getMap().render(tile, getTileFile(tile)); + pushUpdate(tile.getWorld(), new Client.Tile(tile.getFilename())); + return result; + } + + + private HashMap worldTileDirectories = new HashMap(); + private File getTileFile(MapTile tile) { + World world = tile.getWorld(); + File worldTileDirectory = worldTileDirectories.get(world); + if (worldTileDirectory == null) { + worldTileDirectory = new File(DynmapPlugin.tilesDirectory, tile.getWorld().getName()); + worldTileDirectories.put(world, worldTileDirectory); + } + worldTileDirectory.mkdirs(); + return new File(worldTileDirectory, tile.getFilename()); + } + + public void pushUpdate(Object update) { + for(int i=0;i onTileInvalidated = new Event(); + public abstract MapTile[] getTiles(Location l); public abstract MapTile[] getAdjecentTiles(MapTile tile); public abstract DynmapChunk[] getRequiredChunks(MapTile tile); - public abstract boolean render(MapTile tile); - - public abstract boolean isRendered(MapTile tile); + public abstract boolean render(MapTile tile, File outputFile); } diff --git a/src/main/java/org/dynmap/PlayerList.java b/src/main/java/org/dynmap/PlayerList.java index 6037fb23..570723c5 100644 --- a/src/main/java/org/dynmap/PlayerList.java +++ b/src/main/java/org/dynmap/PlayerList.java @@ -68,6 +68,21 @@ public class PlayerList { hide(playerName); } + // TODO: Clean this up... one day + public Player[] getVisiblePlayers(String worldName) { + ArrayList visiblePlayers = new ArrayList(); + Player[] onlinePlayers = server.getOnlinePlayers(); + for (int i = 0; i < onlinePlayers.length; i++) { + Player p = onlinePlayers[i]; + if (p.getWorld().getName().equals(worldName) && !hiddenPlayerNames.contains(p.getName())) { + visiblePlayers.add(p); + } + } + Player[] result = new Player[visiblePlayers.size()]; + visiblePlayers.toArray(result); + return result; + } + public Player[] getVisiblePlayers() { ArrayList visiblePlayers = new ArrayList(); Player[] onlinePlayers = server.getOnlinePlayers(); diff --git a/src/main/java/org/dynmap/UpdateQueue.java b/src/main/java/org/dynmap/UpdateQueue.java index bddcb098..24a35eb5 100644 --- a/src/main/java/org/dynmap/UpdateQueue.java +++ b/src/main/java/org/dynmap/UpdateQueue.java @@ -11,7 +11,7 @@ public class UpdateQueue { private static final int maxUpdateAge = 120000; - public void pushUpdate(Object obj) { + public synchronized void pushUpdate(Object obj) { long now = System.currentTimeMillis(); long deadline = now - maxUpdateAge; synchronized (lock) { @@ -27,7 +27,7 @@ public class UpdateQueue { private ArrayList tmpupdates = new ArrayList(); - public Object[] getUpdatedObjects(long since) { + public synchronized Object[] getUpdatedObjects(long since) { long now = System.currentTimeMillis(); long deadline = now - maxUpdateAge; Object[] updates; diff --git a/src/main/java/org/dynmap/debug/BukkitPlayerDebugger.java b/src/main/java/org/dynmap/debug/BukkitPlayerDebugger.java index 56b2d587..30c67553 100644 --- a/src/main/java/org/dynmap/debug/BukkitPlayerDebugger.java +++ b/src/main/java/org/dynmap/debug/BukkitPlayerDebugger.java @@ -1,8 +1,6 @@ package org.dynmap.debug; import java.util.HashSet; -import java.util.logging.Level; -import java.util.logging.Logger; import org.bukkit.ChatColor; import org.bukkit.entity.Player; @@ -11,14 +9,9 @@ import org.bukkit.event.Event.Priority; import org.bukkit.event.player.PlayerChatEvent; import org.bukkit.event.player.PlayerEvent; import org.bukkit.event.player.PlayerListener; -import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.java.JavaPlugin; public class BukkitPlayerDebugger implements Debugger { - protected static final Logger log = Logger.getLogger("Minecraft"); - - private boolean isLogging = false; - private JavaPlugin plugin; private HashSet debugees = new HashSet(); private String debugCommand; @@ -28,10 +21,10 @@ public class BukkitPlayerDebugger implements Debugger { public BukkitPlayerDebugger(JavaPlugin plugin) { this.plugin = plugin; - PluginDescriptionFile pdfFile = plugin.getDescription(); - debugCommand = "/debug_" + pdfFile.getName(); - undebugCommand = "/undebug_" + pdfFile.getName(); - prepend = pdfFile.getName() + ": "; + String name = "dynmap"; + debugCommand = "/debug_" + name; + undebugCommand = "/undebug_" + name; + prepend = name + ": "; } public synchronized void enable() { @@ -63,19 +56,15 @@ public class BukkitPlayerDebugger implements Debugger { public synchronized void debug(String message) { sendToDebuggees(message); - if (isLogging) - log.info(prepend + message); } public synchronized void error(String message) { sendToDebuggees(prepend + ChatColor.RED + message); - log.log(Level.SEVERE, prepend + message); } public synchronized void error(String message, Throwable thrown) { sendToDebuggees(prepend + ChatColor.RED + message); sendToDebuggees(thrown.toString()); - log.log(Level.SEVERE, prepend + message); } protected class CommandListener extends PlayerListener { diff --git a/src/main/java/org/dynmap/debug/Debug.java b/src/main/java/org/dynmap/debug/Debug.java new file mode 100644 index 00000000..5de813ad --- /dev/null +++ b/src/main/java/org/dynmap/debug/Debug.java @@ -0,0 +1,32 @@ +package org.dynmap.debug; + +import java.util.LinkedList; +import java.util.List; + +public class Debug { + private static List debuggers = new LinkedList(); + + public synchronized static void addDebugger(Debugger d) { + debuggers.add(d); + } + + public synchronized static void removeDebugger(Debugger d) { + debuggers.remove(d); + } + + public synchronized static void clearDebuggers() { + debuggers.clear(); + } + + public synchronized static void debug(String message) { + for(Debugger d : debuggers) d.debug(message); + } + + public synchronized static void error(String message) { + for(Debugger d : debuggers) d.error(message); + } + + public synchronized static void error(String message, Throwable thrown) { + for(Debugger d : debuggers) d.error(message, thrown); + } +} diff --git a/src/main/java/org/dynmap/debug/LogDebugger.java b/src/main/java/org/dynmap/debug/LogDebugger.java new file mode 100644 index 00000000..ec159c2e --- /dev/null +++ b/src/main/java/org/dynmap/debug/LogDebugger.java @@ -0,0 +1,25 @@ +package org.dynmap.debug; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LogDebugger implements Debugger { + protected static final Logger log = Logger.getLogger("Minecraft"); + private static String prepend = "dynmap: "; + + @Override + public void debug(String message) { + log.info(prepend + message); + } + + @Override + public void error(String message) { + log.log(Level.SEVERE, prepend + message); + } + + @Override + public void error(String message, Throwable thrown) { + log.log(Level.SEVERE, prepend + message); + } + +} diff --git a/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java b/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java index 565a4c2d..afdb1db7 100644 --- a/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java @@ -4,12 +4,11 @@ import java.awt.Color; import java.util.Map; import org.bukkit.World; -import org.dynmap.debug.Debugger; public class CaveTileRenderer extends DefaultTileRenderer { - public CaveTileRenderer(Debugger debugger, Map configuration) { - super(debugger, configuration); + public CaveTileRenderer(Map configuration) { + super(configuration); } @Override diff --git a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java index 06bdc0d5..147282c6 100644 --- a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java @@ -10,32 +10,38 @@ import java.util.Map; import javax.imageio.ImageIO; import org.bukkit.World; -import org.dynmap.debug.Debugger; +import org.dynmap.debug.Debug; public class DefaultTileRenderer implements MapTileRenderer { protected static Color translucent = new Color(0, 0, 0, 0); private String name; - protected Debugger debugger; + protected int maximumHeight = 127; + @Override public String getName() { return name; } - public DefaultTileRenderer(Debugger debugger, Map configuration) { - this.debugger = debugger; + public DefaultTileRenderer(Map configuration) { name = (String) configuration.get("prefix"); + Object o = configuration.get("maximumheight"); + if (o != null) { + maximumHeight = Integer.parseInt(String.valueOf(o)); + if (maximumHeight > 127) + maximumHeight = 127; + } } - public boolean render(KzedMapTile tile, String path) { - World world = tile.getMap().getWorld(); + public boolean render(KzedMapTile tile, File outputFile) { + World world = tile.getWorld(); BufferedImage im = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); WritableRaster r = im.getRaster(); boolean isempty = true; - int ix = tile.mx; - int iy = tile.my; - int iz = tile.mz; + int ix = KzedMap.anchorx + tile.px / 2 + tile.py / 2; + int iy = maximumHeight; + int iz = KzedMap.anchorz + tile.px / 2 - tile.py / 2; int jx, jz; @@ -92,9 +98,11 @@ public class DefaultTileRenderer implements MapTileRenderer { } /* save the generated tile */ - saveTile(tile, im, path); + saveImage(im, outputFile); im.flush(); - ((KzedMap) tile.getMap()).invalidateTile(new KzedZoomedMapTile((KzedMap) tile.getMap(), tile)); + + tile.file = outputFile; + ((KzedMap) tile.getMap()).invalidateTile(new KzedZoomedMapTile(world, (KzedMap) tile.getMap(), tile)); return !isempty; } @@ -154,23 +162,15 @@ public class DefaultTileRenderer implements MapTileRenderer { } /* save rendered tile, update zoom-out tile */ - public void saveTile(KzedMapTile tile, BufferedImage im, String path) { - String tilePath = getPath(tile, path); - - debugger.debug("saving tile " + tilePath); - + public void saveImage(BufferedImage im, File outputFile) { + Debug.debug("saving image " + outputFile.getPath()); /* save image */ try { - File file = new File(tilePath); - ImageIO.write(im, "png", file); + ImageIO.write(im, "png", outputFile); } catch (IOException e) { - debugger.error("Failed to save tile: " + tilePath, e); + Debug.error("Failed to save image: " + outputFile.getPath(), e); } catch (java.lang.NullPointerException e) { - debugger.error("Failed to save tile (NullPointerException): " + tilePath, e); + Debug.error("Failed to save image (NullPointerException): " + outputFile.getPath(), e); } } - - public static String getPath(KzedMapTile tile, String outputPath) { - return new File(new File(outputPath), tile.getName() + ".png").getPath(); - } } diff --git a/src/main/java/org/dynmap/kzedmap/KzedMap.java b/src/main/java/org/dynmap/kzedmap/KzedMap.java index 155f0790..860a1e07 100644 --- a/src/main/java/org/dynmap/kzedmap/KzedMap.java +++ b/src/main/java/org/dynmap/kzedmap/KzedMap.java @@ -15,10 +15,9 @@ import java.util.logging.Logger; import org.bukkit.Location; import org.bukkit.World; import org.dynmap.DynmapChunk; -import org.dynmap.MapManager; import org.dynmap.MapTile; import org.dynmap.MapType; -import org.dynmap.debug.Debugger; +import org.dynmap.debug.Debug; public class KzedMap extends MapType { protected static final Logger log = Logger.getLogger("Minecraft"); @@ -43,14 +42,13 @@ public class KzedMap extends MapType { MapTileRenderer[] renderers; ZoomedTileRenderer zoomrenderer; - public KzedMap(MapManager manager, World world, Debugger debugger, Map configuration) { - super(manager, world, debugger); + public KzedMap(Map configuration) { if (colors == null) { colors = loadColorSet("colors.txt"); } renderers = loadRenderers(configuration); - zoomrenderer = new ZoomedTileRenderer(debugger, configuration); + zoomrenderer = new ZoomedTileRenderer(configuration); } private MapTileRenderer[] loadRenderers(Map configuration) { @@ -63,11 +61,12 @@ public class KzedMap extends MapType { String typeName = (String) configuredRenderer.get("class"); log.info("Loading renderer '" + typeName.toString() + "'..."); Class mapTypeClass = Class.forName(typeName); - Constructor constructor = mapTypeClass.getConstructor(Debugger.class, Map.class); - MapTileRenderer mapTileRenderer = (MapTileRenderer) constructor.newInstance(getDebugger(), configuredRenderer); + Constructor constructor = mapTypeClass.getConstructor(Map.class); + MapTileRenderer mapTileRenderer = (MapTileRenderer) constructor.newInstance(configuredRenderer); renderers.add(mapTileRenderer); } catch (Exception e) { - getDebugger().error("Error loading renderer", e); + Debug.error("Error loading renderer", e); + e.printStackTrace(); } } MapTileRenderer[] result = new MapTileRenderer[renderers.size()]; @@ -77,6 +76,8 @@ public class KzedMap extends MapType { @Override public MapTile[] getTiles(Location l) { + World world = l.getWorld(); + int x = l.getBlockX(); int y = l.getBlockY(); int z = l.getBlockZ(); @@ -92,7 +93,7 @@ public class KzedMap extends MapType { ArrayList tiles = new ArrayList(); - addTile(tiles, tx, ty); + addTile(tiles, world, tx, ty); boolean ledge = tilex(px - 4) != tx; boolean tedge = tiley(py - 4) != ty; @@ -100,22 +101,22 @@ public class KzedMap extends MapType { boolean bedge = tiley(py + 4) != ty; if (ledge) - addTile(tiles, tx - tileWidth, ty); + addTile(tiles, world, tx - tileWidth, ty); if (redge) - addTile(tiles, tx + tileWidth, ty); + addTile(tiles, world, tx + tileWidth, ty); if (tedge) - addTile(tiles, tx, ty - tileHeight); + addTile(tiles, world, tx, ty - tileHeight); if (bedge) - addTile(tiles, tx, ty + tileHeight); + addTile(tiles, world, tx, ty + tileHeight); if (ledge && tedge) - addTile(tiles, tx - tileWidth, ty - tileHeight); + addTile(tiles, world, tx - tileWidth, ty - tileHeight); if (ledge && bedge) - addTile(tiles, tx - tileWidth, ty + tileHeight); + addTile(tiles, world, tx - tileWidth, ty + tileHeight); if (redge && tedge) - addTile(tiles, tx + tileWidth, ty - tileHeight); + addTile(tiles, world, tx + tileWidth, ty - tileHeight); if (redge && bedge) - addTile(tiles, tx + tileWidth, ty + tileHeight); + addTile(tiles, world, tx + tileWidth, ty + tileHeight); MapTile[] result = new MapTile[tiles.size()]; tiles.toArray(result); @@ -126,35 +127,41 @@ public class KzedMap extends MapType { public MapTile[] getAdjecentTiles(MapTile tile) { if (tile instanceof KzedMapTile) { KzedMapTile t = (KzedMapTile) tile; + World world = tile.getWorld(); MapTileRenderer renderer = t.renderer; return new MapTile[] { - new KzedMapTile(this, renderer, t.px - tileWidth, t.py), - new KzedMapTile(this, renderer, t.px + tileWidth, t.py), - new KzedMapTile(this, renderer, t.px, t.py - tileHeight), - new KzedMapTile(this, renderer, t.px, t.py + tileHeight) }; + new KzedMapTile(world, this, renderer, t.px - tileWidth, t.py), + new KzedMapTile(world, this, renderer, t.px + tileWidth, t.py), + new KzedMapTile(world, this, renderer, t.px, t.py - tileHeight), + new KzedMapTile(world, this, renderer, t.px, t.py + tileHeight) }; } return new MapTile[0]; } - public void addTile(ArrayList tiles, int px, int py) { + public void addTile(ArrayList tiles, World world, int px, int py) { for (int i = 0; i < renderers.length; i++) { - tiles.add(new KzedMapTile(this, renderers[i], px, py)); + tiles.add(new KzedMapTile(world, this, renderers[i], px, py)); } } public void invalidateTile(MapTile tile) { - getMapManager().invalidateTile(tile); + onTileInvalidated.trigger(tile); } @Override public DynmapChunk[] getRequiredChunks(MapTile tile) { if (tile instanceof KzedMapTile) { KzedMapTile t = (KzedMapTile) tile; - int x1 = t.mx - KzedMap.tileHeight / 2; - int x2 = t.mx + KzedMap.tileWidth / 2 + KzedMap.tileHeight / 2; + + int ix = KzedMap.anchorx + t.px / 2 + t.py / 2; + int iy = 127; + int iz = KzedMap.anchorz + t.px / 2 - t.py / 2; + + int x1 = ix - KzedMap.tileHeight / 2; + int x2 = ix + KzedMap.tileWidth / 2 + KzedMap.tileHeight / 2; - int z1 = t.mz - KzedMap.tileHeight / 2; - int z2 = t.mz + KzedMap.tileWidth / 2 + KzedMap.tileHeight / 2; + int z1 = iz - KzedMap.tileHeight / 2; + int z2 = iz + KzedMap.tileWidth / 2 + KzedMap.tileHeight / 2; int x, z; @@ -174,21 +181,12 @@ public class KzedMap extends MapType { } @Override - public boolean render(MapTile tile) { + public boolean render(MapTile tile, File outputFile) { if (tile instanceof KzedZoomedMapTile) { - zoomrenderer.render((KzedZoomedMapTile) tile, getMapManager().tileDirectory.getAbsolutePath()); + zoomrenderer.render((KzedZoomedMapTile) tile, outputFile); return true; } else if (tile instanceof KzedMapTile) { - return ((KzedMapTile) tile).renderer.render((KzedMapTile) tile, getMapManager().tileDirectory.getAbsolutePath()); - } - return false; - } - - @Override - public boolean isRendered(MapTile tile) { - if (tile instanceof KzedMapTile) { - File tileFile = new File(DefaultTileRenderer.getPath((KzedMapTile) tile, getMapManager().tileDirectory.getAbsolutePath())); - return tileFile.exists(); + return ((KzedMapTile) tile).renderer.render((KzedMapTile) tile, outputFile); } return false; } @@ -235,10 +233,10 @@ public class KzedMap extends MapType { /* load colorset */ File cfile = new File(colorsetpath); if (cfile.isFile()) { - getDebugger().debug("Loading colors from '" + colorsetpath + "'..."); + Debug.debug("Loading colors from '" + colorsetpath + "'..."); stream = new FileInputStream(cfile); } else { - getDebugger().debug("Loading colors from jar..."); + Debug.debug("Loading colors from jar..."); stream = KzedMap.class.getResourceAsStream("/colors.txt"); } @@ -270,7 +268,7 @@ public class KzedMap extends MapType { } scanner.close(); } catch (Exception e) { - getDebugger().error("Could not load colors", e); + Debug.error("Could not load colors", e); return null; } return colors; diff --git a/src/main/java/org/dynmap/kzedmap/KzedMapTile.java b/src/main/java/org/dynmap/kzedmap/KzedMapTile.java index 6b6da8c3..cfcd7f64 100644 --- a/src/main/java/org/dynmap/kzedmap/KzedMapTile.java +++ b/src/main/java/org/dynmap/kzedmap/KzedMapTile.java @@ -1,42 +1,33 @@ package org.dynmap.kzedmap; -import java.util.logging.Logger; - +import java.io.File; +import org.bukkit.World; import org.dynmap.MapTile; public class KzedMapTile extends MapTile { - protected static final Logger log = Logger.getLogger("Minecraft"); - public KzedMap map; - public MapTileRenderer renderer; - - /* projection position */ public int px, py; + + // Hack. + public File file = null; - /* minecraft space origin */ - public int mx, my, mz; - - /* create new MapTile */ - public KzedMapTile(KzedMap map, MapTileRenderer renderer, int px, int py) { - super(map); + public KzedMapTile(World world, KzedMap map, MapTileRenderer renderer, int px, int py) { + super(world, map); this.map = map; this.renderer = renderer; this.px = px; this.py = py; - - mx = KzedMap.anchorx + px / 2 + py / 2; - my = KzedMap.anchory; - mz = KzedMap.anchorz + px / 2 - py / 2; } @Override - public String getName() { - return renderer.getName() + "_" + px + "_" + py; + public String getFilename() { + return renderer.getName() + "_" + px + "_" + py + ".png"; } + @Override public int hashCode() { - return getName().hashCode(); + return getFilename().hashCode() ^ getWorld().hashCode(); } @Override @@ -48,11 +39,10 @@ public class KzedMapTile extends MapTile { } public boolean equals(KzedMapTile o) { - return o.getName().equals(getName()); + return o.px == px && o.py == py && o.getWorld().equals(getWorld()); } - /* return a simple string representation... */ public String toString() { - return getName(); + return getWorld().getName() + ":" + getFilename(); } } diff --git a/src/main/java/org/dynmap/kzedmap/KzedZoomedMapTile.java b/src/main/java/org/dynmap/kzedmap/KzedZoomedMapTile.java index 58238962..d0362d81 100644 --- a/src/main/java/org/dynmap/kzedmap/KzedZoomedMapTile.java +++ b/src/main/java/org/dynmap/kzedmap/KzedZoomedMapTile.java @@ -1,19 +1,18 @@ package org.dynmap.kzedmap; -import java.awt.image.BufferedImage; - +import org.bukkit.World; import org.dynmap.MapTile; public class KzedZoomedMapTile extends MapTile { @Override - public String getName() { - return "z" + originalTile.renderer.getName() + "_" + getTileX() + "_" + getTileY(); + public String getFilename() { + return "z" + originalTile.renderer.getName() + "_" + getTileX() + "_" + getTileY() + ".png"; } public KzedMapTile originalTile; - public KzedZoomedMapTile(KzedMap map, KzedMapTile original) { - super(map); + public KzedZoomedMapTile(World world, KzedMap map, KzedMapTile original) { + super(world, map); this.originalTile = original; } @@ -42,7 +41,7 @@ public class KzedZoomedMapTile extends MapTile { @Override public int hashCode() { - return getName().hashCode(); + return getFilename().hashCode() ^ getWorld().hashCode(); } @Override diff --git a/src/main/java/org/dynmap/kzedmap/MapTileRenderer.java b/src/main/java/org/dynmap/kzedmap/MapTileRenderer.java index eb87a5c1..7674990f 100644 --- a/src/main/java/org/dynmap/kzedmap/MapTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/MapTileRenderer.java @@ -1,7 +1,9 @@ package org.dynmap.kzedmap; +import java.io.File; + public interface MapTileRenderer { String getName(); - boolean render(KzedMapTile tile, String path); + boolean render(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 793624a6..094cb949 100644 --- a/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java @@ -6,19 +6,14 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Map; - import javax.imageio.ImageIO; - -import org.dynmap.debug.Debugger; +import org.dynmap.debug.Debug; public class ZoomedTileRenderer { - protected Debugger debugger; - - public ZoomedTileRenderer(Debugger debugger, Map configuration) { - this.debugger = debugger; + public ZoomedTileRenderer(Map configuration) { } - public void render(KzedZoomedMapTile zt, String outputPath) { + public void render(KzedZoomedMapTile zt, File outputPath) { KzedMapTile originalTile = zt.originalTile; int px = originalTile.px; int py = originalTile.py; @@ -27,17 +22,17 @@ public class ZoomedTileRenderer { BufferedImage image = null; try { - image = ImageIO.read(new File(new File(outputPath), originalTile.getName() + ".png")); + image = ImageIO.read(originalTile.file); } catch (IOException e) { } if (image == null) { - debugger.debug("Could not load original tile, won't render zoom-out tile."); + Debug.debug("Could not load original tile, won't render zoom-out tile."); return; } BufferedImage zIm = null; - File zoomFile = new File(new File(outputPath), zt.getName() + ".png"); + File zoomFile = outputPath; try { zIm = ImageIO.read(zoomFile); } catch (IOException e) { @@ -46,9 +41,9 @@ public class ZoomedTileRenderer { if (zIm == null) { /* create new one */ zIm = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); - debugger.debug("New zoom-out tile created " + zt.getName()); + Debug.debug("New zoom-out tile created " + zt.getFilename()); } else { - debugger.debug("Loaded zoom-out tile from " + zt.getName()); + Debug.debug("Loaded zoom-out tile from " + zt.getFilename()); } /* update zoom-out tile */ @@ -77,11 +72,11 @@ public class ZoomedTileRenderer { /* save zoom-out tile */ try { ImageIO.write(zIm, "png", zoomFile); - debugger.debug("Saved zoom-out tile at " + zoomFile.getName()); + Debug.debug("Saved zoom-out tile at " + zoomFile.getName()); } catch (IOException e) { - debugger.error("Failed to save zoom-out tile: " + zoomFile.getName(), e); + Debug.error("Failed to save zoom-out tile: " + zoomFile.getName(), e); } catch (java.lang.NullPointerException e) { - debugger.error("Failed to save zoom-out tile (NullPointerException): " + zoomFile.getName(), e); + Debug.error("Failed to save zoom-out tile (NullPointerException): " + zoomFile.getName(), e); } zIm.flush(); } diff --git a/src/main/java/org/dynmap/web/BoundInputStream.java b/src/main/java/org/dynmap/web/BoundInputStream.java new file mode 100644 index 00000000..66665f16 --- /dev/null +++ b/src/main/java/org/dynmap/web/BoundInputStream.java @@ -0,0 +1,61 @@ +package org.dynmap.web; + +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Logger; + +public class BoundInputStream extends InputStream { + protected static final Logger log = Logger.getLogger("Minecraft"); + private InputStream base; + private long bound; + + public BoundInputStream(InputStream base, long bound) { + this.base = base; + this.bound = bound; + } + + @Override + public int read() throws IOException { + if (bound <= 0) return -1; + int r = base.read(); + if (r >= 0) + bound--; + return r; + } + + @Override + public int available() throws IOException { + return (int)Math.min(base.available(), bound); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (bound <= 0) return -1; + len = (int)Math.min(bound, len); + int r = base.read(b, off, len); + bound -= r; + return r; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public long skip(long n) throws IOException { + long r = base.skip(Math.min(bound, n)); + bound -= r; + return r; + } + + @Override + public void close() throws IOException { + base.close(); + } +} diff --git a/src/main/java/org/dynmap/web/HttpErrorHandler.java b/src/main/java/org/dynmap/web/HttpErrorHandler.java new file mode 100644 index 00000000..f2476390 --- /dev/null +++ b/src/main/java/org/dynmap/web/HttpErrorHandler.java @@ -0,0 +1,20 @@ +package org.dynmap.web; + +import java.io.IOException; + +public class HttpErrorHandler { + public static void handle(HttpResponse response, int statusCode, String statusMessage) throws IOException { + response.statusCode = statusCode; + response.statusMessage = statusMessage; + response.fields.put("Content-Length", "0"); + response.getBody(); + } + + public static void handleNotFound(HttpResponse response) throws IOException { + handle(response, 404, "Not found"); + } + + public static void handleMethodNotAllowed(HttpResponse response) throws IOException { + handle(response, 405, "Method not allowed"); + } +} diff --git a/src/main/java/org/dynmap/web/HttpField.java b/src/main/java/org/dynmap/web/HttpField.java new file mode 100644 index 00000000..06535189 --- /dev/null +++ b/src/main/java/org/dynmap/web/HttpField.java @@ -0,0 +1,6 @@ +package org.dynmap.web; + +public class HttpField { + public static final String contentLength = "Content-Length"; + public static final String contentType = "Content-Type"; +} diff --git a/src/main/java/org/dynmap/web/HttpMethods.java b/src/main/java/org/dynmap/web/HttpMethods.java new file mode 100644 index 00000000..8c116519 --- /dev/null +++ b/src/main/java/org/dynmap/web/HttpMethods.java @@ -0,0 +1,8 @@ +package org.dynmap.web; + +public class HttpMethods { + public static final String Get = "GET"; + public static final String Post = "POST"; + public static final String Put = "PUT"; + public static final String Delete = "DELETE"; +} diff --git a/src/main/java/org/dynmap/web/HttpResponse.java b/src/main/java/org/dynmap/web/HttpResponse.java index 2e0622f8..0f481002 100644 --- a/src/main/java/org/dynmap/web/HttpResponse.java +++ b/src/main/java/org/dynmap/web/HttpResponse.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.Map; public class HttpResponse { + private HttpServerConnection connection; public String version = "1.0"; public int statusCode = 200; public String statusMessage = "OK"; @@ -14,7 +15,7 @@ public class HttpResponse { private OutputStream body; public OutputStream getBody() throws IOException { if (body != null) { - HttpServerConnection.writeResponseHeader(body, this); + connection.writeResponseHeader(this); OutputStream b = body; body = null; return b; @@ -22,7 +23,8 @@ public class HttpResponse { return null; } - public HttpResponse(OutputStream body) { + public HttpResponse(HttpServerConnection connection, OutputStream body) { + this.connection = connection; this.body = body; } } diff --git a/src/main/java/org/dynmap/web/HttpServerConnection.java b/src/main/java/org/dynmap/web/HttpServerConnection.java index 5fefc144..847227ac 100644 --- a/src/main/java/org/dynmap/web/HttpServerConnection.java +++ b/src/main/java/org/dynmap/web/HttpServerConnection.java @@ -1,11 +1,10 @@ package org.dynmap.web; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.PrintStream; +import java.io.StringWriter; import java.net.Socket; import java.util.Map.Entry; import java.util.logging.Level; @@ -13,26 +12,58 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.dynmap.debug.Debug; + public class HttpServerConnection extends Thread { protected static final Logger log = Logger.getLogger("Minecraft"); + private static Pattern requestHeaderLine = Pattern.compile("^(\\S+)\\s+(\\S+)\\s+HTTP/(.+)$"); + private static Pattern requestHeaderField = Pattern.compile("^([^:]+):\\s*(.+)$"); + private Socket socket; private HttpServer server; + + private PrintStream printOut; + private StringWriter sw = new StringWriter(); + private Matcher requestHeaderLineMatcher; + private Matcher requestHeaderFieldMatcher; public HttpServerConnection(Socket socket, HttpServer server) { this.socket = socket; this.server = server; } - private static Pattern requestHeaderLine = Pattern.compile("^(\\S+)\\s+(\\S+)\\s+HTTP/(.+)$"); - private static Pattern requestHeaderField = Pattern.compile("^([^:]+):\\s*(.+)$"); + private final static void readLine(InputStream in, StringWriter sw) throws IOException { + int readc; + while((readc = in.read()) > 0) { + char c = (char)readc; + if (c == '\n') + break; + else if (c != '\r') + sw.append(c); + } + } + + private final String readLine(InputStream in) throws IOException { + readLine(in, sw); + String r = sw.toString(); + sw.getBuffer().setLength(0); + return r; + } - private static boolean readRequestHeader(InputStream in, HttpRequest request) throws IOException { - BufferedReader r = new BufferedReader(new InputStreamReader(in)); - String statusLine = r.readLine(); + private final boolean readRequestHeader(InputStream in, HttpRequest request) throws IOException { + String statusLine = readLine(in); + if (statusLine == null) return false; - Matcher m = requestHeaderLine.matcher(statusLine); + + if (requestHeaderLineMatcher == null) { + requestHeaderLineMatcher = requestHeaderLine.matcher(statusLine); + } else { + requestHeaderLineMatcher.reset(statusLine); + } + + Matcher m = requestHeaderLineMatcher; if (!m.matches()) return false; request.method = m.group(1); @@ -40,10 +71,14 @@ public class HttpServerConnection extends Thread { request.version = m.group(3); String line; - while ((line = r.readLine()) != null) { - if (line.equals("")) - break; - m = requestHeaderField.matcher(line); + while (!(line = readLine(in)).equals("")) { + if (requestHeaderFieldMatcher == null) { + requestHeaderFieldMatcher = requestHeaderField.matcher(line); + } else { + requestHeaderFieldMatcher.reset(line); + } + + m = requestHeaderFieldMatcher; // Warning: unknown lines are ignored. if (m.matches()) { String fieldName = m.group(1); @@ -55,79 +90,119 @@ public class HttpServerConnection extends Thread { return true; } - public static void writeResponseHeader(OutputStream out, HttpResponse response) throws IOException { - BufferedOutputStream o = new BufferedOutputStream(out); - StringBuilder sb = new StringBuilder(); - sb.append("HTTP/"); - sb.append(response.version); - sb.append(" "); - sb.append(response.statusCode); - sb.append(" "); - sb.append(response.statusMessage); - sb.append("\r\n"); + public static final void writeResponseHeader(PrintStream out, HttpResponse response) throws IOException { + out.append("HTTP/"); + out.append(response.version); + out.append(" "); + out.append(String.valueOf(response.statusCode)); + out.append(" "); + out.append(response.statusMessage); + out.append("\r\n"); for (Entry field : response.fields.entrySet()) { - sb.append(field.getKey()); - sb.append(": "); - sb.append(field.getValue()); - sb.append("\r\n"); + out.append(field.getKey()); + out.append(": "); + out.append(field.getValue()); + out.append("\r\n"); } - sb.append("\r\n"); - o.write(sb.toString().getBytes()); - o.flush(); + out.append("\r\n"); + out.flush(); + } + + public final void writeResponseHeader(HttpResponse response) throws IOException { + writeResponseHeader(printOut, response); } public void run() { try { socket.setSoTimeout(5000); - - HttpRequest request = new HttpRequest(); - if (!readRequestHeader(socket.getInputStream(), request)) { - socket.close(); - return; - } - - // TODO: Optimize HttpHandler-finding by using a real path-aware - // tree. - HttpHandler handler = null; - String relativePath = null; - for (Entry entry : server.handlers.entrySet()) { - String key = entry.getKey(); - boolean directoryHandler = key.endsWith("/"); - if (directoryHandler && request.path.startsWith(entry.getKey()) || !directoryHandler && request.path.equals(entry.getKey())) { - relativePath = request.path.substring(entry.getKey().length()); - handler = entry.getValue(); - break; - } - } - - if (handler == null) { - socket.close(); - return; - } - - HttpResponse response = new HttpResponse(socket.getOutputStream()); - - try { - handler.handle(relativePath, request, response); - } catch (IOException e) { - throw e; - } catch (Exception e) { - log.log(Level.SEVERE, "HttpHandler '" + handler + "' has thown an exception", e); - if (socket != null) { + InputStream in = socket.getInputStream(); + OutputStream out = socket.getOutputStream(); + + printOut = new PrintStream(out); + while (true) { + HttpRequest request = new HttpRequest(); + + if (!readRequestHeader(in, request)) { socket.close(); + return; + } + + long bound = -1; + BoundInputStream boundBody = null; + { + String contentLengthStr = request.fields.get(HttpField.contentLength); + if (contentLengthStr != null) { + try { + bound = Long.parseLong(contentLengthStr); + } catch (NumberFormatException e) { + } + if (bound >= 0) { + request.body = boundBody = new BoundInputStream(in, bound); + } else { + request.body = in; + } + } } - return; - } - if (response.fields.get("Content-Length") == null) { - response.fields.put("Content-Length", "0"); - /* OutputStream out = */response.getBody(); - } + // TODO: Optimize HttpHandler-finding by using a real path-aware tree. + HttpHandler handler = null; + String relativePath = null; + for (Entry entry : server.handlers.entrySet()) { + String key = entry.getKey(); + boolean directoryHandler = key.endsWith("/"); + if (directoryHandler && request.path.startsWith(entry.getKey()) || !directoryHandler && request.path.equals(entry.getKey())) { + relativePath = request.path.substring(entry.getKey().length()); + handler = entry.getValue(); + break; + } + } - String connection = response.fields.get("Connection"); - if (connection != null && connection.equals("close")) { - socket.close(); - return; + if (handler == null) { + socket.close(); + return; + } + + if (bound > 0) { + boundBody.skip(bound); + } + + HttpResponse response = new HttpResponse(this, out); + + try { + handler.handle(relativePath, request, response); + } catch (IOException e) { + throw e; + } catch (Exception e) { + log.log(Level.SEVERE, "HttpHandler '" + handler + "' has thown an exception", e); + if (socket != null) { + out.flush(); + socket.close(); + } + return; + } + + String connection = response.fields.get("Connection"); + String contentLength = response.fields.get("Content-Length"); + if (contentLength == null && connection == null) { + response.fields.put("Content-Length", "0"); + OutputStream responseBody = response.getBody(); + + // The HttpHandler has already send the headers and written to the body without setting the Content-Length. + if (responseBody == null) { + Debug.debug("Response was given without Content-Length by '" + handler + "' for path '" + request.path + "'."); + out.flush(); + socket.close(); + return; + } + } + + if (connection != null && connection.equals("close")) { + out.flush(); + socket.close(); + return; + } + + out.flush(); } } catch (IOException e) { if (socket != null) { @@ -136,6 +211,7 @@ public class HttpServerConnection extends Thread { } catch (IOException ex) { } } + return; } catch (Exception e) { if (socket != null) { try { @@ -145,6 +221,7 @@ public class HttpServerConnection extends Thread { } log.log(Level.SEVERE, "Exception while handling request: ", e); e.printStackTrace(); + return; } } } diff --git a/src/main/java/org/dynmap/web/handlers/ClientUpdateHandler.java b/src/main/java/org/dynmap/web/handlers/ClientUpdateHandler.java index c814db9c..58d33786 100644 --- a/src/main/java/org/dynmap/web/handlers/ClientUpdateHandler.java +++ b/src/main/java/org/dynmap/web/handlers/ClientUpdateHandler.java @@ -2,7 +2,11 @@ package org.dynmap.web.handlers; import java.io.BufferedOutputStream; import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.bukkit.Location; +import org.bukkit.Server; import org.bukkit.World; import org.bukkit.entity.Player; import org.dynmap.Client; @@ -16,22 +20,36 @@ import org.dynmap.web.Json; public class ClientUpdateHandler implements HttpHandler { private MapManager mapManager; private PlayerList playerList; - private World world; + private Server server; - public ClientUpdateHandler(MapManager mapManager, PlayerList playerList, World world) { + public ClientUpdateHandler(MapManager mapManager, PlayerList playerList, Server server) { this.mapManager = mapManager; this.playerList = playerList; - this.world = world; + this.server = server; } + Pattern updatePathPattern = Pattern.compile("world/([a-zA-Z0-9_]+)/([0-9]*)"); @Override public void handle(String path, HttpRequest request, HttpResponse response) throws Exception { + + Matcher match = updatePathPattern.matcher(path); + + if (!match.matches()) + return; + + String worldName = match.group(1); + String timeKey = match.group(2); + + World world = server.getWorld(worldName); + if (world == null) + return; + long current = System.currentTimeMillis(); - long cutoff = 0; + long since = 0; if (path.length() > 0) { try { - cutoff = Long.parseLong(path); + since = Long.parseLong(timeKey); } catch (NumberFormatException e) { } } @@ -45,10 +63,12 @@ public class ClientUpdateHandler implements HttpHandler { update.players = new Client.Player[players.length]; for(int i=0;i onMessageReceived = new Event(); + @Override + public void handle(String path, HttpRequest request, HttpResponse response) throws Exception { + if (!request.method.equals(HttpMethods.Post)) { + HttpErrorHandler.handleMethodNotAllowed(response); + return; + } + + InputStreamReader reader = new InputStreamReader(request.body); + + JSONObject o = (JSONObject)parser.parse(reader); + Message message = new Message(); + message.name = String.valueOf(o.get("name")); + message.message = String.valueOf(o.get("message")); + + onMessageReceived.trigger(message); + + response.fields.put(HttpField.contentLength, "0"); + response.getBody(); + } + public class Message { + public String name; + public String message; + } +} diff --git a/web/clock.digital.js b/web/clock.digital.js new file mode 100644 index 00000000..c0bf1c83 --- /dev/null +++ b/web/clock.digital.js @@ -0,0 +1,42 @@ +function MinecraftDigitalClock(element) { + this.create(element); +} +MinecraftDigitalClock.prototype = { + element: null, + timeout: null, + time: null, + create: function(element) { + this.element = element; + $(element).addClass('clock'); + }, + setTime: function(time) { + if (this.timeout != null) { + window.clearTimeout(this.timeout); + this.timeout = null; + } + this.time = getMinecraftTime(time); + this.element + .addClass(this.time.day ? 'day' : 'night') + .removeClass(this.time.night ? 'day' : 'night') + .text(this.formatTime(this.time)); + + if (this.timeout == null) { + var me = this; + this.timeout = window.setTimeout(function() { + me.timeout = null; + me.setTime(me.time.servertime+(1000/60)); + }, 700); + } + }, + formatTime: function(time) { + var formatDigits = function(n, digits) { + var s = n.toString(); + while (s.length < digits) { + s = '0' + s; + } + return s; + } + return formatDigits(time.hours, 2) + ':' + formatDigits(time.minutes, 2); + } +}; +clocks.digital = function(element) { return new MinecraftDigitalClock(element); }; \ No newline at end of file diff --git a/web/clock.timeofday.js b/web/clock.timeofday.js new file mode 100644 index 00000000..93f90038 --- /dev/null +++ b/web/clock.timeofday.js @@ -0,0 +1,59 @@ +function MinecraftTimeOfDay(element,elementsun,elementmoon) { + this.create(element, elementsun, elementmoon); +} +MinecraftTimeOfDay.prototype = { + element: null, + elementsun: null, + elementmoon: null, + create: function(element,elementsun,elementmoon) { + if (!element) element = $('
'); + this.element = element; + + if (!elementsun) elementsun = $('
'); + this.elementsun = elementsun; + this.elementsun.appendTo(this.element); + if (!elementmoon) elementmoon = $('
'); + this.elementmoon = elementmoon; + this.elementmoon.appendTo(this.elementsun); + this.element.height(60); + this.element.addClass('timeofday'); + this.elementsun.height(60); + this.elementsun.addClass('timeofday'); + this.elementsun.addClass('sun'); + this.elementmoon.height(60); + this.elementmoon.addClass('timeofday'); + this.elementmoon.addClass('moon'); + this.elementmoon.html(" ‏ "); + this.elementsun.css("background-position", (-120) + "px " + (-120) + "px"); + this.elementmoon.css("background-position", (-120) + "px " + (-120) + "px"); + + return element; + }, + setTime: function(time) { + var sunangle; + + if(time > 23100 || time < 12900) + { + //day mode + var movedtime = time + 900; + movedtime = (movedtime >= 24000) ? movedtime - 24000 : movedtime; + //Now we have 0 -> 13800 for the day period + //Devide by 13800*2=27600 instead of 24000 to compress day + sunangle = ((movedtime)/27600 * 2 * Math.PI); + } + else + { + //night mode + var movedtime = time - 12900; + //Now we have 0 -> 10200 for the night period + //Devide by 10200*2=20400 instead of 24000 to expand night + sunangle = Math.PI + ((movedtime)/20400 * 2 * Math.PI); + } + + var moonangle = sunangle + Math.PI; + + this.elementsun.css("background-position", (-50 * Math.cos(sunangle)) + "px " + (-50 * Math.sin(sunangle)) + "px"); + this.elementmoon.css("background-position", (-50 * Math.cos(moonangle)) + "px " + (-50 * Math.sin(moonangle)) + "px"); + } +}; +clocks.timeofday = function(element) { return new MinecraftTimeOfDay(element); }; \ No newline at end of file diff --git a/web/compass.png b/web/compass.png new file mode 100644 index 00000000..6fdd21a0 Binary files /dev/null and b/web/compass.png differ diff --git a/web/index.html b/web/index.html index 78fb8cac..fead257b 100644 --- a/web/index.html +++ b/web/index.html @@ -17,7 +17,9 @@ - + + +