diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 7b5b0ac5..f1799abe 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -4,8 +4,8 @@ description = 'dynmap' dependencies { compile group: 'org.bukkit', name: 'bukkit', version:'1.7.10-R0.1-SNAPSHOT' compile 'com.nijikokun.bukkit:Permissions:3.1.6' - compile project(":dynmap-api") - compile "us.dynmap:DynmapCore:${project.version}" + compile project(path: ":dynmap-api", configuration: "shadow") + compile project(path: ":dynmap-core", configuration: "shadow") compile group: 'ru.tehkode', name: 'PermissionsEx', version:'1.19.1' compile group: 'de.bananaco', name: 'bPermissions', version:'2.9.1' compile group: 'com.platymuus.bukkit.permissions', name: 'PermissionsBukkit', version:'1.6' @@ -39,7 +39,7 @@ shadowJar { dependencies { include(dependency('org.bstats::')) include(dependency(':dynmap-api')) - include(dependency('us.dynmap:DynmapCore:')) + include(dependency(":dynmap-core")) include(dependency(':bukkit-helper')) include(dependency(':bukkit-helper-113')) } diff --git a/dynmap-core/.gitignore b/dynmap-core/.gitignore new file mode 100644 index 00000000..84c048a7 --- /dev/null +++ b/dynmap-core/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/dynmap-core/build.gradle b/dynmap-core/build.gradle new file mode 100644 index 00000000..e3044c29 --- /dev/null +++ b/dynmap-core/build.gradle @@ -0,0 +1,52 @@ +description = "DynmapCore" + +dependencies { + compile "us.dynmap:DynmapCoreAPI:${project.version}" + compile 'org.eclipse.jetty:jetty-server:8.1.21.v20160908' + compile 'org.eclipse.jetty:jetty-servlet:8.1.21.v20160908' + compile 'com.googlecode.json-simple:json-simple:1.1.1' + compile 'org.yaml:snakeyaml:1.9' + compile 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20180219.1' +} + +processResources { + // replace stuff in mcmod.info, nothing else + from('src/main/resources') { + include 'core.yml' + include 'lightings.txt' + include 'perspectives.txt' + include 'extracted/web/version.js' + include 'extracted/web/index.html' + include 'extracted/web/login.html' + // replace version and mcversion + expand( + buildnumber: project.parent.ext.globals.buildNumber, + version: project.version + ) + } +} + +jar { + classifier = 'unshaded' +} + +shadowJar { + dependencies { + include(dependency('com.googlecode.json-simple:json-simple:')) + include(dependency('org.yaml:snakeyaml:')) + include(dependency('com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:')) + include(dependency('org.eclipse.jetty::')) + include(dependency('org.eclipse.jetty.orbit:javax.servlet:')) + } + relocate('org.json.simple', 'org.dynmap.json.simple') + relocate('org.yaml.snakeyaml', 'org.dynmap.snakeyaml') + relocate('org.eclipse.jetty', 'org.dynmap.jetty') + relocate('org.owasp.html', 'org.dynmap.org.owasp.html') + relocate('javax.servlet', 'org.dynmap.javax.servlet' ) + destinationDir = file '../target' + classifier = '' +} + +artifacts { + archives shadowJar +} diff --git a/dynmap-core/src/.gitignore b/dynmap-core/src/.gitignore new file mode 100644 index 00000000..e43b0f98 --- /dev/null +++ b/dynmap-core/src/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/dynmap-core/src/main/java/org/dynmap/AsynchronousQueue.java b/dynmap-core/src/main/java/org/dynmap/AsynchronousQueue.java new file mode 100644 index 00000000..30bdfb62 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/AsynchronousQueue.java @@ -0,0 +1,161 @@ +package org.dynmap; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; + +public class AsynchronousQueue { + private Object lock = new Object(); + private Thread thread; + private LinkedBlockingQueue queue = new LinkedBlockingQueue(); + private Set set = new HashSet(); + private Handler handler; + private int dequeueTime; + private int accelDequeueTime; + public int accelDequeueThresh; + private int pendingcnt; + private int pendinglimit; + private boolean normalprio; + + public AsynchronousQueue(Handler handler, int dequeueTime, int accelDequeueThresh, int accelDequeueTime, int pendinglimit, boolean normalprio) { + this.handler = handler; + this.dequeueTime = dequeueTime; + this.accelDequeueTime = accelDequeueTime; + this.accelDequeueThresh = accelDequeueThresh; + if(pendinglimit < 1) pendinglimit = 1; + this.pendinglimit = pendinglimit; + this.normalprio = normalprio; + } + + public boolean push(T t) { + synchronized (lock) { + if (!set.add(t)) { + return false; + } + } + queue.offer(t); + return true; + } + + private T pop() { + try { + T t = queue.take(); + synchronized (lock) { + set.remove(t); + } + return t; + } catch (InterruptedException ix) { + return null; + } + } + + public boolean remove(T t) { + synchronized (lock) { + if (set.remove(t)) { + queue.remove(t); + return true; + } + } + return false; + } + + public int size() { + return set.size(); + } + + public List popAll() { + List s; + synchronized(lock) { + s = new ArrayList(queue); + queue.clear(); + set.clear(); + } + return s; + } + + public void start() { + synchronized (lock) { + thread = new Thread(new Runnable() { + @Override + public void run() { + running(); + } + }); + thread.start(); + try { + if(!normalprio) + 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..."); + + oldThread.interrupt(); + try { + oldThread.join(1000); + } catch (InterruptedException e) { + Log.info("Waiting for map renderer to stop is interrupted"); + } + } + } + + private void running() { + try { + while (Thread.currentThread() == thread) { + synchronized(lock) { + while(pendingcnt >= pendinglimit) { + try { + lock.wait(accelDequeueTime); + } catch (InterruptedException ix) { + if(Thread.currentThread() != thread) + return; + throw ix; + } + } + } + T t = pop(); + if (t != null) { + synchronized(lock) { + pendingcnt++; + } + handler.handle(t); + } + if(set.size() >= accelDequeueThresh) + sleep(accelDequeueTime); + else + sleep(dequeueTime); + } + + } catch (Exception ex) { + Log.severe("Exception on rendering-thread", ex); + } + } + + private boolean sleep(int time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + return false; + } + return true; + } + + public void done(T t) { + synchronized (lock) { + if(pendingcnt > 0) pendingcnt--; + lock.notifyAll(); + } + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/ChatEvent.java b/dynmap-core/src/main/java/org/dynmap/ChatEvent.java new file mode 100644 index 00000000..99f0a830 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/ChatEvent.java @@ -0,0 +1,12 @@ +package org.dynmap; + +public class ChatEvent { + public String source; + public String name; + public String message; + public ChatEvent(String source, String name, String message) { + this.source = source; + this.name = name; + this.message = message; + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/Client.java b/dynmap-core/src/main/java/org/dynmap/Client.java new file mode 100644 index 00000000..e48f06bf --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/Client.java @@ -0,0 +1,275 @@ +package org.dynmap; + +import java.io.IOException; +import java.io.Writer; +import java.util.Random; + +import org.json.simple.JSONAware; +import org.json.simple.JSONStreamAware; +import org.owasp.html.PolicyFactory; +import org.owasp.html.Sanitizers; +import org.dynmap.common.DynmapChatColor; + +public class Client { + + public static class Update implements JSONAware, JSONStreamAware { + public long timestamp = System.currentTimeMillis(); + + @Override + public String toJSONString() { + return org.dynmap.web.Json.stringifyJson(this); + } + + @Override + public void writeJSONString(Writer w) throws IOException { + w.write(toJSONString()); + } + } + + public static class ChatMessage extends Update { + public String type = "chat"; + public String source; + public String playerName; // Note: this needs to be client-safe HTML text (can include tags, but only sanitized ones) + public String message; + public String account; + public String channel; + public ChatMessage(String source, String channel, String playerName, String message, String playeraccount) { + this.source = source; + if (ClientUpdateComponent.hideNames) + this.playerName = ""; + else if (ClientUpdateComponent.usePlayerColors) + this.playerName = Client.encodeColorInHTML(playerName); + else + this.playerName = Client.stripColor(playerName); + this.message = DynmapChatColor.stripColor(message); + this.account = playeraccount; + this.channel = channel; + } + @Override + public boolean equals(Object o) { + if(o instanceof ChatMessage) { + ChatMessage m = (ChatMessage)o; + return m.source.equals(source) && m.playerName.equals(playerName) && m.message.equals(message); + } + return false; + } + @Override + public int hashCode() { + return source.hashCode() ^ playerName.hashCode() ^ message.hashCode(); + } + } + + public static class PlayerJoinMessage extends Update { + public String type = "playerjoin"; + public String playerName; // Note: this needs to be client-safe HTML text (can include tags, but only sanitized ones) + public String account; + public PlayerJoinMessage(String playerName, String playeraccount) { + if (ClientUpdateComponent.hideNames) + this.playerName = ""; + else if (ClientUpdateComponent.usePlayerColors) + this.playerName = Client.encodeColorInHTML(playerName); + else + this.playerName = Client.stripColor(playerName); + this.account = playeraccount; + } + @Override + public boolean equals(Object o) { + if(o instanceof PlayerJoinMessage) { + PlayerJoinMessage m = (PlayerJoinMessage)o; + return m.playerName.equals(playerName); + } + return false; + } + @Override + public int hashCode() { + return account.hashCode(); + } + } + + public static class PlayerQuitMessage extends Update { + public String type = "playerquit"; + public String playerName; // Note: this needs to be client-safe HTML text (can include tags, but only sanitized ones) + public String account; + public PlayerQuitMessage(String playerName, String playeraccount) { + if (ClientUpdateComponent.hideNames) + this.playerName = ""; + else if (ClientUpdateComponent.usePlayerColors) + this.playerName = Client.encodeColorInHTML(playerName); + else + this.playerName = Client.stripColor(playerName); + this.account = playeraccount; + } + @Override + public boolean equals(Object o) { + if(o instanceof PlayerQuitMessage) { + PlayerQuitMessage m = (PlayerQuitMessage)o; + return m.playerName.equals(playerName); + } + return false; + } + @Override + public int hashCode() { + return account.hashCode(); + } + } + + public static class Tile extends Update { + public String type = "tile"; + public String name; + + public Tile(String name) { + this.name = name; + } + @Override + public boolean equals(Object o) { + if(o instanceof Tile) { + Tile m = (Tile)o; + return m.name.equals(name); + } + return false; + } + @Override + public int hashCode() { + return name.hashCode(); + } + } + + public static class DayNight extends Update { + public String type = "daynight"; + public boolean isday; + + public DayNight(boolean isday) { + this.isday = isday; + } + @Override + public boolean equals(Object o) { + if(o instanceof DayNight) { + return true; + } + return false; + } + @Override + public int hashCode() { + return 12345; + } + } + + public static class ComponentMessage extends Update { + public String type = "component"; + /* Each subclass must provide 'ctype' string for component 'type' */ + } + + // Strip color - assume we're returning safe html text + public static String stripColor(String s) { + s = DynmapChatColor.stripColor(s); /* Strip standard color encoding */ + /* Handle Essentials nickname encoding too */ + int idx = 0; + while((idx = s.indexOf('&', idx)) >= 0) { + char c = s.charAt(idx+1); /* Get next character */ + if(c == '&') { /* Another ampersand */ + s = s.substring(0, idx) + s.substring(idx+1); + } + else { + s = s.substring(0, idx) + s.substring(idx+2); + } + idx++; + } + // Apply sanitize policy before returning + return sanitizeHTML(s); + } + private static String[][] codes = { + { "0", "" }, + { "1", "" }, + { "2", "" }, + { "3", "" }, + { "4", "" }, + { "5", "" }, + { "6", "" }, + { "7", "" }, + { "8", "" }, + { "9", "" }, + { "a", "" }, + { "b", "" }, + { "c", "" }, + { "d", "" }, + { "e", "" }, + { "f", "" }, + { "l", "" }, + { "m", "" }, + { "n", "" }, + { "o", "" }, + { "r", "" } + }; + private static Random rnd = new Random(); + private static String rndchars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + // Replace color codes with corresponding "); + } + return sanitizeHTML(sb.toString()); + } + + private static PolicyFactory sanitizer = null; + public static String sanitizeHTML(String html) { + PolicyFactory s = sanitizer; + if (s == null) { + // Generous but safe html formatting allowances + s = Sanitizers.FORMATTING.and(Sanitizers.BLOCKS).and(Sanitizers.IMAGES).and(Sanitizers.LINKS).and(Sanitizers.STYLES); + sanitizer = s; + } + return sanitizer.sanitize(html); + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/ClientComponent.java b/dynmap-core/src/main/java/org/dynmap/ClientComponent.java new file mode 100644 index 00000000..f27cf284 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/ClientComponent.java @@ -0,0 +1,67 @@ +package org.dynmap; + +import static org.dynmap.JSONUtils.a; +import static org.dynmap.JSONUtils.s; + +import java.util.List; +import java.util.Map; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +public class ClientComponent extends Component { + private boolean disabled; + + public ClientComponent(final DynmapCore plugin, final ConfigurationNode configuration) { + super(plugin, configuration); + plugin.events.addListener("buildclientconfiguration", new Event.Listener() { + @Override + public void triggered(JSONObject root) { + if(!disabled) + buildClientConfiguration(root); + } + }); + } + + protected void disableComponent() { + disabled = true; + } + + protected void buildClientConfiguration(JSONObject root) { + JSONObject o = createClientConfiguration(); + a(root, "components", o); + } + + protected JSONObject createClientConfiguration() { + JSONObject o = convertMap(configuration); + o.remove("class"); + return o; + } + + protected static final JSONObject convertMap(Map m) { + JSONObject o = new JSONObject(); + for(Map.Entry entry : m.entrySet()) { + s(o, entry.getKey(), convert(entry.getValue())); + } + return o; + } + + @SuppressWarnings("unchecked") + protected static final JSONArray convertList(List l) { + JSONArray o = new JSONArray(); + for(Object entry : l) { + o.add(convert(entry)); + } + return o; + } + + @SuppressWarnings("unchecked") + protected static final Object convert(Object o) { + if (o instanceof Map) { + return convertMap((Map)o); + } else if (o instanceof List) { + return convertList((List)o); + } + return o; + } + +} diff --git a/dynmap-core/src/main/java/org/dynmap/ClientConfigurationComponent.java b/dynmap-core/src/main/java/org/dynmap/ClientConfigurationComponent.java new file mode 100644 index 00000000..26cb2f9f --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/ClientConfigurationComponent.java @@ -0,0 +1,76 @@ +package org.dynmap; + +import static org.dynmap.JSONUtils.a; +import static org.dynmap.JSONUtils.s; +import org.dynmap.Event.Listener; +import org.json.simple.JSONObject; + +public class ClientConfigurationComponent extends Component { + public ClientConfigurationComponent(final DynmapCore core, ConfigurationNode configuration) { + super(core, configuration); + core.events.addListener("buildclientconfiguration", new Listener() { + @Override + public void triggered(JSONObject t) { + ConfigurationNode c = core.configuration; + s(t, "confighash", core.getConfigHashcode()); + s(t, "updaterate", c.getFloat("updaterate", 1.0f)); + s(t, "showplayerfacesinmenu", c.getBoolean("showplayerfacesinmenu", true)); + s(t, "joinmessage", c.getString("joinmessage", "%playername% joined")); + s(t, "quitmessage", c.getString("quitmessage", "%playername% quit")); + s(t, "spammessage", c.getString("spammessage", "You may only chat once every %interval% seconds.")); + s(t, "webprefix", unescapeString(c.getString("webprefix", "[WEB] "))); + s(t, "defaultzoom", c.getInteger("defaultzoom", 0)); + s(t, "sidebaropened", c.getString("sidebaropened", "false")); + s(t, "dynmapversion", core.getDynmapPluginVersion()); + s(t, "coreversion", core.getDynmapCoreVersion()); + s(t, "cyrillic", c.getBoolean("cyrillic-support", false)); + s(t, "showlayercontrol", c.getString("showlayercontrol", "true")); + s(t, "grayplayerswhenhidden", c.getBoolean("grayplayerswhenhidden", true)); + s(t, "login-enabled", core.isLoginSupportEnabled()); + String sn = core.getServer().getServerName(); + if(sn.equals("Unknown Server")) + sn = "Minecraft Dynamic Map"; + s(t, "title", c.getString("webpage-title", sn)); + s(t, "msg-maptypes", c.getString("msg/maptypes", "Map Types")); + s(t, "msg-players", c.getString("msg/players", "Players")); + s(t, "msg-chatrequireslogin", c.getString("msg/chatrequireslogin", "Chat Requires Login")); + s(t, "msg-chatnotallowed", c.getString("msg/chatnotallowed", "You are not permitted to send chat messages")); + s(t, "msg-hiddennamejoin", c.getString("msg/hiddennamejoin", "Player joined")); + s(t, "msg-hiddennamequit", c.getString("msg/hiddennamequit", "Player quit")); + s(t, "maxcount", core.getMaxPlayers()); + + DynmapWorld defaultWorld = null; + String defmap = null; + a(t, "worlds", null); + for(DynmapWorld world : core.mapManager.getWorlds()) { + if (world.maps.size() == 0) continue; + if (defaultWorld == null) defaultWorld = world; + JSONObject wo = new JSONObject(); + s(wo, "name", world.getName()); + s(wo, "title", world.getTitle()); + s(wo, "protected", world.isProtected()); + DynmapLocation center = world.getCenterLocation(); + s(wo, "center/x", center.x); + s(wo, "center/y", center.y); + s(wo, "center/z", center.z); + s(wo, "extrazoomout", world.getExtraZoomOutLevels()); + s(wo, "sealevel", world.sealevel); + s(wo, "worldheight", world.worldheight); + a(t, "worlds", wo); + + for(MapType mt : world.maps) { + mt.buildClientConfiguration(wo, world); + if(defmap == null) defmap = mt.getName(); + } + } + s(t, "defaultworld", c.getString("defaultworld", defaultWorld == null ? "world" : defaultWorld.getName())); + s(t, "defaultmap", c.getString("defaultmap", defmap == null ? "surface" : defmap)); + if(c.getString("followmap", null) != null) + s(t, "followmap", c.getString("followmap")); + if(c.getInteger("followzoom",-1) >= 0) + s(t, "followzoom", c.getInteger("followzoom", 0)); + } + }); + } + +} diff --git a/dynmap-core/src/main/java/org/dynmap/ClientUpdateComponent.java b/dynmap-core/src/main/java/org/dynmap/ClientUpdateComponent.java new file mode 100644 index 00000000..2ce32343 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/ClientUpdateComponent.java @@ -0,0 +1,176 @@ +package org.dynmap; + +import static org.dynmap.JSONUtils.a; +import static org.dynmap.JSONUtils.s; + +import java.util.List; +import org.dynmap.common.DynmapPlayer; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +public class ClientUpdateComponent extends Component { + private int hideifshadow; + private int hideifunder; + private boolean hideifsneaking; + private boolean hideifinvisiblepotion; + private boolean is_protected; + public static boolean usePlayerColors; + public static boolean hideNames; + + public ClientUpdateComponent(final DynmapCore core, ConfigurationNode configuration) { + super(core, configuration); + + hideNames = configuration.getBoolean("hidenames", false); + hideifshadow = configuration.getInteger("hideifshadow", 15); + hideifunder = configuration.getInteger("hideifundercover", 15); + hideifsneaking = configuration.getBoolean("hideifsneaking", false); + hideifinvisiblepotion = configuration.getBoolean("hide-if-invisiblity-potion", true); + is_protected = configuration.getBoolean("protected-player-info", false); + usePlayerColors = configuration.getBoolean("use-name-colors", false); + if(is_protected) + core.player_info_protected = true; + + core.events.addListener("buildclientupdate", new Event.Listener() { + @Override + public void triggered(ClientUpdateEvent e) { + buildClientUpdate(e); + } + }); + } + + protected void buildClientUpdate(ClientUpdateEvent e) { + DynmapWorld world = e.world; + JSONObject u = e.update; + long since = e.timestamp; + String worldName = world.getName(); + boolean see_all = true; + + if(is_protected && (!e.include_all_users)) { + if(e.user != null) + see_all = core.getServer().checkPlayerPermission(e.user, "playermarkers.seeall"); + else + see_all = false; + } + if((e.include_all_users) && is_protected) { /* If JSON request AND protected, leave mark for script */ + s(u, "protected", true); + } + + s(u, "confighash", core.getConfigHashcode()); + + s(u, "servertime", world.getTime() % 24000); + s(u, "hasStorm", world.hasStorm()); + s(u, "isThundering", world.isThundering()); + + s(u, "players", new JSONArray()); + List players = core.playerList.getVisiblePlayers(); + for(DynmapPlayer p : players) { + boolean hide = false; + DynmapLocation pl = p.getLocation(); + DynmapWorld pw = core.getWorld(pl.world); + if(pw == null) { + hide = true; + } + JSONObject jp = new JSONObject(); + + s(jp, "type", "player"); + if (hideNames) + s(jp, "name", ""); + else if (usePlayerColors) + s(jp, "name", Client.encodeColorInHTML(p.getDisplayName())); + else + s(jp, "name", Client.stripColor(p.getDisplayName())); + s(jp, "account", p.getName()); + if((!hide) && (hideifshadow < 15)) { + if(pw.getLightLevel((int)pl.x, (int)pl.y, (int)pl.z) <= hideifshadow) { + hide = true; + } + } + if((!hide) && (hideifunder < 15)) { + if(pw.canGetSkyLightLevel()) { /* If we can get real sky level */ + if(pw.getSkyLightLevel((int)pl.x, (int)pl.y, (int)pl.z) <= hideifunder) { + hide = true; + } + } + else if(pw.isNether() == false) { /* Not nether */ + if(pw.getHighestBlockYAt((int)pl.x, (int)pl.z) > pl.y) { + hide = true; + } + } + } + if((!hide) && hideifsneaking && p.isSneaking()) { + hide = true; + } + if((!hide) && is_protected && (!see_all)) { + if(e.user != null) { + hide = !core.testIfPlayerVisibleToPlayer(e.user, p.getName()); + } + else { + hide = true; + } + } + if((!hide) && hideifinvisiblepotion && p.isInvisible()) { + hide = true; + } + + /* Don't leak player location for world not visible on maps, or if sendposition disbaled */ + DynmapWorld pworld = MapManager.mapman.worldsLookup.get(pl.world); + /* Fix typo on 'sendpositon' to 'sendposition', keep bad one in case someone used it */ + if(configuration.getBoolean("sendposition", true) && configuration.getBoolean("sendpositon", true) && + (pworld != null) && pworld.sendposition && (!hide)) { + s(jp, "world", pl.world); + s(jp, "x", pl.x); + s(jp, "y", pl.y); + s(jp, "z", pl.z); + } + else { + s(jp, "world", "-some-other-bogus-world-"); + s(jp, "x", 0.0); + s(jp, "y", 64.0); + s(jp, "z", 0.0); + } + /* Only send health if enabled AND we're on visible world */ + if (configuration.getBoolean("sendhealth", false) && (pworld != null) && pworld.sendhealth && (!hide)) { + s(jp, "health", p.getHealth()); + s(jp, "armor", p.getArmorPoints()); + } + else { + s(jp, "health", 0); + s(jp, "armor", 0); + } + s(jp, "sort", p.getSortWeight()); + a(u, "players", jp); + } + List hidden = core.playerList.getHiddenPlayers(); + if(configuration.getBoolean("includehiddenplayers", false)) { + for(DynmapPlayer p : hidden) { + JSONObject jp = new JSONObject(); + s(jp, "type", "player"); + if (hideNames) + s(jp, "name", ""); + else if (usePlayerColors) + s(jp, "name", Client.encodeColorInHTML(p.getDisplayName())); + else + s(jp, "name", Client.stripColor(p.getDisplayName())); + s(jp, "account", p.getName()); + s(jp, "world", "-hidden-player-"); + s(jp, "x", 0.0); + s(jp, "y", 64.0); + s(jp, "z", 0.0); + s(jp, "health", 0); + s(jp, "armor", 0); + s(jp, "sort", p.getSortWeight()); + a(u, "players", jp); + } + s(u, "currentcount", core.getCurrentPlayers()); + } + else { + s(u, "currentcount", core.getCurrentPlayers() - hidden.size()); + } + + s(u, "updates", new JSONArray()); + for(Object update : core.mapManager.getWorldUpdates(worldName, since)) { + a(u, "updates", (Client.Update)update); + } + } + +} diff --git a/dynmap-core/src/main/java/org/dynmap/ClientUpdateEvent.java b/dynmap-core/src/main/java/org/dynmap/ClientUpdateEvent.java new file mode 100644 index 00000000..0fab4b0e --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/ClientUpdateEvent.java @@ -0,0 +1,17 @@ +package org.dynmap; + +import org.json.simple.JSONObject; + +public class ClientUpdateEvent { + public long timestamp; + public DynmapWorld world; + public JSONObject update; + public String user; + public boolean include_all_users; + + public ClientUpdateEvent(long timestamp, DynmapWorld world, JSONObject update) { + this.timestamp = timestamp; + this.world = world; + this.update = update; + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/Color.java b/dynmap-core/src/main/java/org/dynmap/Color.java new file mode 100644 index 00000000..1aba01e1 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/Color.java @@ -0,0 +1,89 @@ +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 { + /* ARGB 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 >> 16) & 0xFF; + } + public final int getGreen() { + return (val >> 8) & 0xFF; + } + public final int getBlue() { + return val & 0xFF; + } + public final int getAlpha() { + return ((val >> 24) & 0xFF); + } + public final boolean isTransparent() { + return ((val & 0xFF000000) == 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 = ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF); + } + public final int getARGB() { + return val; + } + public final void setARGB(int c) { + val = c; + } + public final int getComponent(int idx) { + return 0xFF & (val >> ((3-idx)*8)); + } + public final void setAlpha(int v) { + val = (val & 0x00FFFFFF) | (v << 24); + } + /** + * Scale each color component, based on the corresponding component + * @param c - color to blend + */ + public final void blendColor(Color c) { + blendColor(c.val); + } + /** + * Scale each color component, based on the corresponding component + * @param argb - ARGB to blend + */ + public final void blendColor(int argb) { + int nval = (((((val >> 24) & 0xFF) * ((argb >> 24) & 0xFF)) / 255) << 24); + nval = nval | (((((val >> 16) & 0xFF) * ((argb >> 16) & 0xFF)) / 255) << 16); + nval = nval | (((((val >> 8) & 0xFF) * ((argb >> 8) & 0xFF)) / 255) << 8); + nval = nval | (((val & 0xFF) * (argb & 0xFF)) / 255); + val = nval; + } + /** + * Scale each color component, based on the corresponding component + * @param argb0 - first color + * @param argb1 second color + * @return blended color + */ + public static final int blendColor(int argb0, int argb1) { + int nval = (((((argb0 >> 24) & 0xFF) * ((argb1 >> 24) & 0xFF)) / 255) << 24); + nval = nval | (((((argb0 >> 16) & 0xFF) * ((argb1 >> 16) & 0xFF)) / 255) << 16); + nval = nval | (((((argb0 >> 8) & 0xFF) * ((argb1 >> 8) & 0xFF)) / 255) << 8); + nval = nval | (((argb0 & 0xFF) * (argb1 & 0xFF)) / 255); + return nval; + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/ColorScheme.java b/dynmap-core/src/main/java/org/dynmap/ColorScheme.java new file mode 100644 index 00000000..2362b9a4 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/ColorScheme.java @@ -0,0 +1,286 @@ +package org.dynmap; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Scanner; + +import org.dynmap.common.BiomeMap; +import org.dynmap.debug.Debug; + +public class ColorScheme { + private static final HashMap cache = new HashMap(); + + public String name; + /* Switch to arrays - faster than map */ + public Color[][] colors; /* [blk-type][step] */ + public Color[][][] datacolors; /* [bkt-type][blk-dat][step] */ + public final Color[][] biomecolors; /* [Biome.ordinal][step] */ + public final Color[][] raincolors; /* [rain * 63][step] */ + public final Color[][] tempcolors; /* [temp * 63][step] */ + + public ColorScheme(String name, Color[][] colors, Color[][][] datacolors, Color[][] biomecolors, Color[][] raincolors, Color[][] tempcolors) { + this.name = name; + this.colors = colors; + this.datacolors = datacolors; + this.biomecolors = biomecolors; + this.raincolors = raincolors; + this.tempcolors = tempcolors; + //TODO: see if we can fix this for IDs vs names... +// for(int i = 0; i < colors.length; i++) { +// int id = MapManager.mapman.getBlockAlias(i); +// if(id != i) { +// this.colors[i] = this.colors[id]; +// this.datacolors[i] = this.datacolors[id]; +// } +// } + } + + private static File getColorSchemeDirectory(DynmapCore core) { + return new File(core.getDataFolder(), "colorschemes"); + } + + public static ColorScheme getScheme(DynmapCore core, String name) { + if (name == null) + name = "default"; + ColorScheme scheme = cache.get(name); + if (scheme == null) { + scheme = loadScheme(core, name); + cache.put(name, scheme); + } + return scheme; + } + + public static ColorScheme loadScheme(DynmapCore core, String name) { + File colorSchemeFile = new File(getColorSchemeDirectory(core), name + ".txt"); + Color[][] colors = new Color[4096][]; + Color[][][] datacolors = new Color[4096][][]; + Color[][] biomecolors = new Color[BiomeMap.values().length][]; + Color[][] raincolors = new Color[64][]; + Color[][] tempcolors = new Color[64][]; + + /* Default the biome color */ + for(int i = 0; i < biomecolors.length; i++) { + Color[] c = new Color[5]; + int red = 0x80 | (0x40 * ((i >> 0) & 1)) | (0x20 * ((i >> 3) & 1)) | (0x10 * ((i >> 6) & 1)); + int green = 0x80 | (0x40 * ((i >> 1) & 1)) | (0x20 * ((i >> 4) & 1)) | (0x10 * ((i >> 7) & 1)); + int blue = 0x80 | (0x40 * ((i >> 2) & 1)) | (0x20 * ((i >> 5) & 1)); + c[0] = new Color(red, green, blue); + c[3] = new Color(red*4/5, green*4/5, blue*4/5); + c[1] = new Color(red/2, green/2, blue/2); + c[2] = new Color(red*2/5, green*2/5, blue*2/5); + c[4] = new Color((c[1].getRed()+c[3].getRed())/2, (c[1].getGreen()+c[3].getGreen())/2, (c[1].getBlue()+c[3].getBlue())/2, (c[1].getAlpha()+c[3].getAlpha())/2); + + biomecolors[i] = c; + } + + InputStream stream; + try { + Debug.debug("Loading colors from '" + colorSchemeFile + "'..."); + stream = new FileInputStream(colorSchemeFile); + + Scanner scanner = new Scanner(stream); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.startsWith("#") || line.equals("")) { + continue; + } + /* Make parser less pedantic - tabs or spaces should be fine */ + String[] split = line.split("[\t ]"); + int cnt = 0; + for(String s: split) { if(s.length() > 0) cnt++; } + String[] nsplit = new String[cnt]; + cnt = 0; + for(String s: split) { if(s.length() > 0) { nsplit[cnt] = s; cnt++; } } + split = nsplit; + if (split.length < 17) { + continue; + } + Integer id; + Integer dat = null; + boolean isbiome = false; + boolean istemp = false; + boolean israin = false; + int idx = split[0].indexOf(':'); + if(idx > 0) { /* ID:data - data color */ + id = new Integer(split[0].substring(0, idx)); + dat = new Integer(split[0].substring(idx+1)); + } + else if(split[0].charAt(0) == '[') { /* Biome color data */ + String bio = split[0].substring(1); + idx = bio.indexOf(']'); + if(idx >= 0) bio = bio.substring(0, idx); + isbiome = true; + id = -1; + BiomeMap[] bm = BiomeMap.values(); + for(int i = 0; i < bm.length; i++) { + if(bm[i].toString().equalsIgnoreCase(bio)) { + id = i; + break; + } + else if(bio.equalsIgnoreCase("BIOME_" + i)) { + id = i; + break; + } + } + if(id < 0) { /* Not biome - check for rain or temp */ + if(bio.startsWith("RAINFALL-")) { + try { + double v = Double.parseDouble(bio.substring(9)); + if((v >= 0) && (v <= 1.00)) { + id = (int)(v * 63.0); + israin = true; + } + } catch (NumberFormatException nfx) { + } + } + else if(bio.startsWith("TEMPERATURE-")) { + try { + double v = Double.parseDouble(bio.substring(12)); + if((v >= 0) && (v <= 1.00)) { + id = (int)(v * 63.0); + istemp = true; + } + } catch (NumberFormatException nfx) { + } + } + } + } + else { + id = new Integer(split[0]); + } + if((!isbiome) && (id >= colors.length)) { + Color[][] newcolors = new Color[id+1][]; + System.arraycopy(colors, 0, newcolors, 0, colors.length); + colors = newcolors; + Color[][][] newdatacolors = new Color[id+1][][]; + System.arraycopy(datacolors, 0, newdatacolors, 0, datacolors.length); + datacolors = newdatacolors; + } + + Color[] c = new Color[5]; + + /* store colors by raycast sequence number */ + c[0] = new Color(Integer.parseInt(split[1]), Integer.parseInt(split[2]), Integer.parseInt(split[3]), Integer.parseInt(split[4])); + c[3] = new Color(Integer.parseInt(split[5]), Integer.parseInt(split[6]), Integer.parseInt(split[7]), Integer.parseInt(split[8])); + c[1] = new Color(Integer.parseInt(split[9]), Integer.parseInt(split[10]), Integer.parseInt(split[11]), Integer.parseInt(split[12])); + c[2] = new Color(Integer.parseInt(split[13]), Integer.parseInt(split[14]), Integer.parseInt(split[15]), Integer.parseInt(split[16])); + /* Blended color - for 'smooth' option on flat map */ + c[4] = new Color((c[1].getRed()+c[3].getRed())/2, (c[1].getGreen()+c[3].getGreen())/2, (c[1].getBlue()+c[3].getBlue())/2, (c[1].getAlpha()+c[3].getAlpha())/2); + + if(isbiome) { + if(istemp) { + tempcolors[id] = c; + } + else if(israin) { + raincolors[id] = c; + } + else if((id >= 0) && (id < biomecolors.length)) + biomecolors[id] = c; + } + else if(dat != null) { + Color[][] dcolor = datacolors[id]; /* Existing list? */ + if(dcolor == null) { + dcolor = new Color[16][]; /* Make 16 index long list */ + datacolors[id] = dcolor; + } + if((dat >= 0) && (dat < 16)) { /* Add color to list */ + dcolor[dat] = c; + } + if(dat == 0) { /* Index zero is base color too */ + colors[id] = c; + } + } + else { + colors[id] = c; + } + } + scanner.close(); + /* Last, push base color into any open slots in data colors list */ + for(int k = 0; k < datacolors.length; k++) { + Color[][] dc = datacolors[k]; /* see if data colors too */ + if(dc != null) { + Color[] c = colors[k]; + for(int i = 0; i < 16; i++) { + if(dc[i] == null) + dc[i] = c; + } + } + } + /* And interpolate any missing rain and temperature colors */ + interpolateColorTable(tempcolors); + interpolateColorTable(raincolors); + } catch (RuntimeException e) { + Log.severe("Could not load colors '" + name + "' ('" + colorSchemeFile + "').", e); + return null; + } catch (FileNotFoundException e) { + Log.severe("Could not load colors '" + name + "' ('" + colorSchemeFile + "'): File not found.", e); + } + return new ColorScheme(name, colors, datacolors, biomecolors, raincolors, tempcolors); + } + + public static void interpolateColorTable(Color[][] c) { + int idx = -1; + for(int k = 0; k < c.length; k++) { + if(c[k] == null) { /* Missing? */ + if((idx >= 0) && (k == (c.length-1))) { /* We're last - so fill forward from last color */ + for(int kk = idx+1; kk <= k; kk++) { + c[kk] = c[idx]; + } + } + /* Skip - will backfill when we find next color */ + } + else if(idx == -1) { /* No previous color, just backfill this color */ + for(int kk = 0; kk < k; kk++) { + c[kk] = c[k]; + } + idx = k; /* This is now last defined color */ + } + else { /* Else, interpolate between last idx and this one */ + int cnt = c[k].length; + for(int kk = idx+1; kk < k; kk++) { + double interp = (double)(kk-idx)/(double)(k-idx); + Color[] cc = new Color[cnt]; + for(int jj = 0; jj < cnt; jj++) { + cc[jj] = new Color( + (int)((1.0-interp)*c[idx][jj].getRed() + interp*c[k][jj].getRed()), + (int)((1.0-interp)*c[idx][jj].getGreen() + interp*c[k][jj].getGreen()), + (int)((1.0-interp)*c[idx][jj].getBlue() + interp*c[k][jj].getBlue()), + (int)((1.0-interp)*c[idx][jj].getAlpha() + interp*c[k][jj].getAlpha())); + } + c[kk] = cc; + } + idx = k; + } + } + } + public Color[] getRainColor(double rain) { + int idx = (int)(rain * 63.0); + if((idx >= 0) && (idx < raincolors.length)) + return raincolors[idx]; + else + return null; + } + public Color[] getTempColor(double temp) { + int idx = (int)(temp * 63.0); + if((idx >= 0) && (idx < tempcolors.length)) + return tempcolors[idx]; + else + return null; + } + public void resizeColorArray(int idx) { + if(idx >= colors.length){ + Color[][] newcolors = new Color[idx+1][]; + System.arraycopy(colors, 0, newcolors, 0, colors.length); + colors = newcolors; + Color[][][] newdatacolors = new Color[idx+1][][]; + System.arraycopy(datacolors, 0, newdatacolors, 0, datacolors.length); + datacolors = newdatacolors; + } + } + public static void reset() { + cache.clear(); + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/Component.java b/dynmap-core/src/main/java/org/dynmap/Component.java new file mode 100644 index 00000000..0d6a47f6 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/Component.java @@ -0,0 +1,21 @@ +package org.dynmap; + +public abstract class Component { + protected DynmapCore core; + protected ConfigurationNode configuration; + public Component(DynmapCore core, ConfigurationNode configuration) { + this.core = core; + this.configuration = configuration; + } + + public void dispose() { + } + + /* Substitute proper values for escape sequences */ + public static String unescapeString(String v) { + /* Replace color code &color; */ + v = v.replace("&color;", "\u00A7"); + + return v; + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/ComponentManager.java b/dynmap-core/src/main/java/org/dynmap/ComponentManager.java new file mode 100644 index 00000000..ce737940 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/ComponentManager.java @@ -0,0 +1,47 @@ +package org.dynmap; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ComponentManager { + public Set components = new HashSet(); + public Map> componentLookup = new HashMap>(); + + public void add(Component c) { + if (components.add(c)) { + String key = c.getClass().toString(); + List clist = componentLookup.get(key); + if (clist == null) { + clist = new ArrayList(); + componentLookup.put(key, clist); + } + clist.add(c); + } + } + + public void remove(Component c) { + if (components.remove(c)) { + String key = c.getClass().toString(); + List clist = componentLookup.get(key); + if (clist != null) { + clist.remove(c); + } + } + } + + public void clear() { + componentLookup.clear(); + components.clear(); + } + + public Iterable getComponents(Class c) { + List list = componentLookup.get(c.toString()); + if (list == null) + return new ArrayList(); + return list; + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/ConfigurationNode.java b/dynmap-core/src/main/java/org/dynmap/ConfigurationNode.java new file mode 100644 index 00000000..350e41fa --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/ConfigurationNode.java @@ -0,0 +1,447 @@ +package org.dynmap; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.CollectionNode; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.reader.UnicodeReader; +import org.yaml.snakeyaml.representer.Represent; +import org.yaml.snakeyaml.representer.Representer; + +public class ConfigurationNode implements Map { + public Map entries; + private File f; + private Yaml yaml; + + public ConfigurationNode() { + entries = new LinkedHashMap(); + } + + private void initparse() { + if(yaml == null) { + DumperOptions options = new DumperOptions(); + + options.setIndent(4); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + + yaml = new Yaml(new SafeConstructor(), new EmptyNullRepresenter(), options); + } + } + + public ConfigurationNode(File f) { + this.f = f; + entries = new LinkedHashMap(); + } + + public ConfigurationNode(Map map) { + if (map == null) { + throw new IllegalArgumentException(); + } + entries = map; + } + + public ConfigurationNode(InputStream in) { + load(in); + } + + @SuppressWarnings("unchecked") + public boolean load(InputStream in) { + initparse(); + + Object o = yaml.load(new UnicodeReader(in)); + if((o != null) && (o instanceof Map)) + entries = (Map)o; + return (entries != null); + } + + @SuppressWarnings("unchecked") + public boolean load() { + initparse(); + + FileInputStream fis = null; + try { + fis = new FileInputStream(f); + Object o = yaml.load(new UnicodeReader(fis)); + if((o != null) && (o instanceof Map)) + entries = (Map)o; + fis.close(); + } + catch (YAMLException e) { + Log.severe("Error parsing " + f.getPath() + ". Use http://yamllint.com to debug the YAML syntax." ); + throw e; + } catch(IOException iox) { + Log.severe("Error reading " + f.getPath()); + return false; + } finally { + if(fis != null) { + try { fis.close(); } catch (IOException x) {} + } + } + return (entries != null); + } + + public boolean save() { + return save(f); + } + + public boolean save(File file) { + initparse(); + + FileOutputStream stream = null; + + File parent = file.getParentFile(); + + if (parent != null) { + parent.mkdirs(); + } + + try { + stream = new FileOutputStream(file); + OutputStreamWriter writer = new OutputStreamWriter(stream, "UTF-8"); + yaml.dump(entries, writer); + return true; + } catch (IOException e) { + } finally { + try { + if (stream != null) { + stream.close(); + } + } catch (IOException e) {} + } + return false; + } + + @SuppressWarnings("unchecked") + public Object getObject(String path) { + if (path.isEmpty()) + return entries; + int separator = path.indexOf('/'); + if (separator < 0) + return get(path); + String localKey = path.substring(0, separator); + Object subvalue = get(localKey); + if (subvalue == null) + return null; + if (!(subvalue instanceof Map)) + return null; + Map submap; + try { + submap = (Map)subvalue; + } catch (ClassCastException e) { + return null; + } + + String subpath = path.substring(separator + 1); + return new ConfigurationNode(submap).getObject(subpath); + + } + + public Object getObject(String path, Object def) { + Object o = getObject(path); + if (o == null) + return def; + return o; + } + + @SuppressWarnings("unchecked") + public T getGeneric(String path, T def) { + Object o = getObject(path, def); + try { + return (T)o; + } catch(ClassCastException e) { + return def; + } + } + + public int getInteger(String path, int def) { + return Integer.parseInt(getObject(path, def).toString()); + } + + public double getLong(String path, long def) { + return Long.parseLong(getObject(path, def).toString()); + } + + public float getFloat(String path, float def) { + return Float.parseFloat(getObject(path, def).toString()); + } + + public double getDouble(String path, double def) { + return Double.parseDouble(getObject(path, def).toString()); + } + + public boolean getBoolean(String path, boolean def) { + return Boolean.parseBoolean(getObject(path, def).toString()); + } + + public String getString(String path) { + return getString(path, null); + } + + public List getStrings(String path, List def) { + Object o = getObject(path); + if (!(o instanceof List)) { + return def; + } + ArrayList strings = new ArrayList(); + for(Object i : (List)o) { + strings.add(i.toString()); + } + return strings; + } + + public String getString(String path, String def) { + Object o = getObject(path, def); + if (o == null) + return null; + return o.toString(); + } + + @SuppressWarnings("unchecked") + public List getList(String path) { + try { + List list = (List)getObject(path, null); + return list; + } catch (ClassCastException e) { + try { + T o = (T)getObject(path, null); + if (o == null) { + return new ArrayList(); + } + ArrayList al = new ArrayList(); + al.add(o); + return al; + } catch (ClassCastException e2) { + return new ArrayList(); + } + } + } + + public List> getMapList(String path) { + return getList(path); + } + + public ConfigurationNode getNode(String path) { + Map v = null; + v = getGeneric(path, v); + if (v == null) + return null; + return new ConfigurationNode(v); + } + + @SuppressWarnings("unchecked") + public List getNodes(String path) { + List o = getList(path); + + if(o == null) + return new ArrayList(); + + ArrayList nodes = new ArrayList(); + for(Object i : (List)o) { + if (i instanceof Map) { + Map map; + try { + map = (Map)i; + } catch(ClassCastException e) { + continue; + } + nodes.add(new ConfigurationNode(map)); + } + } + return nodes; + } + + public void extend(Map other) { + if (other != null) + extendMap(this, other); + } + + private final static Object copyValue(Object v) { + if(v instanceof Map) { + @SuppressWarnings("unchecked") + Map mv = (Map)v; + LinkedHashMap newv = new LinkedHashMap(); + for(Map.Entry me : mv.entrySet()) { + newv.put(me.getKey(), copyValue(me.getValue())); + } + return newv; + } + else if(v instanceof List) { + @SuppressWarnings("unchecked") + List lv = (List)v; + ArrayList newv = new ArrayList(); + for(int i = 0; i < lv.size(); i++) { + newv.add(copyValue(lv.get(i))); + } + return newv; + } + else { + return v; + } + } + + private final static void extendMap(Map left, Map right) { + ConfigurationNode original = new ConfigurationNode(left); + for(Map.Entry entry : right.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + original.put(key, copyValue(value)); + } + } + + public T createInstance(Class[] constructorParameters, Object[] constructorArguments) { + String typeName = getString("class"); + try { + Class mapTypeClass = Class.forName(typeName); + + Class[] constructorParameterWithConfiguration = new Class[constructorParameters.length+1]; + for(int i = 0; i < constructorParameters.length; i++) { constructorParameterWithConfiguration[i] = constructorParameters[i]; } + constructorParameterWithConfiguration[constructorParameterWithConfiguration.length-1] = ConfigurationNode.class; + + Object[] constructorArgumentsWithConfiguration = new Object[constructorArguments.length+1]; + for(int i = 0; i < constructorArguments.length; i++) { constructorArgumentsWithConfiguration[i] = constructorArguments[i]; } + constructorArgumentsWithConfiguration[constructorArgumentsWithConfiguration.length-1] = this; + Constructor constructor = mapTypeClass.getConstructor(constructorParameterWithConfiguration); + @SuppressWarnings("unchecked") + T t = (T)constructor.newInstance(constructorArgumentsWithConfiguration); + return t; + } catch (Exception e) { + // TODO: Remove reference to MapManager. + Log.severe("Error loading maptype", e); + e.printStackTrace(); + } + return null; + } + + public List createInstances(String path, Class[] constructorParameters, Object[] constructorArguments) { + List nodes = getNodes(path); + List instances = new ArrayList(); + for(ConfigurationNode node : nodes) { + T instance = node.createInstance(constructorParameters, constructorArguments); + if (instance != null) { + instances.add(instance); + } + } + return instances; + } + + @Override + public int size() { + return entries.size(); + } + + @Override + public boolean isEmpty() { + return entries.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return entries.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return entries.containsValue(value); + } + + @Override + public Object get(Object key) { + return entries.get(key); + } + + @Override + public Object put(String key, Object value) { + return entries.put(key, value); + } + + @Override + public Object remove(Object key) { + return entries.remove(key); + } + + @Override + public void putAll(Map m) { + entries.putAll(m); + } + + @Override + public void clear() { + entries.clear(); + } + + @Override + public Set keySet() { + return entries.keySet(); + } + + @Override + public Collection values() { + return entries.values(); + } + + @Override + public Set> entrySet() { + return entries.entrySet(); + } + + private class EmptyNullRepresenter extends Representer { + + public EmptyNullRepresenter() { + super(); + this.nullRepresenter = new EmptyRepresentNull(); + } + + protected class EmptyRepresentNull implements Represent { + public Node representData(Object data) { + return representScalar(Tag.NULL, ""); // Changed "null" to "" so as to avoid writing nulls + } + } + + // Code borrowed from snakeyaml (http://code.google.com/p/snakeyaml/source/browse/src/test/java/org/yaml/snakeyaml/issues/issue60/SkipBeanTest.java) + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) { + NodeTuple tuple = super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + Node valueNode = tuple.getValueNode(); + if (valueNode instanceof CollectionNode) { + // Removed null check + if (Tag.SEQ.equals(valueNode.getTag())) { + SequenceNode seq = (SequenceNode) valueNode; + if (seq.getValue().isEmpty()) { + return null; // skip empty lists + } + } + if (Tag.MAP.equals(valueNode.getTag())) { + MappingNode seq = (MappingNode) valueNode; + if (seq.getValue().isEmpty()) { + return null; // skip empty maps + } + } + } + return tuple; + } + // End of borrowed code + } + +} diff --git a/dynmap-core/src/main/java/org/dynmap/DynmapChunk.java b/dynmap-core/src/main/java/org/dynmap/DynmapChunk.java new file mode 100644 index 00000000..62e60148 --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/DynmapChunk.java @@ -0,0 +1,22 @@ +package org.dynmap; + +public class DynmapChunk { + public int x, z; + + public DynmapChunk(int x, int z) { + this.x = x; + this.z = z; + } + @Override + public boolean equals(Object o) { + if(o instanceof DynmapChunk) { + DynmapChunk dc = (DynmapChunk)o; + return (dc.x == this.x) && (dc.z == this.z); + } + return false; + } + @Override + public int hashCode() { + return x ^ (z << 5); + } +} diff --git a/dynmap-core/src/main/java/org/dynmap/DynmapCore.java b/dynmap-core/src/main/java/org/dynmap/DynmapCore.java new file mode 100644 index 00000000..dd127c6f --- /dev/null +++ b/dynmap-core/src/main/java/org/dynmap/DynmapCore.java @@ -0,0 +1,2425 @@ +package org.dynmap; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Constructor; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.dynmap.blockstate.BlockStateManager; +import org.dynmap.common.DynmapCommandSender; +import org.dynmap.common.DynmapListenerManager; +import org.dynmap.common.DynmapListenerManager.EventType; +import org.dynmap.common.DynmapPlayer; +import org.dynmap.common.DynmapServerInterface; +import org.dynmap.debug.Debug; +import org.dynmap.debug.Debugger; +import org.dynmap.exporter.DynmapExpCommands; +import org.dynmap.hdmap.HDBlockModels; +import org.dynmap.hdmap.HDBlockStateTextureMap; +import org.dynmap.hdmap.HDMapManager; +import org.dynmap.hdmap.TexturePack; +import org.dynmap.markers.MarkerAPI; +import org.dynmap.markers.impl.MarkerAPIImpl; +import org.dynmap.modsupport.ModSupportImpl; +import org.dynmap.renderer.DynmapBlockState; +import org.dynmap.servlet.FileResourceHandler; +import org.dynmap.servlet.JettyNullLogger; +import org.dynmap.servlet.LoginServlet; +import org.dynmap.servlet.MapStorageResourceHandler; +import org.dynmap.storage.MapStorage; +import org.dynmap.storage.filetree.FileTreeMapStorage; +import org.dynmap.storage.mysql.MySQLMapStorage; +import org.dynmap.storage.mariadb.MariaDBMapStorage; +import org.dynmap.storage.sqllte.SQLiteMapStorage; +import org.dynmap.utils.BlockStep; +import org.dynmap.utils.ImageIOManager; +import org.dynmap.web.BanIPFilter; +import org.dynmap.web.CustomHeaderFilter; +import org.dynmap.web.FilterHandler; +import org.dynmap.web.HandlerRouter; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerList; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.server.session.HashSessionIdManager; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.resource.FileResource; +import org.eclipse.jetty.util.thread.ExecutorThreadPool; +import org.yaml.snakeyaml.Yaml; + +import javax.servlet.*; +import javax.servlet.http.HttpServlet; + +public class DynmapCore implements DynmapCommonAPI { + // Current architectural limit for Minecraft block IDs + public static final int BLOCKTABLELEN = 4096; + /** + * Callbacks for core initialization - subclassed by platform plugins + */ + public static abstract class EnableCoreCallbacks { + /** + * Called during enableCore to report that confniguration.txt is loaded + */ + public abstract void configurationLoaded(); + } + private File jarfile; + private DynmapServerInterface server; + private String version; + private String platform = null; + private String platformVersion = null; + private Server webServer = null; + private String webhostname = null; + private int webport = 0; + private HandlerRouter router = null; + public MapManager mapManager = null; + public PlayerList playerList; + public ConfigurationNode configuration; + public ConfigurationNode world_config; + public ComponentManager componentManager = new ComponentManager(); + public DynmapListenerManager listenerManager = new DynmapListenerManager(this); + public PlayerFaces playerfacemgr; + public Events events = new Events(); + public String deftemplatesuffix = ""; + private DynmapMapCommands dmapcmds = new DynmapMapCommands(); + private DynmapExpCommands dynmapexpcmds = new DynmapExpCommands(); + boolean bettergrass = false; + boolean smoothlighting = false; + private boolean ctmsupport = false; + private boolean customcolorssupport = false; + private String def_image_format = "png"; + private HashSet enabledTriggers = new HashSet(); + public boolean disable_chat_to_web = false; + private WebAuthManager authmgr; + public boolean player_info_protected; + private boolean transparentLeaves = true; + private List sortPermissionNodes; + private int perTickLimit = 50; // 50 ms + private boolean dumpMissing = false; + + private int config_hashcode; /* Used to signal need to reload web configuration (world changes, config update, etc) */ + private int fullrenderplayerlimit; /* Number of online players that will cause fullrender processing to pause */ + private int updateplayerlimit; /* Number of online players that will cause update processing to pause */ + private boolean didfullpause; + private boolean didupdatepause; + private Map> ids_by_ip = new HashMap>(); + private boolean persist_ids_by_ip = false; + private int snapshotcachesize; + private boolean snapshotsoftref; + private int[] blockmaterialmap = new int[0]; + private String[] biomenames = new String[0]; + private Map blockmap = null; + private Map itemmap = null; + private static String[] blocknames = null; + private BlockStateManager blkstateman = new BlockStateManager(); + + private boolean loginRequired; + + /* Flag to let code know that we're doing reload - make sure we don't double-register event handlers */ + public boolean is_reload = false; + public static boolean ignore_chunk_loads = false; /* Flag keep us from processing our own chunk loads */ + + private MarkerAPIImpl markerapi; + + private File dataDirectory; + private File tilesDirectory; + private File exportDirectory; + private String plugin_ver; + private MapStorage defaultStorage; + + private String[] deftriggers = { }; + + /* Constructor for core */ + public DynmapCore() { + } + + /* Cleanup method */ + public void cleanup() { + server = null; + markerapi = null; + } + + // Set plugin jar file + public void setPluginJarFile(File f) { + jarfile = f; + } + // Get plugin jar file + public File getPluginJarFile() { + return jarfile; + } + /* Dependencies - need to be supplied by plugin wrapper */ + public void setPluginVersion(String pluginver, String platform) { + this.plugin_ver = pluginver; + this.platform = platform; + } + /* Default platform to forge... */ + public void setPluginVersion(String pluginver) { + setPluginVersion(pluginver, "Forge"); + } + public void setDataFolder(File dir) { + dataDirectory = dir; + } + public final File getDataFolder() { + return dataDirectory; + } + public final File getTilesFolder() { + return tilesDirectory; + } + public final File getExportFolder() { + return exportDirectory; + } + public void setMinecraftVersion(String mcver) { + this.platformVersion = mcver; + } + + public void setServer(DynmapServerInterface srv) { + server = srv; + } + public final DynmapServerInterface getServer() { return server; } + + public final void setBlockMaterialMap(int[] materials) { + blockmaterialmap = materials; + } + public final int[] getBlockMaterialMap() { + return blockmaterialmap; + } + + public final Map getBlockIDMap() { + return blockmap; + } + + public static final String getBlockName(int id) { + return blocknames[id]; + } + + public final void setBiomeNames(String[] names) { + biomenames = names; + } + + public final String getBiomeName(int biomeid) { + String n = null; + if ((biomeid >= 0) && (biomeid < biomenames.length)) { + n = biomenames[biomeid]; + } + if(n == null) n = "biome" + biomeid; + return n; + } + public final String[] getBiomeNames() { + return biomenames; + } + + public final MapManager getMapManager() { + return mapManager; + } + + public final void setTriggerDefault(String[] triggers) { + deftriggers = triggers; + } + + public final void setLeafTransparency(boolean trans) { + transparentLeaves = trans; + } + public final boolean getLeafTransparency() { + return transparentLeaves; + } + + /* Add/Replace branches in configuration tree with contribution from a separate file */ + private void mergeConfigurationBranch(ConfigurationNode cfgnode, String branch, boolean replace_existing, boolean islist) { + Object srcbranch = cfgnode.getObject(branch); + if(srcbranch == null) + return; + /* See if top branch is in configuration - if not, just add whole thing */ + Object destbranch = configuration.getObject(branch); + if(destbranch == null) { /* Not found */ + configuration.put(branch, srcbranch); /* Add new tree to configuration */ + return; + } + /* If list, merge by "name" attribute */ + if(islist) { + List dest = configuration.getNodes(branch); + List src = cfgnode.getNodes(branch); + /* Go through new records : see what to do with each */ + for(ConfigurationNode node : src) { + String name = node.getString("name", null); + if(name == null) continue; + /* Walk destination - see if match */ + boolean matched = false; + for(ConfigurationNode dnode : dest) { + String dname = dnode.getString("name", null); + if(dname == null) continue; + if(dname.equals(name)) { /* Match? */ + if(replace_existing) { + dnode.clear(); + dnode.putAll(node); + } + matched = true; + break; + } + } + /* If no match, add to end */ + if(!matched) { + dest.add(node); + } + } + configuration.put(branch,dest); + } + /* If configuration node, merge by key */ + else { + ConfigurationNode src = cfgnode.getNode(branch); + ConfigurationNode dest = configuration.getNode(branch); + for(String key : src.keySet()) { /* Check each contribution */ + if(dest.containsKey(key)) { /* Exists? */ + if(replace_existing) { /* If replacing, do so */ + dest.put(key, src.getObject(key)); + } + } + else { /* Else, always add if not there */ + dest.put(key, src.getObject(key)); + } + } + } + } + /* Table of default templates - all are resources in dynmap.jar unnder templates/, and go in templates directory when needed */ + private static final String[] stdtemplates = { "normal.txt", "nether.txt", "normal-lowres.txt", + "nether-lowres.txt", "normal-hires.txt", "nether-hires.txt", + "normal-vlowres.txt", "nether-vlowres.txt", "the_end.txt", "the_end-vlowres.txt", + "the_end-lowres.txt", "the_end-hires.txt", + "normal-low_boost_hi.txt", "normal-hi_boost_vhi.txt", "normal-hi_boost_xhi.txt", + "nether-low_boost_hi.txt", "nether-hi_boost_vhi.txt", "nether-hi_boost_xhi.txt", + "the_end-low_boost_hi.txt", "the_end-hi_boost_vhi.txt", "the_end-hi_boost_xhi.txt" + }; + + private static final String CUSTOM_PREFIX = "custom-"; + /* Load templates from template folder */ + private void loadTemplates() { + File templatedir = new File(dataDirectory, "templates"); + templatedir.mkdirs(); + /* First, prime the templates directory with default standard templates, if needed */ + for(String stdtemplate : stdtemplates) { + File f = new File(templatedir, stdtemplate); + updateVersionUsingDefaultResource("/templates/" + stdtemplate, f); + } + /* Now process files */ + String[] templates = templatedir.list(); + /* Go through list - process all ones not starting with 'custom' first */ + for(String tname: templates) { + /* If matches naming convention */ + if(tname.endsWith(".txt") && (!tname.startsWith(CUSTOM_PREFIX))) { + File tf = new File(templatedir, tname); + ConfigurationNode cn = new ConfigurationNode(tf); + cn.load(); + /* Supplement existing values (don't replace), since configuration.txt is more custom than these */ + mergeConfigurationBranch(cn, "templates", false, false); + } + } + /* Go through list again - this time do custom- ones */ + for(String tname: templates) { + /* If matches naming convention */ + if(tname.endsWith(".txt") && tname.startsWith(CUSTOM_PREFIX)) { + File tf = new File(templatedir, tname); + ConfigurationNode cn = new ConfigurationNode(tf); + cn.load(); + /* This are overrides - replace even configuration.txt content */ + mergeConfigurationBranch(cn, "templates", true, false); + } + } + } + + public boolean enableCore() { + boolean rslt = initConfiguration(null); + if (rslt) + rslt = enableCore(null); + return rslt; + } + + public boolean initConfiguration(EnableCoreCallbacks cb) { + /* Start with clean events */ + events = new Events(); + /* Default to being unprotected - set to protected by update components */ + player_info_protected = false; + + /* Load plugin version info */ + loadVersion(); + + /* Initialize confguration.txt if needed */ + File f = new File(dataDirectory, "configuration.txt"); + if(!createDefaultFileFromResource("/configuration.txt", f)) { + return false; + } + + /* Load configuration.txt */ + configuration = new ConfigurationNode(f); + configuration.load(); + + /* Prime the tiles directory */ + tilesDirectory = getFile(configuration.getString("tilespath", "web/tiles")); + if (!tilesDirectory.isDirectory() && !tilesDirectory.mkdirs()) { + Log.warning("Could not create directory for tiles ('" + tilesDirectory + "')."); + } + // Prime the exports directory + exportDirectory = getFile(configuration.getString("exportpath", "export")); + if (!exportDirectory.isDirectory() && !exportDirectory.mkdirs()) { + Log.warning("Could not create directory for exports ('" + exportDirectory + "')."); + } + // Create default storage handler + String storetype = configuration.getString("storage/type", "filetree"); + if (storetype.equals("filetree")) { + defaultStorage = new FileTreeMapStorage(); + } + else if (storetype.equals("sqlite")) { + defaultStorage = new SQLiteMapStorage(); + } + else if (storetype.equals("mysql")) { + defaultStorage = new MySQLMapStorage(); + } + else if (storetype.equals("mariadb")) { + defaultStorage = new MariaDBMapStorage(); + } + else { + Log.severe("Invalid storage type for map data: " + storetype); + return false; + } + if (!defaultStorage.init(this)) { + Log.severe("Map storage initialization failure"); + return false; + } + + /* Register API with plugin, if needed */ + if(!markerAPIInitialized()) { + MarkerAPIImpl api = MarkerAPIImpl.initializeMarkerAPI(this); + this.registerMarkerAPI(api); + } + /* Call back to plugin to report that configuration is available */ + if(cb != null) + cb.configurationLoaded(); + return true; + } + + public boolean enableCore(EnableCoreCallbacks cb) { + /* Update extracted files, if needed */ + updateExtractedFiles(); + /* Initialize authorization manager */ + if(configuration.getBoolean("login-enabled", false)) { + authmgr = new WebAuthManager(this); + defaultStorage.setLoginEnabled(this); + } + + /* Add options to avoid 0.29 re-render (fixes very inconsistent with previous maps) */ + HDMapManager.waterlightingfix = configuration.getBoolean("correct-water-lighting", false); + HDMapManager.biomeshadingfix = configuration.getBoolean("correct-biome-shading", false); + /* Load control for leaf transparency (spout lighting bug workaround) */ + transparentLeaves = configuration.getBoolean("transparent-leaves", true); + /* Get default image format */ + def_image_format = configuration.getString("image-format", "png"); + MapType.ImageFormat fmt = MapType.ImageFormat.fromID(def_image_format); + if(fmt == null) { + Log.severe("Invalid image-format: " + def_image_format); + def_image_format = "png"; + } + + DynmapWorld.doInitialScan(configuration.getBoolean("initial-zoomout-validate", true)); + + smoothlighting = configuration.getBoolean("smooth-lighting", false); + ctmsupport = configuration.getBoolean("ctm-support", true); + customcolorssupport = configuration.getBoolean("custom-colors-support", true); + Log.verbose = configuration.getBoolean("verbose", true); + deftemplatesuffix = configuration.getString("deftemplatesuffix", ""); + /* Get snapshot cache size */ + snapshotcachesize = configuration.getInteger("snapshotcachesize", 500); + /* Get soft ref flag for cache (weak=false, soft=true) */ + snapshotsoftref = configuration.getBoolean("soft-ref-cache", true); + /* Default better-grass */ + bettergrass = configuration.getBoolean("better-grass", false); + /* Load full render processing player limit */ + fullrenderplayerlimit = configuration.getInteger("fullrenderplayerlimit", 0); + /* Load update render processing player limit */ + updateplayerlimit = configuration.getInteger("updateplayerlimit", 0); + /* Load sort permission nodes */ + sortPermissionNodes = configuration.getStrings("player-sort-permission-nodes", null); + + perTickLimit = configuration.getInteger("per-tick-time-limit", 50); + if (perTickLimit < 5) perTickLimit = 5; + + dumpMissing = configuration.getBoolean("dump-missing-blocks", false); + + /* Load preupdate/postupdate commands */ + ImageIOManager.preUpdateCommand = configuration.getString("custom-commands/image-updates/preupdatecommand", ""); + ImageIOManager.postUpdateCommand = configuration.getString("custom-commands/image-updates/postupdatecommand", ""); + + /* Get block and item maps */ + blockmap = server.getBlockUniqueIDMap(); + itemmap = server.getItemUniqueIDMap(); + + /* Build block name list */ + blocknames = new String[DynmapCore.BLOCKTABLELEN]; + for (Entry v : blockmap.entrySet()) { + blocknames[v.getValue()] = v.getKey(); + } + + /* Process mod support */ + ModSupportImpl.complete(this.dataDirectory); + /* Load block models */ + Log.verboseinfo("Loading models..."); + HDBlockModels.loadModels(this, configuration); + /* Load texture mappings */ + Log.verboseinfo("Loading texture mappings..."); + TexturePack.loadTextureMapping(this, configuration); + + /* Now, process worlds.txt - merge it in as an override of existing values (since it is only user supplied values) */ + File f = new File(dataDirectory, "worlds.txt"); + if(!createDefaultFileFromResource("/worlds.txt", f)) { + return false; + } + world_config = new ConfigurationNode(f); + world_config.load(); + + /* Now, process templates */ + Log.verboseinfo("Loading templates..."); + loadTemplates(); + + /* If we're persisting ids-by-ip, load it */ + persist_ids_by_ip = configuration.getBoolean("persist-ids-by-ip", true); + if(persist_ids_by_ip) { + Log.verboseinfo("Loading userid-by-IP data..."); + loadIDsByIP(); + } + + loadDebuggers(); + + playerList = new PlayerList(getServer(), getFile("hiddenplayers.txt"), configuration); + playerList.load(); + + mapManager = new MapManager(this, configuration); + mapManager.startRendering(); + + playerfacemgr = new PlayerFaces(this); + + updateConfigHashcode(); /* Initialize/update config hashcode */ + + loginRequired = configuration.getBoolean("login-required", false); + + loadWebserver(); + + enabledTriggers.clear(); + List triggers = configuration.getStrings("render-triggers", new ArrayList()); + if ((triggers != null) && (triggers.size() > 0)) + { + for (Object trigger : triggers) { + enabledTriggers.add((String) trigger); + } + } + else { + for (String def : deftriggers) { + enabledTriggers.add(def); + } + } + + // Load components. + for(Component component : configuration.createInstances("components", new Class[] { DynmapCore.class }, new Object[] { this })) { + componentManager.add(component); + } + Log.verboseinfo("Loaded " + componentManager.components.size() + " components."); + + if (!configuration.getBoolean("disable-webserver", false)) { + startWebserver(); + } + + /* Add login/logoff listeners */ + listenerManager.addListener(EventType.PLAYER_JOIN, new DynmapListenerManager.PlayerEventListener() { + @Override + public void playerEvent(DynmapPlayer p) { + playerJoined(p); + } + }); + listenerManager.addListener(EventType.PLAYER_QUIT, new DynmapListenerManager.PlayerEventListener() { + @Override + public void playerEvent(DynmapPlayer p) { + playerQuit(p); + } + }); + + /* Print version info */ + Log.info("version " + plugin_ver + " is enabled - core version " + version ); + + events.trigger("initialized", null); + + //dumpColorMap("standard.txt", "standard"); + //dumpColorMap("dokudark.txt", "dokudark.zip"); + //dumpColorMap("dokulight.txt", "dokulight.zip"); + //dumpColorMap("dokuhigh.txt", "dokuhigh.zip"); + //dumpColorMap("misa.txt", "misa.zip"); + //dumpColorMap("sphax.txt", "sphax.zip"); + + return true; + } + + void dumpColorMap(String id, String name) { + int[] sides = new int[] { BlockStep.Y_MINUS.ordinal(), BlockStep.X_PLUS.ordinal(), BlockStep.Z_PLUS.ordinal(), + BlockStep.Y_PLUS.ordinal(), BlockStep.X_MINUS.ordinal(), BlockStep.Z_MINUS.ordinal() }; + FileWriter fw = null; + try { + fw = new FileWriter(id); + TexturePack tp = TexturePack.getTexturePack(this, name); + if (tp == null) return; + tp = tp.resampleTexturePack(1); + if (tp == null) return; + Color c = new Color(); + for (int gidx = 0; gidx < DynmapBlockState.getGlobalIndexMax(); gidx++) { + DynmapBlockState blk = DynmapBlockState.getStateByGlobalIndex(gidx); + if (blk.isAir()) continue; + int meta0color = 0; + HDBlockStateTextureMap map = HDBlockStateTextureMap.getByBlockState(blk); + boolean done = false; + for (int i = 0; (!done) && (i < sides.length); i++) { + int idx = map.getIndexForFace(sides[i]); + if (idx < 0) continue; + int rgb[] = tp.getTileARGB(idx % 1000000); + if (rgb == null) continue; + if (rgb[0] == 0) continue; + c.setARGB(rgb[0]); + idx = (idx / 1000000); + switch(idx) { + case 1: // grass + case 18: // grass + System.out.println("Used grass for " + blk); + c.blendColor(tp.getTrivialGrassMultiplier() | 0xFF000000); + break; + case 2: // foliage + case 19: // foliage + case 22: // foliage + System.out.println("Used foliage for " + blk); + c.blendColor(tp.getTrivialFoliageMultiplier() | 0xFF000000); + break; + case 13: // pine + c.blendColor(0x619961 | 0xFF000000); + break; + case 14: // birch + c.blendColor(0x80a755 | 0xFF000000); + break; + case 15: // lily + c.blendColor(0x208030 | 0xFF000000); + break; + case 3: // water + case 20: // water + System.out.println("Used water for " + blk); + c.blendColor(tp.getTrivialWaterMultiplier() | 0xFF000000); + break; + case 12: // clear inside + if (blk.isWater()) { // special case for water + System.out.println("Used water for " + blk); + c.blendColor(tp.getTrivialWaterMultiplier() | 0xFF000000); + } + break; + } + int custmult = tp.getCustomBlockMultiplier(blk); + if (custmult != 0xFFFFFF) { + System.out.println(String.format("Custom color: %06x for %s", custmult, blk)); + if ((custmult & 0xFF000000) == 0) { + custmult |= 0xFF000000; + } + c.blendColor(custmult); + } + String ln = ""; + if (blk.stateIndex == 0) { + meta0color = c.getARGB(); + ln = blk.blockName + " "; + } + else { + ln = blk + " "; + } + if ((blk.stateIndex == 0) || (meta0color != c.getARGB())) { + ln += c.getRed() + " " + c.getGreen() + " " + c.getBlue() + " " + c.getAlpha(); + ln += " " + (c.getRed()*4/5) + " " + (c.getGreen()*4/5) + " " + (c.getBlue()*4/5) + " " + c.getAlpha(); + ln += " " + (c.getRed()/2) + " " + (c.getGreen()/2) + " " + (c.getBlue()/2) + " " + c.getAlpha(); + ln += " " + (c.getRed()*2/5) + " " + (c.getGreen()*2/5) + " " + (c.getBlue()*2/5) + " " + c.getAlpha() + "\n"; + fw.write(ln); + } + done = true; + } + } + } catch (IOException iox) { + } finally { + if (fw != null) { try { fw.close(); } catch (IOException x) {} } + } + } + + private void playerJoined(DynmapPlayer p) { + playerList.updateOnlinePlayers(null); + if((fullrenderplayerlimit > 0) || (updateplayerlimit > 0)) { + int pcnt = getServer().getOnlinePlayers().length; + + if ((fullrenderplayerlimit > 0) && (pcnt == fullrenderplayerlimit)) { + if(getPauseFullRadiusRenders() == false) { /* If not paused, pause it */ + setPauseFullRadiusRenders(true); + Log.info("Pause full/radius renders - player limit reached"); + didfullpause = true; + } + else { + didfullpause = false; + } + } + if ((updateplayerlimit > 0) && (pcnt == updateplayerlimit)) { + if(getPauseUpdateRenders() == false) { /* If not paused, pause it */ + setPauseUpdateRenders(true); + Log.info("Pause tile update renders - player limit reached"); + didupdatepause = true; + } + else { + didupdatepause = false; + } + } + } + /* Add player info to IP-to-ID table */ + InetSocketAddress addr = p.getAddress(); + if(addr != null) { + String ip = addr.getAddress().getHostAddress(); + LinkedList ids = ids_by_ip.get(ip); + if(ids == null) { + ids = new LinkedList(); + ids_by_ip.put(ip, ids); + } + String pid = p.getName(); + if(ids.indexOf(pid) != 0) { + ids.remove(pid); /* Remove from list */ + ids.addFirst(pid); /* Put us first on list */ + } + } + /* Check sort weight permissions list */ + if ((sortPermissionNodes != null) && (sortPermissionNodes.size() > 0)) { + int ord; + for (ord = 0; ord < sortPermissionNodes.size(); ord++) { + if (p.hasPermissionNode(sortPermissionNodes.get(ord))) { + break; + } + } + p.setSortWeight(ord); + } + else { + p.setSortWeight(0); // Initialize to zero + } + /* And re-attach to active jobs */ + if(mapManager != null) + mapManager.connectTasksToPlayer(p); + } + + /* Called by plugin each time a player quits the server */ + private void playerQuit(DynmapPlayer p) { + playerList.updateOnlinePlayers(p.getName()); + if ((fullrenderplayerlimit > 0) || (updateplayerlimit > 0)) { + /* Quitting player is still online at this moment, so discount count by 1 */ + int pcnt = getServer().getOnlinePlayers().length - 1; + if ((fullrenderplayerlimit > 0) && (pcnt == (fullrenderplayerlimit - 1))) { + if(didfullpause && getPauseFullRadiusRenders()) { /* Only unpause if we did the pause */ + setPauseFullRadiusRenders(false); + Log.info("Resume full/radius renders - below player limit"); + } + didfullpause = false; + } + if ((updateplayerlimit > 0) && (pcnt == (updateplayerlimit - 1))) { + if(didupdatepause && getPauseUpdateRenders()) { /* Only unpause if we did the pause */ + setPauseUpdateRenders(false); + Log.info("Resume tile update renders - below player limit"); + } + didupdatepause = false; + } + } + } + + public void updateConfigHashcode() { + config_hashcode = (int)System.currentTimeMillis(); + } + + public int getConfigHashcode() { + return config_hashcode; + } + + private FileResource createFileResource(String path) { + try { + File f = new File(path); + URI uri = f.toURI(); + URL url = uri.toURL(); + return new FileResource(url); + } catch(Exception e) { + Log.info("Could not create file resource"); + return null; + } + } + + public void loadWebserver() { + org.eclipse.jetty.util.log.Log.setLog(new JettyNullLogger()); + String ip = server.getServerIP(); + if ((ip == null) || (ip.trim().length() == 0)) { + ip = "0.0.0.0"; + } + webhostname = configuration.getString("webserver-bindaddress", ip); + webport = configuration.getInteger("webserver-port", 8123); + + webServer = new Server(); + webServer.setSessionIdManager(new HashSessionIdManager()); + + int maxconnections = configuration.getInteger("max-sessions", 30); + if(maxconnections < 2) maxconnections = 2; + LinkedBlockingQueue queue = new LinkedBlockingQueue(maxconnections); + ExecutorThreadPool pool = new ExecutorThreadPool(2, maxconnections, 60, TimeUnit.SECONDS, queue); + webServer.setThreadPool(pool); + + SelectChannelConnector connector=new SelectChannelConnector(); + connector.setMaxIdleTime(5000); + connector.setAcceptors(1); + connector.setAcceptQueueSize(50); + connector.setLowResourcesMaxIdleTime(1000); + connector.setLowResourcesConnections(maxconnections/2); + if(webhostname.equals("0.0.0.0") == false) + connector.setHost(webhostname); + connector.setPort(webport); + webServer.setConnectors(new Connector[]{connector}); + + webServer.setStopAtShutdown(true); + //webServer.setGracefulShutdown(1000); + + final boolean allow_symlinks = configuration.getBoolean("allow-symlinks", false); + router = new HandlerRouter() {{ + this.addHandler("/", new FileResourceHandler() {{ + this.setAliases(allow_symlinks); + this.setWelcomeFiles(new String[] { "index.html" }); + this.setDirectoriesListed(true); + this.setBaseResource(createFileResource(getFile(getWebPath()).getAbsolutePath())); + }}); + this.addHandler("/tiles/*", new MapStorageResourceHandler() {{ + this.setCore(DynmapCore.this); + }}); + }}; + + if(allow_symlinks) + Log.verboseinfo("Web server is permitting symbolic links"); + else + Log.verboseinfo("Web server is not permitting symbolic links"); + + List filters = new LinkedList(); + + /* Check for banned IPs */ + boolean checkbannedips = configuration.getBoolean("check-banned-ips", true); + if (checkbannedips) { + filters.add(new BanIPFilter(this)); + } +// filters.add(new LoginFilter(this)); + + /* Load customized response headers, if any */ + filters.add(new CustomHeaderFilter(configuration.getNode("http-response-headers"))); + + FilterHandler fh = new FilterHandler(router, filters); + HandlerList hlist = new HandlerList(); + hlist.setHandlers(new org.eclipse.jetty.server.Handler[] { new SessionHandler(), fh }); + webServer.setHandler(hlist); + + addServlet("/up/configuration", new org.dynmap.servlet.ClientConfigurationServlet(this)); + addServlet("/standalone/config.js", new org.dynmap.servlet.ConfigJSServlet(this)); + if(authmgr != null) { + LoginServlet login = new LoginServlet(this); + addServlet("/up/login", login); + addServlet("/up/register", login); + } + } + + public boolean isLoginSupportEnabled() { + return (authmgr != null); + } + + public boolean isLoginRequired() { + return loginRequired; + } + + public boolean isCTMSupportEnabled() { + return ctmsupport; + } + + public boolean isCustomColorsSupportEnabled() { + return customcolorssupport; + } + + public Set getIPBans() { + return getServer().getIPBans(); + } + + public void addServlet(String path, HttpServlet servlet) { + new ServletHolder(servlet); + router.addServlet(path, servlet); + } + + + public void startWebserver() { + try { + if(webServer != null) { + webServer.start(); + Log.info("Web server started on address " + webhostname + ":" + webport); + } + } catch (Exception e) { + Log.severe("Failed to start WebServer on address " + webhostname + ":" + webport + " : " + e.getMessage()); + } + } + + public void disableCore() { + if(persist_ids_by_ip) + saveIDsByIP(); + + if (webServer != null) { + try { + webServer.stop(); + for(int i = 0; i < 100; i++) { /* Limit wait to 10 seconds */ + if(webServer.isStopping()) + Thread.sleep(100); + } + if(webServer.isStopping()) { + Log.warning("Graceful shutdown timed out - continuing to terminate"); + } + } catch (Exception e) { + Log.severe("Failed to stop WebServer!", e); + } + webServer = null; + } + + if (componentManager != null) { + int componentCount = componentManager.components.size(); + for(Component component : componentManager.components) { + component.dispose(); + } + componentManager.clear(); + Log.info("Unloaded " + componentCount + " components."); + } + + if (mapManager != null) { + mapManager.stopRendering(); + mapManager = null; + } + + playerfacemgr = null; + /* Clean up registered listeners */ + listenerManager.cleanup(); + + /* Don't clean up markerAPI - other plugins may still be accessing it */ + + authmgr = null; + + Debug.clearDebuggers(); + } + + 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(getDataFolder(), path); + } + + protected void loadDebuggers() { + List debuggersConfiguration = configuration.getNodes("debuggers"); + Debug.clearDebuggers(); + for (ConfigurationNode debuggerConfiguration : debuggersConfiguration) { + try { + Class debuggerClass = Class.forName((String) debuggerConfiguration.getString("class")); + Constructor constructor = debuggerClass.getConstructor(DynmapCore.class, ConfigurationNode.class); + Debugger debugger = (Debugger) constructor.newInstance(this, debuggerConfiguration); + Debug.addDebugger(debugger); + } catch (Exception e) { + Log.severe("Error loading debugger: " + e); + e.printStackTrace(); + continue; + } + } + } + + /* Parse argument strings : handle quoted strings */ + public static String[] parseArgs(String[] args, DynmapCommandSender snd) { + ArrayList rslt = new ArrayList(); + /* Build command line, so we can parse our way - make sure there is trailing space */ + String cmdline = ""; + for(int i = 0; i < args.length; i++) { + cmdline += args[i] + " "; + } + boolean inquote = false; + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < cmdline.length(); i++) { + char c = cmdline.charAt(i); + if(inquote) { /* If in quote, accumulate until end or another quote */ + if(c == '\"') { /* End quote */ + inquote = false; + } + else { + sb.append(c); + } + } + else if(c == '\"') { /* Start of quote? */ + inquote = true; + } + else if(c == ' ') { /* Ending space? */ + rslt.add(sb.toString()); + sb.setLength(0); + } + else { + sb.append(c); + } + } + if(inquote) { /* If still in quote, syntax error */ + snd.sendMessage("Error: unclosed doublequote"); + return null; + } + return rslt.toArray(new String[rslt.size()]); + } + + private static final Set commands = new HashSet(Arrays.asList(new String[] { + "render", + "hide", + "show", + "version", + "fullrender", + "cancelrender", + "radiusrender", + "updaterender", + "reload", + "stats", + "triggerstats", + "resetstats", + "sendtoweb", + "pause", + "purgequeue", + "purgemap", + "purgeworld", + "quiet", + "ids-for-ip", + "ips-for-id", + "add-id-for-ip", + "del-id-for-ip", + "webregister", + "help"})); + + private static class CommandInfo { + final String cmd; + final String subcmd; + final String args; + final String helptext; + public CommandInfo(String cmd, String subcmd, String helptxt) { + this.cmd = cmd; + this.subcmd = subcmd; + this.helptext = helptxt; + this.args = ""; + } + public CommandInfo(String cmd, String subcmd, String args, String helptxt) { + this.cmd = cmd; + this.subcmd = subcmd; + this.args = args; + this.helptext = helptxt; + } + public boolean matches(String c, String sc) { + return (cmd.equals(c) && subcmd.equals(sc)); + } + public boolean matches(String c) { + return cmd.equals(c); + } + }; + + private static final CommandInfo[] commandinfo = { + new CommandInfo("dynmap", "", "Control execution of dynmap."), + new CommandInfo("dynmap", "hide", "Hides the current player from the map."), + new CommandInfo("dynmap", "hide", "", "Hides on the map."), + new CommandInfo("dynmap", "show", "Shows the current player on the map."), + new CommandInfo("dynmap", "show", "", "Shows on the map."), + new CommandInfo("dynmap", "render", "Renders the tile at your location."), + new CommandInfo("dynmap", "fullrender", "Render all maps for entire world from your location."), + new CommandInfo("dynmap", "fullrender", "", "Render all maps for world ."), + new CommandInfo("dynmap", "fullrender", ":", "Render map of world'."), + new CommandInfo("dynmap", "radiusrender", "", "Render at least block radius from your location on all maps."), + new CommandInfo("dynmap", "radiusrender", " ", "Render at least block radius from your location on map ."), + new CommandInfo("dynmap", "radiusrender", " ", "Render at least block radius from location , on world ."), + new CommandInfo("dynmap", "radiusrender", " ", "Render at least block radius from location , on world on map ."), + new CommandInfo("dynmap", "updaterender", "Render updates starting at your location on all maps."), + new CommandInfo("dynmap", "updaterender", "", "Render updates starting at your location on map ."), + new CommandInfo("dynmap", "updaterender", " ", "Render updates starting at location , on world for map ."), + new CommandInfo("dynmap", "cancelrender", "Cancels any active renders on current world."), + new CommandInfo("dynmap", "cancelrender", "", "Cancels any active renders of world ."), + new CommandInfo("dynmap", "stats", "Show render statistics."), + new CommandInfo("dynmap", "triggerstats", "Show render update trigger statistics."), + new CommandInfo("dynmap", "resetstats", "Reset render statistics."), + new CommandInfo("dynmap", "sendtoweb", "", "Send message to web users."), + new CommandInfo("dynmap", "purgequeue", "Empty all pending tile updates from update queue."), + new CommandInfo("dynmap", "purgequeue", "", "Empty all pending tile updates from update queue for world ."), + new CommandInfo("dynmap", "purgemap", " ", "Delete all existing tiles for map on world ."), + new CommandInfo("dynmap", "purgeworld", "", "Delete all existing directories for world ."), + new CommandInfo("dynmap", "pause", "Show render pause state."), + new CommandInfo("dynmap", "pause", "", "Set render pause state."), + new CommandInfo("dynmap", "quiet", "Stop output from active jobs."), + new CommandInfo("dynmap", "ids-for-ip", "", "Show player IDs that have logged in from address ."), + new CommandInfo("dynmap", "ips-for-id", "", "Show IP addresses that have been used for player ."), + new CommandInfo("dynmap", "add-id-for-ip", " ", "Associate player with IP address ."), + new CommandInfo("dynmap", "del-id-for-ip", " ", "Disassociate player from IP address ."), + new CommandInfo("dynmap", "webregister", "Start registration process for creating web login account"), + new CommandInfo("dynmap", "webregister", "", "Start registration process for creating web login account for player "), + new CommandInfo("dynmap", "version", "Return version information"), + new CommandInfo("dmarker", "", "Manipulate map markers."), + new CommandInfo("dmarker", "add", "