diff --git a/configuration.txt b/configuration.txt index 31b7be9b..20e94a60 100644 --- a/configuration.txt +++ b/configuration.txt @@ -75,7 +75,7 @@ worlds: renderers: - class: org.dynmap.kzedmap.DefaultTileRenderer prefix: nt - maximumheight: 64 + maximumheight: 127 colorscheme: default web: @@ -88,6 +88,12 @@ web: allowchat: true allowwebchat: true webchat-interval: 5 + # Set to true to enable HeroChat support + enableherochat: false + # Control which HeroChat channel messages from web are directed to + herochatwebchannel: Global + # Control which channels are monitored and reported to the web + herochatchannels: [ Global ] showplayerfacesinmenu: true diff --git a/src/main/java/org/dynmap/Color.java b/src/main/java/org/dynmap/Color.java new file mode 100644 index 00000000..4c7fe0ae --- /dev/null +++ b/src/main/java/org/dynmap/Color.java @@ -0,0 +1,46 @@ +package org.dynmap; + +/** + * Simple replacement for java.awt.Color for dynmap - it's not an invariant, so we don't make millions + * of them during rendering + */ +public class Color { + /* RGBA value */ + private int val; + + public static final int TRANSPARENT = 0; + + public Color(int red, int green, int blue, int alpha) { + setRGBA(red, green, blue, alpha); + } + public Color(int red, int green, int blue) { + setRGBA(red, green, blue, 0xFF); + } + public Color() { + setTransparent(); + } + public final int getRed() { + return (val >> 24) & 0xFF; + } + public final int getGreen() { + return (val >> 16) & 0xFF; + } + public final int getBlue() { + return (val >> 8) & 0xFF; + } + public final int getAlpha() { + return (val & 0xFF); + } + public final boolean isTransparent() { + return (val == TRANSPARENT); + } + public final void setTransparent() { + val = TRANSPARENT; + } + public final void setColor(Color c) { + val = c.val; + } + public final void setRGBA(int red, int green, int blue, int alpha) { + val = ((red & 0xFF) << 24) | ((green & 0xFF) << 16) | ((blue & 0xFF) << 8) | (alpha & 0xFF); + } +} diff --git a/src/main/java/org/dynmap/ColorScheme.java b/src/main/java/org/dynmap/ColorScheme.java index 99894b82..bd59b34d 100644 --- a/src/main/java/org/dynmap/ColorScheme.java +++ b/src/main/java/org/dynmap/ColorScheme.java @@ -1,6 +1,6 @@ package org.dynmap; -import java.awt.Color; +import org.dynmap.Color; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; diff --git a/src/main/java/org/dynmap/DynmapPlugin.java b/src/main/java/org/dynmap/DynmapPlugin.java index 26decd3a..c9f731b1 100644 --- a/src/main/java/org/dynmap/DynmapPlugin.java +++ b/src/main/java/org/dynmap/DynmapPlugin.java @@ -58,6 +58,7 @@ public class DynmapPlugin extends JavaPlugin { public Configuration configuration; public HashSet enabledTriggers = new HashSet(); public PermissionProvider permissions; + public HeroChatHandler hchand; public Timer timer; @@ -106,12 +107,18 @@ public class DynmapPlugin extends JavaPlugin { timer.scheduleAtFixedRate(new JsonTimerTask(this, configuration), jsonInterval, jsonInterval); } + hchand = new HeroChatHandler(configuration, this, getServer()); + enabledTriggers.clear(); for (Object trigger : configuration.getList("render-triggers")) { enabledTriggers.add((String) trigger); } registerEvents(); + + /* Print version info */ + PluginDescriptionFile pdfFile = this.getDescription(); + log.info("[dynmap] version " + pdfFile.getVersion() + " is enabled" ); } public void loadWebserver() { @@ -154,9 +161,7 @@ public class DynmapPlugin extends JavaPlugin { } catch (IOException e) { log.severe("Failed to start WebServer on " + bindAddress + ":" + port + "!"); } - /* Print version info */ - PluginDescriptionFile pdfFile = this.getDescription(); - log.info("[dynmap] version " + pdfFile.getVersion() + " is enabled" ); + } public void onDisable() { @@ -402,6 +407,8 @@ public class DynmapPlugin extends JavaPlugin { public void webChat(String name, String message) { mapManager.pushUpdate(new Client.ChatMessage("web", name, message)); log.info("[WEB]" + name + ": " + message); - getServer().broadcastMessage("[WEB]" + name + ": " + message); + /* Let HeroChat take a look - only broadcast to players if it doesn't handle it */ + if(hchand.sendWebMessageToHeroChat(name, message) == false) + getServer().broadcastMessage("[WEB]" + name + ": " + message); } } diff --git a/src/main/java/org/dynmap/HeroChatHandler.java b/src/main/java/org/dynmap/HeroChatHandler.java new file mode 100644 index 00000000..3f84a7d9 --- /dev/null +++ b/src/main/java/org/dynmap/HeroChatHandler.java @@ -0,0 +1,282 @@ +package org.dynmap; + +import java.util.logging.Logger; + +import org.bukkit.Server; +import org.bukkit.event.CustomEventListener; +import org.bukkit.plugin.Plugin; +import org.bukkit.util.config.Configuration; +import org.bukkit.event.Event; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.event.server.ServerListener; +import java.util.List; +import java.util.Collections; +import java.lang.reflect.Method; + +public class HeroChatHandler { + protected static final Logger log = Logger.getLogger("Minecraft"); + + private static final String DEF_CHANNEL = "Global"; + private static final List DEF_CHANNELS = Collections + .singletonList(DEF_CHANNEL); + + private List hcchannels; + private String hcwebinputchannel; + private HeroChatChannel hcwebinputchan; + private DynmapPlugin plugin; + + private class OurPluginListener extends ServerListener { + @Override + public void onPluginEnable(PluginEnableEvent event) { + Plugin plugin = event.getPlugin(); + String name = plugin.getDescription().getName(); + + if (name.equals("HeroChat")) { + activateHeroChat(plugin); + } + } + } + + /* Reflection-based access wrapper for ChannelChatEvent from HeroChat */ + private static class HeroChatChannelChatEvent { + private static Class channelchatevent; + private static Method getsource; + private static Method getmessage; + private static boolean isgood = false; + private Event evt; + + @SuppressWarnings("unchecked") + public static boolean initialize() { + try { + channelchatevent = Class + .forName("com.herocraftonline.dthielke.herochat.event.ChannelChatEvent"); + getsource = channelchatevent.getMethod("getSource", new Class[0]); + getmessage = channelchatevent.getMethod("getMessage", new Class[0]); + isgood = true; + } catch (ClassNotFoundException cnfx) { + } catch (NoSuchMethodException nsmx) { + } + return isgood; + } + + public HeroChatChannelChatEvent(Event evt) { + this.evt = evt; + } + + public static boolean isInstance(Event evt) { + return channelchatevent.isInstance(evt); + } + + public String getSource() { + try { + return (String) getsource.invoke(evt); + } catch (Exception x) { + return null; + } + } + + public String getMessage() { + try { + return (String) getmessage.invoke(evt); + } catch (Exception x) { + return null; + } + } + } + + /* Reflection-based access wrapper for ChannelEvent from HeroChat */ + private static class HeroChatChannelEvent { + private static Class channelevent; + private static Method getchannel; + private static Method iscancelled; + private static boolean isgood = false; + private Event evt; + + @SuppressWarnings("unchecked") + public static boolean initialize() { + try { + channelevent = Class + .forName("com.herocraftonline.dthielke.herochat.event.ChannelEvent"); + getchannel = channelevent.getMethod("getChannel", new Class[0]); + iscancelled = channelevent.getMethod("isCancelled", new Class[0]); + isgood = true; + } catch (ClassNotFoundException cnfx) { + } catch (NoSuchMethodException nsmx) { + } + return isgood; + } + + public HeroChatChannelEvent(Event evt) { + this.evt = evt; + } + + public static boolean isInstance(Event evt) { + return channelevent.isInstance(evt); + } + + public HeroChatChannel getChannel() { + try { + Object o; + o = getchannel.invoke(evt); + if (o != null) { + return new HeroChatChannel(o); + } + } catch (Exception x) { + } + return null; + } + + public boolean isCancelled() { + try { + return (Boolean) iscancelled.invoke(evt); + } catch (Exception x) { + return true; + } + } + } + + /* Reflection-based access wrapper for Channel from HeroChat */ + private static class HeroChatChannel { + private static Class channel; + private static Method getname; + private static Method getnick; + private static Method sendmessage; + private static boolean isgood = false; + private Object chan; + + @SuppressWarnings("unchecked") + public static boolean initialize() { + try { + channel = Class + .forName("com.herocraftonline.dthielke.herochat.channels.Channel"); + getname = channel.getMethod("getName"); + getnick = channel.getMethod("getNick", new Class[0]); + sendmessage = channel.getMethod("sendMessage", new Class[] { + String.class, String.class, String.class, boolean.class } ); + isgood = true; + } catch (ClassNotFoundException cnfx) { + } catch (NoSuchMethodException nsmx) { + System.out.println(nsmx); + } + return isgood; + } + + public HeroChatChannel(Object chan) { + this.chan = chan; + } + + public String getName() { + try { + return (String) getname.invoke(chan); + } catch (Exception x) { + return null; + } + } + + public String getNick() { + try { + return (String) getnick.invoke(chan); + } catch (Exception x) { + return null; + } + } + + public void sendMessage(String source, String msg, String format, boolean sentByPlayer) { + try { + sendmessage.invoke(chan, source, msg, format, sentByPlayer); + } catch (Exception x) { + } + } + } + + private class OurEventListener extends CustomEventListener { + /** + * Handle custom events + */ + @Override + public void onCustomEvent(Event event) { + if (HeroChatChannelEvent.isInstance(event)) { + HeroChatChannelEvent ce = new HeroChatChannelEvent(event); + /* Snoop for our web channel - we'll need it, and we'll see it before it matters, + * since anyone that joins the channel will give us an event (and reflection on + * the plugin class to get the manager didn't work, due to a dependency on the IRC + * plugin that may not be present....) + */ + HeroChatChannel c = ce.getChannel(); + /* If channel name or nickname matches out web channel, remember it */ + if((c != null) && (hcwebinputchannel != null) && + ((c.getName().equals(hcwebinputchannel)) || + c.getNick().equals(hcwebinputchannel))) { + hcwebinputchan = c; + } + if (ce.isCancelled()) + return; + if (HeroChatChannelChatEvent.isInstance(event)) { + HeroChatChannelChatEvent cce = new HeroChatChannelChatEvent( + event); + /* Match on name or nickname of channel */ + if (hcchannels.contains(c.getName()) || + hcchannels.contains(c.getNick())) { + plugin.mapManager.pushUpdate(new Client.ChatMessage( + "player", "[" + c.getNick() + "] " + + cce.getSource(), cce.getMessage())); + } + } + } + } + } + + public HeroChatHandler(Configuration cfg, DynmapPlugin plugin, Server server) { + /* If we're enabling hero chat support */ + if (cfg.getNode("web").getBoolean("enableherochat", false)) { + log.info("[dynmap] HeroChat support configured"); + this.plugin = plugin; + /* Now, get the monitored channel list */ + hcchannels = cfg.getNode("web").getStringList("herochatchannels", + DEF_CHANNELS); + /* And get channel to send web messages */ + hcwebinputchannel = cfg.getNode("web").getString( + "herochatwebchannel", DEF_CHANNEL); + /* Set up to hear when HeroChat is enabled */ + server.getPluginManager().registerEvent(Event.Type.PLUGIN_ENABLE, + new OurPluginListener(), Event.Priority.Normal, plugin); + } + } + + private void activateHeroChat(Plugin herochat) { + if (HeroChatChannelChatEvent.initialize() == false) { + log.severe("[dynmap] Cannot load HeroChat chat event class!"); + return; + } + if (HeroChatChannel.initialize() == false) { + log.severe("[dynmap] Cannot load HeroChat channel class!"); + return; + } + if (HeroChatChannelEvent.initialize() == false) { + log.severe("[dynmap] Cannot load HeroChat channel event class!"); + return; + } + + /* Register event handler */ + plugin.getServer().getPluginManager().registerEvent(Event.Type.CUSTOM_EVENT, + new OurEventListener(), Event.Priority.Monitor, plugin); + log.info("[dynmap] HeroChat integration active"); + } + /** + * Send message from web to appropriate HeroChat channel + * @param sender - sender ID + * @param message - message + * @return true if herochat is handling this, false if not + */ + public boolean sendWebMessageToHeroChat(String sender, String message) { + if(hcwebinputchannel != null) { /* Are we handling them? */ + if(hcwebinputchan != null) { /* Have we seen it yet? Maybe no if nobody has logged on or + * joined it, but then who would see it anyway? + */ + hcwebinputchan.sendMessage(sender, message, "{default}", false); + } + return true; + } + return false; + } +} diff --git a/src/main/java/org/dynmap/UpdateQueue.java b/src/main/java/org/dynmap/UpdateQueue.java index 368c987d..995a57c5 100644 --- a/src/main/java/org/dynmap/UpdateQueue.java +++ b/src/main/java/org/dynmap/UpdateQueue.java @@ -11,10 +11,11 @@ public class UpdateQueue { private static final int maxUpdateAge = 120000; - public synchronized void pushUpdate(Object obj) { - long now = System.currentTimeMillis(); - long deadline = now - maxUpdateAge; + public void pushUpdate(Object obj) { synchronized (lock) { + /* Do inside lock - prevent delay between time and actual work */ + long now = System.currentTimeMillis(); + long deadline = now - maxUpdateAge; ListIterator i = updateQueue.listIterator(0); while (i.hasNext()) { Update u = i.next(); @@ -27,11 +28,11 @@ public class UpdateQueue { private ArrayList tmpupdates = new ArrayList(); - public synchronized Object[] getUpdatedObjects(long since) { - long now = System.currentTimeMillis(); - long deadline = now - maxUpdateAge; + public Object[] getUpdatedObjects(long since) { Object[] updates; synchronized (lock) { + long now = System.currentTimeMillis(); + long deadline = now - maxUpdateAge; tmpupdates.clear(); Iterator it = updateQueue.descendingIterator(); while (it.hasNext()) { diff --git a/src/main/java/org/dynmap/flat/FlatMap.java b/src/main/java/org/dynmap/flat/FlatMap.java index acdc49c0..b9e2abd3 100644 --- a/src/main/java/org/dynmap/flat/FlatMap.java +++ b/src/main/java/org/dynmap/flat/FlatMap.java @@ -1,6 +1,6 @@ package org.dynmap.flat; -import java.awt.Color; +import org.dynmap.Color; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.io.File; @@ -11,6 +11,7 @@ import javax.imageio.ImageIO; import org.bukkit.Location; import org.bukkit.World; +import org.bukkit.World.Environment; import org.dynmap.Client; import org.dynmap.ColorScheme; import org.dynmap.DynmapChunk; @@ -22,10 +23,17 @@ import org.dynmap.debug.Debug; public class FlatMap extends MapType { private String prefix; private ColorScheme colorScheme; - + private int maximumHeight = 127; + public FlatMap(Map configuration) { prefix = (String) configuration.get("prefix"); colorScheme = ColorScheme.getScheme((String) configuration.get("colorscheme")); + Object o = configuration.get("maximumheight"); + if (o != null) { + maximumHeight = Integer.parseInt(String.valueOf(o)); + if (maximumHeight > 127) + maximumHeight = 127; + } } @Override @@ -68,6 +76,7 @@ public class FlatMap extends MapType { public boolean render(MapTile tile, File outputFile) { FlatMapTile t = (FlatMapTile) tile; World w = t.getWorld(); + boolean isnether = (w.getEnvironment() == Environment.NETHER) && (maximumHeight == 127); boolean rendered = false; BufferedImage im = new BufferedImage(t.size, t.size, BufferedImage.TYPE_INT_RGB); @@ -79,17 +88,40 @@ public class FlatMap extends MapType { for (int y = 0; y < t.size; y++) { int mx = x + t.x * t.size; int mz = y + t.y * t.size; - int my = w.getHighestBlockYAt(mx, mz) - 1; - int blockType = w.getBlockTypeIdAt(mx, my, mz); - byte data = 0; - if(colorScheme.datacolors[blockType] != null) { /* If data colored */ - data = w.getBlockAt(mx, my, mz).getData(); + int my; + int blockType; + if(isnether) { + /* Scan until we hit air */ + my = 127; + while((blockType = w.getBlockTypeIdAt(mx, my, mz)) != 0) { + my--; + if(my < 0) { /* Solid - use top */ + my = 127; + blockType = w.getBlockTypeIdAt(mx, my, mz); + break; + } + } + if(blockType == 0) { /* Hit air - now find non-air */ + while((blockType = w.getBlockTypeIdAt(mx, my, mz)) == 0) { + my--; + if(my < 0) { + my = 0; + break; + } + } + } + } + else { + my = w.getHighestBlockYAt(mx, mz) - 1; + if(my > maximumHeight) my = maximumHeight; + blockType = w.getBlockTypeIdAt(mx, my, mz); + } + byte data = 0; + Color[] colors = colorScheme.colors[blockType]; + if(colorScheme.datacolors[blockType] != null) { + data = w.getBlockAt(mx, my, mz).getData(); + colors = colorScheme.datacolors[blockType][data]; } - Color[] colors; - if(data != 0) - colors = colorScheme.datacolors[blockType][data]; - else - colors = colorScheme.colors[blockType]; if (colors == null) continue; Color c = colors[0]; @@ -103,7 +135,7 @@ public class FlatMap extends MapType { // Defines the 'step' in coloring. float step = 10 / 128.0f; - + // The step applied to height. float scale = ((int)(height/step))*step; @@ -135,19 +167,19 @@ public class FlatMap extends MapType { final MapTile mtile = tile; final BufferedImage img = im; MapManager.mapman.enqueueImageWrite(new Runnable() { - public void run() { - Debug.debug("saving image " + fname.getPath()); - try { - ImageIO.write(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); - } - img.flush(); - MapManager.mapman.pushUpdate(mtile.getWorld(), - new Client.Tile(mtile.getFilename())); - } + public void run() { + Debug.debug("saving image " + fname.getPath()); + try { + ImageIO.write(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); + } + img.flush(); + MapManager.mapman.pushUpdate(mtile.getWorld(), + new Client.Tile(mtile.getFilename())); + } }); return rendered; diff --git a/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java b/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java index afdb1db7..3c1f557f 100644 --- a/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/CaveTileRenderer.java @@ -1,6 +1,6 @@ package org.dynmap.kzedmap; -import java.awt.Color; +import org.dynmap.Color; import java.util.Map; import org.bukkit.World; @@ -12,14 +12,20 @@ public class CaveTileRenderer extends DefaultTileRenderer { } @Override - protected Color scan(World world, int x, int y, int z, int seq) { + protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { boolean air = true; - + result.setTransparent(); for (;;) { if (y < 0) - return translucent; + return; int id = world.getBlockTypeIdAt(x, y, z); + if(isnether) { /* Make ceiling into air in nether */ + if(id != 0) + id = 0; + else + isnether = false; + } switch (seq) { case 0: @@ -87,7 +93,8 @@ public class CaveTileRenderer extends DefaultTileRenderer { cg = cg * mult / 256; cb = cb * mult / 256; - return new Color(cr, cg, cb); + result.setRGBA(cr, cg, cb, 255); + return; } } } diff --git a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java index 3b486df2..8e034835 100644 --- a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java @@ -1,6 +1,6 @@ package org.dynmap.kzedmap; -import java.awt.Color; +import org.dynmap.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; @@ -12,7 +12,9 @@ import java.util.Map; import javax.imageio.ImageIO; +import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.World.Environment; import org.dynmap.Client; import org.dynmap.ColorScheme; import org.dynmap.MapManager; @@ -28,25 +30,6 @@ public class DefaultTileRenderer implements MapTileRenderer { protected HashSet highlightBlocks = new HashSet(); protected Color highlightColor = new Color(255, 0, 0); - private static final Color[] woolshades = { - Color.WHITE, - Color.ORANGE, - Color.MAGENTA, - new Color(51,204,255), - Color.YELLOW, - new Color(102,255,102), - Color.PINK, - Color.GRAY, - Color.LIGHT_GRAY, - Color.CYAN, - new Color(255,0,255), - Color.BLUE, - new Color(102,51,51), - Color.GREEN, - Color.RED, - Color.BLACK - }; - @Override public String getName() { return name; @@ -65,6 +48,7 @@ public class DefaultTileRenderer implements MapTileRenderer { public boolean render(KzedMapTile tile, File outputFile) { World world = tile.getWorld(); + boolean isnether = (world.getEnvironment() == Environment.NETHER); BufferedImage im = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); WritableRaster r = im.getRaster(); @@ -74,28 +58,36 @@ public class DefaultTileRenderer implements MapTileRenderer { int iy = maximumHeight; int iz = KzedMap.anchorz + tile.px / 2 - tile.py / 2 + ((127-maximumHeight)/2); + /* Don't mess with existing height-clipped renders */ + if(maximumHeight < 127) + isnether = false; + int jx, jz; - + int x, y; + Color c1 = new Color(); + Color c2 = new Color(); + int[] rgb = new int[3]; /* draw the map */ for (y = 0; y < KzedMap.tileHeight;) { jx = ix; jz = iz; for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) { - Color c1 = scan(world, jx, iy, jz, 0); - Color c2 = scan(world, jx, iy, jz, 2); - isempty = isempty && c1 == translucent && c2 == translucent; - r.setPixel(x, y, new int[] { - c1.getRed(), - c1.getGreen(), - c1.getBlue() }); - r.setPixel(x - 1, y, new int[] { - c2.getRed(), - c2.getGreen(), - c2.getBlue() }); - + scan(world, jx, iy, jz, 0, isnether, c1); + scan(world, jx, iy, jz, 2, isnether, c2); + if(c1.isTransparent() == false) { + rgb[0] = c1.getRed(); rgb[1] = c1.getGreen(); rgb[2] = c1.getBlue(); + r.setPixel(x, y, rgb); + isempty = false; + } + if(c2.isTransparent() == false) { + rgb[0] = c2.getRed(); rgb[1] = c2.getGreen(); rgb[2] = c2.getBlue(); + r.setPixel(x - 1, y, rgb); + isempty = false; + } + jx++; jz++; @@ -107,19 +99,21 @@ public class DefaultTileRenderer implements MapTileRenderer { jz = iz - 1; for (x = KzedMap.tileWidth - 1; x >= 0; x -= 2) { - Color c1 = scan(world, jx, iy, jz, 2); + scan(world, jx, iy, jz, 2, isnether, c1); jx++; jz++; - Color c2 = scan(world, jx, iy, jz, 0); - isempty = isempty && c1 == translucent && c2 == translucent; - r.setPixel(x, y, new int[] { - c1.getRed(), - c1.getGreen(), - c1.getBlue() }); - r.setPixel(x - 1, y, new int[] { - c2.getRed(), - c2.getGreen(), - c2.getBlue() }); + scan(world, jx, iy, jz, 0, isnether, c2); + if(c1.isTransparent() == false) { + rgb[0] = c1.getRed(); rgb[1] = c1.getGreen(); rgb[2] = c1.getBlue(); + r.setPixel(x, y, rgb); + isempty = false; + } + if(c2.isTransparent() == false) { + rgb[0] = c2.getRed(); rgb[1] = c2.getGreen(); rgb[2] = c2.getBlue(); + + r.setPixel(x - 1, y, rgb); + isempty = false; + } } y++; @@ -132,100 +126,112 @@ public class DefaultTileRenderer implements MapTileRenderer { final File fname = outputFile; final KzedMapTile mtile = tile; final BufferedImage img = im; - final KzedZoomedMapTile zmtile = new KzedZoomedMapTile(mtile.getWorld(), - (KzedMap) mtile.getMap(), mtile); - final File zoomFile = MapManager.mapman.getTileFile(zmtile); - - MapManager.mapman.enqueueImageWrite(new Runnable() { - public void run() { - doFileWrites(fname, mtile, img, zmtile, zoomFile); - } + final KzedZoomedMapTile zmtile = new KzedZoomedMapTile(mtile.getWorld(), + (KzedMap) mtile.getMap(), mtile); + final File zoomFile = MapManager.mapman.getTileFile(zmtile); + + MapManager.mapman.enqueueImageWrite(new Runnable() { + public void run() { + doFileWrites(fname, mtile, img, zmtile, zoomFile); + } }); return !isempty; } private void doFileWrites(final File fname, final KzedMapTile mtile, - final BufferedImage img, final KzedZoomedMapTile zmtile, final File zoomFile) { - Debug.debug("saving image " + fname.getPath()); - try { - ImageIO.write(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); - } - mtile.file = fname; - // Since we've already got the new tile, and we're on an async thread, just - // make the zoomed tile here - int px = mtile.px; - int py = mtile.py; - int zpx = zmtile.getTileX(); - int zpy = zmtile.getTileY(); + final BufferedImage img, final KzedZoomedMapTile zmtile, final File zoomFile) { + Debug.debug("saving image " + fname.getPath()); + try { + ImageIO.write(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); + } + mtile.file = fname; + // Since we've already got the new tile, and we're on an async thread, just + // make the zoomed tile here + int px = mtile.px; + int py = mtile.py; + int zpx = zmtile.getTileX(); + int zpy = zmtile.getTileY(); - /* scaled size */ - int scw = KzedMap.tileWidth / 2; - int sch = KzedMap.tileHeight / 2; + /* scaled size */ + int scw = KzedMap.tileWidth / 2; + int sch = KzedMap.tileHeight / 2; - /* origin in zoomed-out tile */ - int ox = 0; - int oy = 0; + /* origin in zoomed-out tile */ + int ox = 0; + int oy = 0; - if (zpx != px) - ox = scw; - if (zpy != py) - oy = sch; + if (zpx != px) + ox = scw; + if (zpy != py) + oy = sch; - BufferedImage zIm = null; - try { - zIm = ImageIO.read(zoomFile); - } catch (IOException e) { - } catch (IndexOutOfBoundsException e) { - } + BufferedImage zIm = null; + try { + zIm = ImageIO.read(zoomFile); + } catch (IOException e) { + } catch (IndexOutOfBoundsException e) { + } - if (zIm == null) { - /* create new one */ - zIm = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); - Debug.debug("New zoom-out tile created " + zmtile.getFilename()); - } else { - Debug.debug("Loaded zoom-out tile from " + zmtile.getFilename()); - } - - /* blit scaled rendered tile onto zoom-out tile */ - Graphics2D g2 = zIm.createGraphics(); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.drawImage(img, ox, oy, scw, sch, null); + if (zIm == null) { + /* create new one */ + zIm = new BufferedImage(KzedMap.tileWidth, KzedMap.tileHeight, BufferedImage.TYPE_INT_RGB); + Debug.debug("New zoom-out tile created " + zmtile.getFilename()); + } else { + Debug.debug("Loaded zoom-out tile from " + zmtile.getFilename()); + } + + /* blit scaled rendered tile onto zoom-out tile */ + Graphics2D g2 = zIm.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(img, ox, oy, scw, sch, null); - img.flush(); + img.flush(); - /* save zoom-out tile */ - - try { - ImageIO.write(zIm, "png", zoomFile); - Debug.debug("Saved zoom-out tile at " + zoomFile.getName()); - } catch (IOException e) { - Debug.error("Failed to save zoom-out tile: " + zoomFile.getName(), e); - } catch (java.lang.NullPointerException e) { - Debug.error("Failed to save zoom-out tile (NullPointerException): " + zoomFile.getName(), e); - } - zIm.flush(); - /* Push updates for both files.*/ - MapManager.mapman.pushUpdate(mtile.getWorld(), - new Client.Tile(mtile.getFilename())); - MapManager.mapman.pushUpdate(zmtile.getWorld(), - new Client.Tile(zmtile.getFilename())); + /* save zoom-out tile */ + + try { + ImageIO.write(zIm, "png", zoomFile); + Debug.debug("Saved zoom-out tile at " + zoomFile.getName()); + } catch (IOException e) { + Debug.error("Failed to save zoom-out tile: " + zoomFile.getName(), e); + } catch (java.lang.NullPointerException e) { + Debug.error("Failed to save zoom-out tile (NullPointerException): " + zoomFile.getName(), e); + } + zIm.flush(); + /* Push updates for both files.*/ + MapManager.mapman.pushUpdate(mtile.getWorld(), + new Client.Tile(mtile.getFilename())); + MapManager.mapman.pushUpdate(zmtile.getWorld(), + new Client.Tile(zmtile.getFilename())); } - protected Color scan(World world, int x, int y, int z, int seq) { + protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { + result.setTransparent(); for (;;) { - if (y < 0) - return translucent; - + if (y < 0) { + return; + } int id = world.getBlockTypeIdAt(x, y, z); byte data = 0; - if(colorScheme.datacolors[id] != null) { /* If data colored */ - data = world.getBlockAt(x, y, z).getData(); + if(isnether) { /* Make bedrock ceiling into air in nether */ + if(id != 0) { + /* Remember first color we see, in case we wind up solid */ + if(result.isTransparent()) + if(colorScheme.colors[id] != null) + result.setColor(colorScheme.colors[id][seq]); + id = 0; + } + else + isnether = false; + } + if(colorScheme.datacolors[id] != null) { /* If data colored */ + data = world.getBlockAt(x, y, z).getData(); } switch (seq) { case 0: @@ -246,24 +252,26 @@ public class DefaultTileRenderer implements MapTileRenderer { if (id != 0) { if (highlightBlocks.contains(id)) { - return highlightColor; + result.setColor(highlightColor); + return; } Color[] colors; if(data != 0) - colors = colorScheme.datacolors[id][data]; + colors = colorScheme.datacolors[id][data]; else - colors = colorScheme.colors[id]; + colors = colorScheme.colors[id]; if (colors != null) { Color c = colors[seq]; if (c.getAlpha() > 0) { /* we found something that isn't transparent! */ if (c.getAlpha() == 255) { /* it's opaque - the ray ends here */ - return c; + result.setColor(c); + return; } /* this block is transparent, so recurse */ - Color bg = scan(world, x, y, z, seq); + scan(world, x, y, z, seq, isnether, result); int cr = c.getRed(); int cg = c.getGreen(); @@ -273,8 +281,8 @@ public class DefaultTileRenderer implements MapTileRenderer { cg *= ca; cb *= ca; int na = 255 - ca; - - return new Color((bg.getRed() * na + cr) >> 8, (bg.getGreen() * na + cg) >> 8, (bg.getBlue() * na + cb) >> 8); + result.setRGBA((result.getRed() * na + cr) >> 8, (result.getGreen() * na + cg) >> 8, (result.getBlue() * na + cb) >> 8, 255); + return; } } } diff --git a/src/main/java/org/dynmap/kzedmap/HighlightTileRenderer.java b/src/main/java/org/dynmap/kzedmap/HighlightTileRenderer.java index dd639b70..826fed17 100644 --- a/src/main/java/org/dynmap/kzedmap/HighlightTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/HighlightTileRenderer.java @@ -1,6 +1,6 @@ package org.dynmap.kzedmap; -import java.awt.Color; +import org.dynmap.Color; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -21,19 +21,30 @@ public class HighlightTileRenderer extends DefaultTileRenderer { highlightBlocks.add((Integer)highlightObj); } } - + @Override - protected Color scan(World world, int x, int y, int z, int seq) { - Color result = translucent; + protected void scan(World world, int x, int y, int z, int seq, boolean isnether, final Color result) { + result.setTransparent(); for (;;) { if (y < 0) { break; } int id = world.getBlockTypeIdAt(x, y, z); + if(isnether) { /* Make bedrock ceiling into air in nether */ + if(id != 0) { + /* Remember first color we see, in case we wind up solid */ + if(result.isTransparent()) + if(colorScheme.colors[id] != null) + result.setColor(colorScheme.colors[id][seq]); + id = 0; + } + else + isnether = false; + } byte data = 0; - if(colorScheme.datacolors[id] != null) { /* If data colored */ - data = world.getBlockAt(x, y, z).getData(); + if(colorScheme.datacolors[id] != null) { /* If data colored */ + data = world.getBlockAt(x, y, z).getData(); } switch (seq) { @@ -56,14 +67,15 @@ public class HighlightTileRenderer extends DefaultTileRenderer { if (id != 0) { Color[] colors; if(data != 0) - colors = colorScheme.datacolors[id][data]; + colors = colorScheme.datacolors[id][data]; else - colors = colorScheme.colors[id]; + colors = colorScheme.colors[id]; if (colors != null) { Color c = colors[seq]; if (highlightBlocks.contains(id)) { - return c; + result.setColor(c); + return; } if (c.getAlpha() > 0) { @@ -76,26 +88,22 @@ public class HighlightTileRenderer extends DefaultTileRenderer { // No need to blend if result is opaque. if (result.getAlpha() < 255) { - Color bg = c; - c = result; - - int cr = c.getRed(); - int cg = c.getGreen(); - int cb = c.getBlue(); - int ca = c.getAlpha(); + int cr = result.getRed(); + int cg = result.getGreen(); + int cb = result.getBlue(); + int ca = result.getAlpha(); cr *= ca; cg *= ca; cb *= ca; int na = 255 - ca; - result = new Color((bg.getRed() * na + cr) >> 8, (bg.getGreen() * na + cg) >> 8, (bg.getBlue() * na + cb) >> 8, - Math.min(255, bg.getAlpha()+c.getAlpha()) // Not really correct, but gets the job done without recursion while still looking ok. + result.setRGBA((c.getRed() * na + cr) >> 8, (c.getGreen() * na + cg) >> 8, (c.getBlue() * na + cb) >> 8, + Math.min(255, c.getAlpha()+ca) // Not really correct, but gets the job done without recursion while still looking ok. ); } } } } } - return result; } } diff --git a/src/main/java/org/dynmap/kzedmap/KzedMap.java b/src/main/java/org/dynmap/kzedmap/KzedMap.java index 09841714..8c44acf2 100644 --- a/src/main/java/org/dynmap/kzedmap/KzedMap.java +++ b/src/main/java/org/dynmap/kzedmap/KzedMap.java @@ -138,6 +138,26 @@ public class KzedMap extends MapType { onTileInvalidated.trigger(tile); } + /** + * Test if point x,z is inside rectangle with corner at r0x,r0z and with + * size vectors s1x,s1z and s2x,s2z + * + */ + private boolean testPointInRectangle(int x, int z, int r0x, int r0z, int s1x, int s1z, + int s2x, int s2z) { + int xr = x - r0x; + int zr = z - r0z; /* Get position relative to rectangle corner */ + int dots1 = xr*s1x + zr*s1z; + int dots2 = xr*s2x + zr*s2z; + /* If dot product of relative point and each side is between zero and dot product + * of each side and itself, we're inside + */ + if((dots1 >= 0) && (dots1 <= (s1x*s1x+s1z*s1z)) && + (dots2 >= 0) && (dots2 <= (s2x*s2x+s2z*s2z))) { + return true; + } + return false; + } @Override public DynmapChunk[] getRequiredChunks(MapTile tile) { if (tile instanceof KzedMapTile) { @@ -155,13 +175,43 @@ public class KzedMap extends MapType { int x, z; + /* Actual pattern of chunks needed is create by the slanted + * square prism corresponding to the render path of the tile. + * Top of prism (corresponding to y=127) is diamond shape from + * ix, iz to ix+64,iz+64 to ix+128,iz to ix+64,iz-64 + * Bottom is same shape, offset by -64 on x, +64 on z (net + * render path to y=0), correspond to ix-64, iz+64 to + * ix,iz+128 to ix+64,iz+64 to ix,iz. Projection of + * the prism on to the x,z plane (which is all that matters for + * chunks) yields a diagonal rectangular area from ix-64(x1),iz+64 + * to ix,iz+128(z2) to ix+128(x2),iz to ix+64,iz-64(z1). + * Chunks outside this are not needed - we scan a simple rectangle + * (chunk grid aligned) and skip adding the ones that are outside. + * This results in 42% less chunks being loaded. + */ ArrayList chunks = new ArrayList(); + for (x = x1; x < x2; x += 16) { for (z = z1; z < z2; z += 16) { + /* If any of the chunk corners are inside the rectangle, we need it */ + if((!testPointInRectangle(x, z, x1, iz + KzedMap.tileWidth/2, + KzedMap.tileWidth/2, KzedMap.tileHeight/2, + KzedMap.tileWidth, -KzedMap.tileHeight)) && + (!testPointInRectangle(x+15, z, x1, iz + KzedMap.tileWidth/2, + KzedMap.tileWidth/2, KzedMap.tileHeight/2, + KzedMap.tileWidth, -KzedMap.tileHeight)) && + (!testPointInRectangle(x+15, z+15, x1, iz + KzedMap.tileWidth/2, + KzedMap.tileWidth/2, KzedMap.tileHeight/2, + KzedMap.tileWidth, -KzedMap.tileHeight)) && + (!testPointInRectangle(x, z+15, x1, iz + KzedMap.tileWidth/2, + KzedMap.tileWidth/2, KzedMap.tileHeight/2, + KzedMap.tileWidth, -KzedMap.tileHeight))) + continue; DynmapChunk chunk = new DynmapChunk(x / 16, z / 16); chunks.add(chunk); } } + DynmapChunk[] result = new DynmapChunk[chunks.size()]; chunks.toArray(result); return result; diff --git a/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java b/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java index 4d5dee8d..a0de1520 100644 --- a/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/ZoomedTileRenderer.java @@ -1,18 +1,8 @@ package org.dynmap.kzedmap; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; import java.io.File; -import java.io.IOException; import java.util.Map; -import javax.imageio.ImageIO; - -import org.dynmap.Client; -import org.dynmap.MapManager; -import org.dynmap.debug.Debug; - public class ZoomedTileRenderer { public ZoomedTileRenderer(Map configuration) { } diff --git a/web/js/map.js b/web/js/map.js index 0ae78f17..fdd69d2b 100644 --- a/web/js/map.js +++ b/web/js/map.js @@ -386,7 +386,7 @@ DynMap.prototype = { swtch(update.type, { tile: function() { - me.onTileUpdated(update.name); + me.onTileUpdated(update.name,update.timestamp); }, playerjoin: function() { $(me).trigger('playerjoin', [ update.playerName ]); @@ -436,12 +436,12 @@ DynMap.prototype = { unregisterTile: function(mapType, tileName) { delete this.registeredTiles[tileName]; }, - onTileUpdated: function(tileName) { + onTileUpdated: function(tileName,timestamp) { var me = this; var tile = this.registeredTiles[tileName]; if (tile) { - tile.lastseen = this.lasttimestamp; + tile.lastseen = timestamp; tile.mapType.onTileUpdated(tile.tileElement, tileName); } },