From 48fea325001e7159e9c60f575eb3c8b8a93935e8 Mon Sep 17 00:00:00 2001 From: Mike Primm Date: Tue, 28 Jun 2011 22:13:42 -0500 Subject: [PATCH] Tighten up lock release logic on I/O and other exceptions Make version in plugin.yml string so that 0.20 isn't 0.2 --- src/main/java/org/dynmap/DynmapWorld.java | 46 ++++---- src/main/java/org/dynmap/MapManager.java | 8 +- src/main/java/org/dynmap/flat/FlatMap.java | 90 ++++++++------- .../dynmap/kzedmap/DefaultTileRenderer.java | 104 ++++++++++-------- .../org/dynmap/utils/FileLockManager.java | 8 +- .../org/dynmap/web/HttpServerConnection.java | 36 ++---- .../org/dynmap/web/handlers/FileHandler.java | 9 +- .../web/handlers/FilesystemHandler.java | 16 +-- src/main/resources/plugin.yml | 2 +- 9 files changed, 163 insertions(+), 156 deletions(-) diff --git a/src/main/java/org/dynmap/DynmapWorld.java b/src/main/java/org/dynmap/DynmapWorld.java index 7be330d3..c4e865d5 100644 --- a/src/main/java/org/dynmap/DynmapWorld.java +++ b/src/main/java/org/dynmap/DynmapWorld.java @@ -301,8 +301,9 @@ public class DynmapWorld { im = ImageIO.read(f); } catch (IOException e) { } catch (IndexOutOfBoundsException e) { + } finally { + FileLockManager.releaseReadLock(f); } - FileLockManager.releaseReadLock(f); if(im != null) { im.getRGB(0, 0, width, height, argb, 0, width); /* Read data */ im.flush(); @@ -333,27 +334,30 @@ public class DynmapWorld { } } FileLockManager.getWriteLock(zf); - TileHashManager hashman = MapManager.mapman.hashman; - long crc = hashman.calculateTileHash(kzIm.argb_buf); /* Get hash of tile */ - int tilex = ztx/step/2; - int tiley = ty/step/2; - String key = world.getName()+".z"+pd.zoomprefix+pd.baseprefix; - if((!zf.exists()) || (crc != MapManager.mapman.hashman.getImageHashCode(key, null, tilex, tiley))) { - try { - if(!zf.getParentFile().exists()) - zf.getParentFile().mkdirs(); - FileLockManager.imageIOWrite(zIm, "png", zf); - Debug.debug("Saved zoom-out tile at " + zf.getPath()); - } catch (IOException e) { - Debug.error("Failed to save zoom-out tile: " + zf.getName(), e); - } catch (java.lang.NullPointerException e) { - Debug.error("Failed to save zoom-out tile (NullPointerException): " + zf.getName(), e); + try { + TileHashManager hashman = MapManager.mapman.hashman; + long crc = hashman.calculateTileHash(kzIm.argb_buf); /* Get hash of tile */ + int tilex = ztx/step/2; + int tiley = ty/step/2; + String key = world.getName()+".z"+pd.zoomprefix+pd.baseprefix; + if((!zf.exists()) || (crc != MapManager.mapman.hashman.getImageHashCode(key, null, tilex, tiley))) { + try { + if(!zf.getParentFile().exists()) + zf.getParentFile().mkdirs(); + FileLockManager.imageIOWrite(zIm, "png", zf); + Debug.debug("Saved zoom-out tile at " + zf.getPath()); + } catch (IOException e) { + Debug.error("Failed to save zoom-out tile: " + zf.getName(), e); + } catch (java.lang.NullPointerException e) { + Debug.error("Failed to save zoom-out tile (NullPointerException): " + zf.getName(), e); + } + hashman.updateHashCode(key, null, tilex, tiley, crc); + MapManager.mapman.pushUpdate(this.world, new Client.Tile(zfname)); + enqueueZoomOutUpdate(zf, pd.zoomlevel+1); } - hashman.updateHashCode(key, null, tilex, tiley, crc); - MapManager.mapman.pushUpdate(this.world, new Client.Tile(zfname)); - enqueueZoomOutUpdate(zf, pd.zoomlevel+1); + } finally { + FileLockManager.releaseWriteLock(zf); + KzedMap.freeBufferedImage(kzIm); } - FileLockManager.releaseWriteLock(zf); - KzedMap.freeBufferedImage(kzIm); } } diff --git a/src/main/java/org/dynmap/MapManager.java b/src/main/java/org/dynmap/MapManager.java index 030758ac..5c2c1874 100644 --- a/src/main/java/org/dynmap/MapManager.java +++ b/src/main/java/org/dynmap/MapManager.java @@ -137,7 +137,7 @@ public class MapManager { MapTile tile = null; int rendercnt = 0; CommandSender sender; - long starttime; + long timeaccum; /* Full world, all maps render */ FullWorldRenderState(DynmapWorld dworld, Location l, CommandSender sender) { @@ -173,7 +173,7 @@ public class MapManager { /* If render queue is empty, start next map */ if(renderQueue.isEmpty()) { if(map_index >= 0) { /* Finished a map? */ - double msecpertile = (double)(tstart - starttime) / (double)((rendercnt>0)?rendercnt:1); + double msecpertile = (double)timeaccum / (double)((rendercnt>0)?rendercnt:1); sender.sendMessage("Full render of map '" + world.maps.get(map_index).getClass().getSimpleName() + "' of world '" + world.world.getName() + "' completed - " + rendercnt + " tiles rendered (" + String.format("%.2f", msecpertile) + " msec/tile)."); } @@ -187,7 +187,6 @@ public class MapManager { return; } map = world.maps.get(map_index); - starttime = System.currentTimeMillis(); /* Now, prime the render queue */ for (MapTile mt : map.getTiles(loc)) { @@ -241,8 +240,9 @@ public class MapManager { found.remove(tile); if(!cache.isEmpty()) { rendercnt++; + timeaccum += System.currentTimeMillis() - tstart; if((rendercnt % 100) == 0) { - double msecpertile = (double)(System.currentTimeMillis() - starttime) / (double)rendercnt; + double msecpertile = (double)timeaccum / (double)rendercnt; sender.sendMessage("Full render of map '" + world.maps.get(map_index).getClass().getSimpleName() + "' on world '" + w.getName() + "' in progress - " + rendercnt + " tiles rendered (" + String.format("%.2f", msecpertile) + " msec/tile)."); } diff --git a/src/main/java/org/dynmap/flat/FlatMap.java b/src/main/java/org/dynmap/flat/FlatMap.java index 810f4beb..5792109a 100644 --- a/src/main/java/org/dynmap/flat/FlatMap.java +++ b/src/main/java/org/dynmap/flat/FlatMap.java @@ -271,61 +271,67 @@ public class FlatMap extends MapType { } } /* Test to see if we're unchanged from older tile */ - FileLockManager.getWriteLock(outputFile); TileHashManager hashman = MapManager.mapman.hashman; long crc = hashman.calculateTileHash(argb_buf); boolean tile_update = false; - if((!outputFile.exists()) || (crc != hashman.getImageHashCode(tile.getKey(), null, t.x, t.y))) { - /* Wrap buffer as buffered image */ - Debug.debug("saving image " + outputFile.getPath()); - if(!outputFile.getParentFile().exists()) - outputFile.getParentFile().mkdirs(); - try { - FileLockManager.imageIOWrite(im.buf_img, "png", outputFile); - } catch (IOException e) { - Debug.error("Failed to save image: " + outputFile.getPath(), e); - } catch (java.lang.NullPointerException e) { - Debug.error("Failed to save image (NullPointerException): " + outputFile.getPath(), e); + FileLockManager.getWriteLock(outputFile); + try { + if((!outputFile.exists()) || (crc != hashman.getImageHashCode(tile.getKey(), null, t.x, t.y))) { + /* Wrap buffer as buffered image */ + Debug.debug("saving image " + outputFile.getPath()); + if(!outputFile.getParentFile().exists()) + outputFile.getParentFile().mkdirs(); + try { + FileLockManager.imageIOWrite(im.buf_img, "png", outputFile); + } catch (IOException e) { + Debug.error("Failed to save image: " + outputFile.getPath(), e); + } catch (java.lang.NullPointerException e) { + Debug.error("Failed to save image (NullPointerException): " + outputFile.getPath(), e); + } + MapManager.mapman.pushUpdate(tile.getWorld(), new Client.Tile(tile.getFilename())); + hashman.updateHashCode(tile.getKey(), null, t.x, t.y, crc); + tile.getDynmapWorld().enqueueZoomOutUpdate(outputFile); + tile_update = true; } - MapManager.mapman.pushUpdate(tile.getWorld(), new Client.Tile(tile.getFilename())); - hashman.updateHashCode(tile.getKey(), null, t.x, t.y, crc); - tile.getDynmapWorld().enqueueZoomOutUpdate(outputFile); - tile_update = true; + else { + Debug.debug("skipping image " + outputFile.getPath() + " - hash match"); + } + } finally { + FileLockManager.releaseWriteLock(outputFile); + KzedMap.freeBufferedImage(im); } - else { - Debug.debug("skipping image " + outputFile.getPath() + " - hash match"); - } - KzedMap.freeBufferedImage(im); - FileLockManager.releaseWriteLock(outputFile); MapManager.mapman.updateStatistics(tile, null, true, tile_update, !rendered); /* If day too, handle it */ if(night_and_day) { File dayfile = new File(tile.getDynmapWorld().worldtilepath, tile.getDayFilename()); - FileLockManager.getWriteLock(dayfile); crc = hashman.calculateTileHash(argb_buf_day); - if((!dayfile.exists()) || (crc != hashman.getImageHashCode(tile.getKey(), "day", t.x, t.y))) { - Debug.debug("saving image " + dayfile.getPath()); - if(!dayfile.getParentFile().exists()) - dayfile.getParentFile().mkdirs(); - try { - FileLockManager.imageIOWrite(im_day.buf_img, "png", dayfile); - } catch (IOException e) { - Debug.error("Failed to save image: " + dayfile.getPath(), e); - } catch (java.lang.NullPointerException e) { - Debug.error("Failed to save image (NullPointerException): " + dayfile.getPath(), e); + FileLockManager.getWriteLock(dayfile); + try { + if((!dayfile.exists()) || (crc != hashman.getImageHashCode(tile.getKey(), "day", t.x, t.y))) { + Debug.debug("saving image " + dayfile.getPath()); + if(!dayfile.getParentFile().exists()) + dayfile.getParentFile().mkdirs(); + try { + FileLockManager.imageIOWrite(im_day.buf_img, "png", dayfile); + } catch (IOException e) { + Debug.error("Failed to save image: " + dayfile.getPath(), e); + } catch (java.lang.NullPointerException e) { + Debug.error("Failed to save image (NullPointerException): " + dayfile.getPath(), e); + } + MapManager.mapman.pushUpdate(tile.getWorld(), new Client.Tile(tile.getDayFilename())); + hashman.updateHashCode(tile.getKey(), "day", t.x, t.y, crc); + tile.getDynmapWorld().enqueueZoomOutUpdate(dayfile); + tile_update = true; } - MapManager.mapman.pushUpdate(tile.getWorld(), new Client.Tile(tile.getDayFilename())); - hashman.updateHashCode(tile.getKey(), "day", t.x, t.y, crc); - tile.getDynmapWorld().enqueueZoomOutUpdate(dayfile); - tile_update = true; + else { + Debug.debug("skipping image " + dayfile.getPath() + " - hash match"); + tile_update = false; + } + } finally { + FileLockManager.releaseWriteLock(dayfile); + KzedMap.freeBufferedImage(im_day); } - else { - Debug.debug("skipping image " + dayfile.getPath() + " - hash match"); - tile_update = false; - } - KzedMap.freeBufferedImage(im_day); - FileLockManager.releaseWriteLock(dayfile); MapManager.mapman.updateStatistics(tile, "day", true, tile_update, !rendered); } diff --git a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java index 5796ad48..1bd97ece 100644 --- a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java @@ -256,29 +256,32 @@ public class DefaultTileRenderer implements MapTileRenderer { int oy = (mtile.py == zmtile.getTileY())?0:KzedMap.tileHeight/2; /* Test to see if we're unchanged from older tile */ - FileLockManager.getWriteLock(fname); TileHashManager hashman = MapManager.mapman.hashman; long crc = hashman.calculateTileHash(img.argb_buf); boolean updated_fname = false; int tx = mtile.px/KzedMap.tileWidth; int ty = mtile.py/KzedMap.tileHeight; - if((!fname.exists()) || (crc != hashman.getImageHashCode(mtile.getKey(), null, tx, ty))) { - Debug.debug("saving image " + fname.getPath()); - if(!fname.getParentFile().exists()) - fname.getParentFile().mkdirs(); - try { - FileLockManager.imageIOWrite(img.buf_img, "png", fname); - } catch (IOException e) { - Debug.error("Failed to save image: " + fname.getPath(), e); - } catch (java.lang.NullPointerException e) { - Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e); + FileLockManager.getWriteLock(fname); + try { + if((!fname.exists()) || (crc != hashman.getImageHashCode(mtile.getKey(), null, tx, ty))) { + Debug.debug("saving image " + fname.getPath()); + if(!fname.getParentFile().exists()) + fname.getParentFile().mkdirs(); + try { + FileLockManager.imageIOWrite(img.buf_img, "png", fname); + } catch (IOException e) { + Debug.error("Failed to save image: " + fname.getPath(), e); + } catch (java.lang.NullPointerException e) { + Debug.error("Failed to save image (NullPointerException): " + fname.getPath(), e); + } + MapManager.mapman.pushUpdate(mtile.getWorld(), new Client.Tile(mtile.getFilename())); + hashman.updateHashCode(mtile.getKey(), null, tx, ty, crc); + updated_fname = true; } - MapManager.mapman.pushUpdate(mtile.getWorld(), new Client.Tile(mtile.getFilename())); - hashman.updateHashCode(mtile.getKey(), null, tx, ty, crc); - updated_fname = true; + } finally { + FileLockManager.releaseWriteLock(fname); + KzedMap.freeBufferedImage(img); } - KzedMap.freeBufferedImage(img); - FileLockManager.releaseWriteLock(fname); MapManager.mapman.updateStatistics(mtile, null, true, updated_fname, !rendered); mtile.file = fname; @@ -287,25 +290,28 @@ public class DefaultTileRenderer implements MapTileRenderer { File dfname = new File(mtile.getDynmapWorld().worldtilepath, mtile.getDayFilename()); if(img_day != null) { - FileLockManager.getWriteLock(dfname); crc = hashman.calculateTileHash(img.argb_buf); - if((!dfname.exists()) || (crc != hashman.getImageHashCode(mtile.getKey(), "day", tx, ty))) { - Debug.debug("saving image " + dfname.getPath()); - if(!dfname.getParentFile().exists()) - dfname.getParentFile().mkdirs(); - try { - FileLockManager.imageIOWrite(img_day.buf_img, "png", dfname); - } catch (IOException e) { - Debug.error("Failed to save image: " + dfname.getPath(), e); - } catch (java.lang.NullPointerException e) { - Debug.error("Failed to save image (NullPointerException): " + dfname.getPath(), e); + FileLockManager.getWriteLock(dfname); + try { + if((!dfname.exists()) || (crc != hashman.getImageHashCode(mtile.getKey(), "day", tx, ty))) { + Debug.debug("saving image " + dfname.getPath()); + if(!dfname.getParentFile().exists()) + dfname.getParentFile().mkdirs(); + try { + FileLockManager.imageIOWrite(img_day.buf_img, "png", dfname); + } catch (IOException e) { + Debug.error("Failed to save image: " + dfname.getPath(), e); + } catch (java.lang.NullPointerException e) { + Debug.error("Failed to save image (NullPointerException): " + dfname.getPath(), e); + } + MapManager.mapman.pushUpdate(mtile.getWorld(), new Client.Tile(mtile.getDayFilename())); + hashman.updateHashCode(mtile.getKey(), "day", tx, ty, crc); + updated_dfname = true; } - MapManager.mapman.pushUpdate(mtile.getWorld(), new Client.Tile(mtile.getDayFilename())); - hashman.updateHashCode(mtile.getKey(), "day", tx, ty, crc); - updated_dfname = true; + } finally { + FileLockManager.releaseWriteLock(dfname); + KzedMap.freeBufferedImage(img_day); } - KzedMap.freeBufferedImage(img_day); - FileLockManager.releaseWriteLock(dfname); MapManager.mapman.updateStatistics(mtile, "day", true, updated_dfname, !rendered); } @@ -313,30 +319,36 @@ public class DefaultTileRenderer implements MapTileRenderer { // make the zoomed tile here boolean ztile_updated = false; FileLockManager.getWriteLock(zoomFile); - if(updated_fname || (!zoomFile.exists())) { - saveZoomedTile(zmtile, zoomFile, zimg, ox, oy, null); - MapManager.mapman.pushUpdate(zmtile.getWorld(), + try { + if(updated_fname || (!zoomFile.exists())) { + saveZoomedTile(zmtile, zoomFile, zimg, ox, oy, null); + MapManager.mapman.pushUpdate(zmtile.getWorld(), new Client.Tile(zmtile.getFilename())); - zmtile.getDynmapWorld().enqueueZoomOutUpdate(zoomFile); - ztile_updated = true; + zmtile.getDynmapWorld().enqueueZoomOutUpdate(zoomFile); + ztile_updated = true; + } + } finally { + FileLockManager.releaseWriteLock(zoomFile); + KzedMap.freeBufferedImage(zimg); } - KzedMap.freeBufferedImage(zimg); - FileLockManager.releaseWriteLock(zoomFile); MapManager.mapman.updateStatistics(zmtile, null, true, ztile_updated, !rendered); if(zimg_day != null) { File zoomFile_day = new File(zmtile.getDynmapWorld().worldtilepath, zmtile.getDayFilename()); ztile_updated = false; FileLockManager.getWriteLock(zoomFile_day); - if(updated_dfname || (!zoomFile_day.exists())) { - saveZoomedTile(zmtile, zoomFile_day, zimg_day, ox, oy, "day"); - MapManager.mapman.pushUpdate(zmtile.getWorld(), + try { + if(updated_dfname || (!zoomFile_day.exists())) { + saveZoomedTile(zmtile, zoomFile_day, zimg_day, ox, oy, "day"); + MapManager.mapman.pushUpdate(zmtile.getWorld(), new Client.Tile(zmtile.getDayFilename())); - zmtile.getDynmapWorld().enqueueZoomOutUpdate(zoomFile_day); - ztile_updated = true; + zmtile.getDynmapWorld().enqueueZoomOutUpdate(zoomFile_day); + ztile_updated = true; + } + } finally { + FileLockManager.releaseWriteLock(zoomFile_day); + KzedMap.freeBufferedImage(zimg_day); } - KzedMap.freeBufferedImage(zimg_day); - FileLockManager.releaseWriteLock(zoomFile_day); MapManager.mapman.updateStatistics(zmtile, "day", true, ztile_updated, !rendered); } } diff --git a/src/main/java/org/dynmap/utils/FileLockManager.java b/src/main/java/org/dynmap/utils/FileLockManager.java index b4598e18..8994b32c 100644 --- a/src/main/java/org/dynmap/utils/FileLockManager.java +++ b/src/main/java/org/dynmap/utils/FileLockManager.java @@ -18,7 +18,7 @@ public class FileLockManager { * Get write lock on file - exclusive lock, no other writers or readers * @throws InterruptedException */ - public static void getWriteLock(File f) { + public static boolean getWriteLock(File f) { String fn = f.getPath(); synchronized(lock) { boolean got_lock = false; @@ -29,6 +29,7 @@ public class FileLockManager { lock.wait(); } catch (InterruptedException ix) { Log.severe("getWriteLock(" + fn + ") interrupted"); + return false; } } else { @@ -38,6 +39,7 @@ public class FileLockManager { } } //Log.info("getWriteLock(" + f + ")"); + return true; } /** * Release write lock @@ -60,7 +62,7 @@ public class FileLockManager { /** * Get read lock on file - multiple readers allowed, blocks writers */ - public static void getReadLock(File f) { + public static boolean getReadLock(File f) { String fn = f.getPath(); synchronized(lock) { boolean got_lock = false; @@ -79,11 +81,13 @@ public class FileLockManager { lock.wait(); } catch (InterruptedException ix) { Log.severe("getReadLock(" + fn + ") interrupted"); + return false; } } } } //Log.info("getReadLock(" + f + ")"); + return true; } /** * Release read lock diff --git a/src/main/java/org/dynmap/web/HttpServerConnection.java b/src/main/java/org/dynmap/web/HttpServerConnection.java index e592188c..ac6540ca 100644 --- a/src/main/java/org/dynmap/web/HttpServerConnection.java +++ b/src/main/java/org/dynmap/web/HttpServerConnection.java @@ -131,7 +131,6 @@ public class HttpServerConnection extends Thread { HttpRequest request = new HttpRequest(); request.rmtaddr = rmtaddr; if (!readRequestHeader(in, request)) { - socket.close(); return; } @@ -174,7 +173,6 @@ public class HttpServerConnection extends Thread { } if (handler == null) { - socket.close(); return; } @@ -186,10 +184,7 @@ public class HttpServerConnection extends Thread { throw e; } catch (Exception e) { Log.severe("HttpHandler '" + handler + "' has thown an exception", e); - if (socket != null) { - out.flush(); - socket.close(); - } + out.flush(); return; } @@ -211,37 +206,28 @@ public class HttpServerConnection extends Thread { if (responseBody == null) { Debug.debug("Response was given without Content-Length by '" + handler + "' for path '" + request.path + "'."); out.flush(); - socket.close(); return; } } + out.flush(); + if (!isKeepalive) { - out.flush(); - socket.close(); return; } - - out.flush(); } } catch (IOException e) { - if (socket != null) { - try { - socket.close(); - } catch (IOException ex) { - } - } - return; + } catch (Exception e) { - if (socket != null) { - try { - socket.close(); - } catch (IOException ex) { - } - } Log.severe("Exception while handling request: ", e); e.printStackTrace(); - return; + } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException ex) { + } + } } } } diff --git a/src/main/java/org/dynmap/web/handlers/FileHandler.java b/src/main/java/org/dynmap/web/handlers/FileHandler.java index 950b7df9..ba4e3eb7 100644 --- a/src/main/java/org/dynmap/web/handlers/FileHandler.java +++ b/src/main/java/org/dynmap/web/handlers/FileHandler.java @@ -111,20 +111,13 @@ public abstract class FileHandler implements HttpHandler { while ((readBytes = fileInput.read(readBuffer)) > 0) { out.write(readBuffer, 0, readBytes); } - } catch (IOException e) { - throw e; } finally { freeReadBuffer(readBuffer); - if(fileInput != null) { - closeFileInput(path, fileInput); - fileInput = null; - } } - } catch (Exception e) { + } finally { if (fileInput != null) { try { closeFileInput(path, fileInput); fileInput = null; } catch (IOException ex) { } } - throw e; } } } diff --git a/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java b/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java index a9f1ebfa..510f0e85 100644 --- a/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java +++ b/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java @@ -24,13 +24,12 @@ public class FilesystemHandler extends FileHandler { protected InputStream getFileInput(String path, HttpRequest request, HttpResponse response) { File file = new File(root, path); FileLockManager.getReadLock(file); + FileInputStream result = null; try { if (file.getCanonicalPath().startsWith(root.getAbsolutePath()) && file.isFile()) { - FileInputStream result; try { result = new FileInputStream(file); } catch (FileNotFoundException e) { - FileLockManager.releaseReadLock(file); return null; } response.fields.put(HttpField.ContentLength, Long.toString(file.length())); @@ -38,14 +37,17 @@ public class FilesystemHandler extends FileHandler { } } catch(IOException ex) { Log.severe("Unable to get canoical path of requested file.", ex); + } finally { + if(result == null) FileLockManager.releaseReadLock(file); } - FileLockManager.releaseReadLock(file); return null; } protected void closeFileInput(String path, InputStream in) throws IOException { - super.closeFileInput(path, in); - File file = new File(root, path); - FileLockManager.releaseReadLock(file); + try { + super.closeFileInput(path, in); + } finally { + File file = new File(root, path); + FileLockManager.releaseReadLock(file); + } } - } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 7bc3e7b7..b63efc54 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: dynmap main: org.dynmap.DynmapPlugin -version: 0.20 +version: "0.20" authors: [FrozenCow, mikeprimm, zeeZ] softdepend: [Permissions] commands: