diff --git a/bin/.gitignore b/bin/.gitignore index 9b0146c..3059bf7 100644 --- a/bin/.gitignore +++ b/bin/.gitignore @@ -1,2 +1,3 @@ /me/ /protocollib/ +/org/ diff --git a/src/me/TheBukor/SkStuff/SkStuff.java b/src/me/TheBukor/SkStuff/SkStuff.java index 464f5c8..557aa6e 100644 --- a/src/me/TheBukor/SkStuff/SkStuff.java +++ b/src/me/TheBukor/SkStuff/SkStuff.java @@ -1,9 +1,12 @@ package me.TheBukor.SkStuff; +import java.io.IOException; + import javax.annotation.Nullable; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.OfflinePlayer; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.entity.Entity; @@ -13,6 +16,7 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; +import org.mcstats.Metrics; import com.sk89q.worldedit.EditSession; import com.sk89q.worldguard.bukkit.WGBukkit; @@ -77,11 +81,13 @@ import me.TheBukor.SkStuff.expressions.ExprSelectionArea; import me.TheBukor.SkStuff.expressions.ExprSelectionOfPlayer; import me.TheBukor.SkStuff.expressions.ExprSelectionPos; import me.TheBukor.SkStuff.expressions.ExprStepLength; +import me.TheBukor.SkStuff.expressions.ExprSuperPickaxe; import me.TheBukor.SkStuff.expressions.ExprTagOf; import me.TheBukor.SkStuff.expressions.ExprTimespanToNumber; import me.TheBukor.SkStuff.expressions.ExprToLowerCase; import me.TheBukor.SkStuff.expressions.ExprToUpperCase; import me.TheBukor.SkStuff.expressions.ExprVanishState; +import me.TheBukor.SkStuff.expressions.ExprWGMemberOwner; import me.TheBukor.SkStuff.expressions.ExprWordsToUpperCase; import me.TheBukor.SkStuff.util.NMSInterface; import me.TheBukor.SkStuff.util.NMS_v1_7_R4; @@ -147,7 +153,7 @@ public class SkStuff extends JavaPlugin { //Skript.registerExpression(ExprEndermanBlocks.class, ItemStack.class, ExpressionType.PROPERTY, "blocks that %entity% can (carry|hold|grab|steal)"); Skript.registerExpression(ExprMCIdOf.class, String.class, ExpressionType.PROPERTY, "(mc|minecraft) [(string|native)] id of %itemtype%", "%itemtype%'s minecraft [(string|native)] id"); Skript.registerExpression(ExprMCIdToItem.class, ItemStack.class, ExpressionType.SIMPLE, "item[[ ](stack|type)] (of|from) (mc|minecraft) [(string|native)] id %string%"); - Skript.registerExpression(ExprLastLocation.class, Location.class, ExpressionType.SIMPLE, " "); + Skript.registerExpression(ExprLastLocation.class, Location.class, ExpressionType.SIMPLE, "[the] (last|past|former) location of %entity%", "%entity%'s (last|past|former) location", "[the] location of %entity% (1|one) tick before", "%entity%'s location (1|one) tick before"); Skript.registerExpression(ExprStepLength.class, Number.class, ExpressionType.PROPERTY, "[the] step length of %entity%", "%entity%'s step length"); nmsMethods.registerCompoundClassInfo(); nmsMethods.registerNBTListClassInfo(); @@ -179,6 +185,7 @@ public class SkStuff extends JavaPlugin { Skript.registerExpression(ExprSelectionPos.class, Location.class, ExpressionType.PROPERTY, "[(world[ ]edit|we)] po(s|int)[ ](0¦1|1¦2) of %player%", "%player%'s [(world[ ]edit|we)] po(s|int)[ ](0¦1|1¦2)"); Skript.registerExpression(ExprSelectionArea.class, Integer.class, ExpressionType.SIMPLE, "(0¦volume|1¦(x( |-)size|width)|2¦(y( |-)size|height)|3¦(z( |-)size|length)|4¦area) of [(world[ ]edit|we)] selection of %player%", "%player%'s [(world[ ]edit|we)] selection (0¦volume|1¦(x( |-)size|width)|2¦(y( |-)size|height)|3¦(z( |-)size|length)|4¦area)"); Skript.registerExpression(ExprSchematicArea.class, Integer.class, ExpressionType.SIMPLE, "(0¦volume|1¦(x( |-)size|width)|2¦(y( |-)size|height)|3¦(z( |-)size|length)|4¦area) of schem[atic] [from] %string%"); + Skript.registerExpression(ExprSuperPickaxe.class, Boolean.class, ExpressionType.PROPERTY, "[(world[ ]edit|we)] super[ ]pick[axe] (state|mode) of %players%", "%players%'s [(world[ ]edit|we)] super[ ]pick[axe] (state|mode)"); Classes.registerClass(new ClassInfo(EditSession.class, "editsession").name("Edit Session").user("edit ?sessions?")); try { Class.forName("com.sk89q.worldedit.extent.logging.AbstractLoggingExtent"); @@ -204,16 +211,17 @@ public class SkStuff extends JavaPlugin { } condAmount += 1; effAmount += 13; - exprAmount += 7; + exprAmount += 8; typeAmount += 1; if (Bukkit.getPluginManager().getPlugin("WorldGuard") != null) { //WorldGuard depends on WorldEdit Plugin umbaska = Bukkit.getPluginManager().getPlugin("Umbaska"); Plugin skRambled = Bukkit.getPluginManager().getPlugin("SkRambled"); boolean registerNewTypes = (umbaska == null && skRambled == null); if (registerNewTypes) { - Skript.registerExpression(ExprFlagOfWGRegion.class, String.class, ExpressionType.PROPERTY, "[w[orld[ ]]g[uard]] flag %wgflag% of %wgregion%"); - Skript.registerExpression(ExprFlagsOfWGRegion.class, Flag.class, ExpressionType.PROPERTY, "all [w[orld[ ]]g[uard]] flags of %wgregion%"); - Classes.registerClass(new ClassInfo(Flag.class, "wgflag").name("WorldGuard Flag").user("(w(orld ?)?g(uard)? )?flags?").defaultExpression(new EventValueExpression(Flag.class)).parser(new Parser>() { + Skript.registerExpression(ExprFlagOfWGRegion.class, String.class, ExpressionType.PROPERTY, "[(world[ ]guard|wg)] flag %wgflag% of %wgregion%"); + Skript.registerExpression(ExprFlagsOfWGRegion.class, Flag.class, ExpressionType.PROPERTY, "[(all|the)] [(world[ ]guard|wg)] flags of %wgregion%"); + Skript.registerExpression(ExprWGMemberOwner.class, OfflinePlayer.class, ExpressionType.PROPERTY, "[the] (0¦members|1¦owner[s]) of [[the] (world[ ]guard|wg) region] %wgregion%"); + Classes.registerClass(new ClassInfo(Flag.class, "wgflag").name("WorldGuard Flag").user("((world ?guard|wg) )?flags?").defaultExpression(new EventValueExpression(Flag.class)).parser(new Parser>() { @Override @Nullable @@ -236,7 +244,7 @@ public class SkStuff extends JavaPlugin { return ".+"; } })); - Classes.registerClass(new ClassInfo(ProtectedRegion.class, "wgregion").name("WorldGuard Region").user("(w(orld ?)?g(uard)? )?regions?").defaultExpression(new EventValueExpression<>(ProtectedRegion.class)).parser(new Parser() { + Classes.registerClass(new ClassInfo(ProtectedRegion.class, "wgregion").name("WorldGuard Region").user("((world ?guard|wg) )?regions?").defaultExpression(new EventValueExpression<>(ProtectedRegion.class)).parser(new Parser() { @Override @Nullable @@ -269,8 +277,9 @@ public class SkStuff extends JavaPlugin { } else { Skript.registerExpression(ExprFlagOfWGRegion.class, String.class, ExpressionType.PROPERTY, "[skstuff] [w[orld[ ]]g[uard]] flag %flag% of %protectedregion%"); Skript.registerExpression(ExprFlagsOfWGRegion.class, Flag.class, ExpressionType.PROPERTY, "[skstuff] [all] [w[orld[ ]]g[uard]] flags of %protectedregion%"); + Skript.registerExpression(ExprWGMemberOwner.class, OfflinePlayer.class, ExpressionType.PROPERTY, "[the] [skstuff] (0¦members|1¦owner[s]) of [[the] (world[ ]guard|wg) region] %protectedregion%"); } - exprAmount += 2; + exprAmount += 3; } } if (Bukkit.getPluginManager().getPlugin("VanishNoPacket") != null) { @@ -280,6 +289,13 @@ public class SkStuff extends JavaPlugin { effAmount += 1; exprAmount += 1; } + try { + Metrics metrics = new Metrics(this); + metrics.start(); + } catch (IOException ex) { + getLogger().warning("Sorry, I've failed to hook SkStuff into Metrics. I'm really sorry."); + getLogger().warning("Here's an error for you: " + ex.getMessage()); + } getLogger().info("Everything ready! Loaded a total of " + condAmount + " conditions, " + effAmount + " effects, " + evtAmount + "events, " + exprAmount + " expressions and " + typeAmount + " types!"); } else { getLogger().info("Unable to find Skript or Skript isn't accepting registrations, disabling SkStuff..."); diff --git a/src/me/TheBukor/SkStuff/expressions/ExprFireProof.java b/src/me/TheBukor/SkStuff/expressions/ExprFireProof.java index 0d037ca..5e80fff 100644 --- a/src/me/TheBukor/SkStuff/expressions/ExprFireProof.java +++ b/src/me/TheBukor/SkStuff/expressions/ExprFireProof.java @@ -23,7 +23,7 @@ public class ExprFireProof extends SimpleExpression { @Override public boolean isSingle() { - return true; + return entities.isSingle(); } @SuppressWarnings("unchecked") diff --git a/src/me/TheBukor/SkStuff/expressions/ExprNoClip.java b/src/me/TheBukor/SkStuff/expressions/ExprNoClip.java index 10e0063..f0fbafb 100644 --- a/src/me/TheBukor/SkStuff/expressions/ExprNoClip.java +++ b/src/me/TheBukor/SkStuff/expressions/ExprNoClip.java @@ -23,7 +23,7 @@ public class ExprNoClip extends SimpleExpression { @Override public boolean isSingle() { - return false; + return entities.isSingle(); } @SuppressWarnings("unchecked") diff --git a/src/me/TheBukor/SkStuff/expressions/ExprSuperPickaxe.java b/src/me/TheBukor/SkStuff/expressions/ExprSuperPickaxe.java new file mode 100644 index 0000000..7040aaa --- /dev/null +++ b/src/me/TheBukor/SkStuff/expressions/ExprSuperPickaxe.java @@ -0,0 +1,82 @@ +package me.TheBukor.SkStuff.expressions; + +import javax.annotation.Nullable; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; + +import com.sk89q.worldedit.bukkit.WorldEditPlugin; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; + +public class ExprSuperPickaxe extends SimpleExpression { + private Expression players; + + @Override + public boolean isSingle() { + return players.isSingle(); + } + + @Override + public Class getReturnType() { + return Boolean.class; + } + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] expr, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + players = (Expression) expr[0]; + return true; + } + + @Override + public String toString(@Nullable Event e, boolean debug) { + return "world edit super pickaxe state of " + players.toString(e, debug); + } + + @Override + @Nullable + protected Boolean[] get(Event e) { + WorldEditPlugin we = (WorldEditPlugin) Bukkit.getPluginManager().getPlugin("WorldEdit"); + Player[] ps = players.getAll(e); + Boolean[] states = new Boolean[ps.length]; + int i = 0; + for (Player p : ps) { + states[i] = we.getSession(p).hasSuperPickAxe(); + i++; + } + return states; + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public Class[] acceptChange(ChangeMode mode) { + if (mode == ChangeMode.SET) { + return CollectionUtils.array(Boolean.class); + } + return null; + } + + @Override + public void change(Event e, @Nullable Object[] delta, ChangeMode mode) { + if (mode == ChangeMode.SET) { + WorldEditPlugin we = (WorldEditPlugin) Bukkit.getPluginManager().getPlugin("WorldEdit"); + Player[] ps = players.getAll(e); + boolean enablePick = (boolean) delta[0]; + for (Player p : ps) { + if (enablePick) { + we.getSession(p).enableSuperPickAxe(); + } else { + we.getSession(p).disableSuperPickAxe(); + } + } + } + } +} \ No newline at end of file diff --git a/src/me/TheBukor/SkStuff/expressions/ExprWGMemberOwner.java b/src/me/TheBukor/SkStuff/expressions/ExprWGMemberOwner.java new file mode 100644 index 0000000..928651a --- /dev/null +++ b/src/me/TheBukor/SkStuff/expressions/ExprWGMemberOwner.java @@ -0,0 +1,111 @@ +package me.TheBukor.SkStuff.expressions; + +import java.util.Arrays; +import java.util.Set; +import java.util.UUID; + +import javax.annotation.Nullable; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.event.Event; + +import com.sk89q.worldguard.domains.DefaultDomain; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; + +public class ExprWGMemberOwner extends SimpleExpression { + private Expression region; + + private int mark; + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + return OfflinePlayer.class; + } + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] expr, int matchedPattern, Kleenean isDelayed, ParseResult result) { + region = (Expression) expr[0]; + mark = result.mark; + return true; + } + + @Override + public String toString(@Nullable Event e, boolean debug) { + String markString = mark == 0 ? "members" : "owners"; + return "the " + markString + " of the worldguard region " + region.toString(e, debug); + } + + @Override + @Nullable + protected OfflinePlayer[] get(Event e) { + ProtectedRegion reg = region.getSingle(e); + Set uuids; + if (mark == 0) { + uuids = reg.getMembers().getUniqueIds(); + } else { + uuids = reg.getOwners().getUniqueIds(); + } + if (uuids.isEmpty()) { + return null; + } + OfflinePlayer[] offPlayers = new OfflinePlayer[uuids.size()]; + int i = 0; + for (UUID uuid : uuids) { + offPlayers[i] = Bukkit.getOfflinePlayer(uuid); + i++; + } + return offPlayers; + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public Class[] acceptChange(ChangeMode mode) { + if (mode == ChangeMode.ADD || mode == ChangeMode.REMOVE) { + return CollectionUtils.array(OfflinePlayer[].class); + } + return null; + } + + @Override + public void change(Event e, @Nullable Object[] delta, ChangeMode mode) { + ProtectedRegion reg = region.getSingle(e); + if (mode == ChangeMode.ADD) { + OfflinePlayer[] toAdd = Arrays.copyOf(delta, delta.length, OfflinePlayer[].class); + for (OfflinePlayer offPlayer : toAdd) { + DefaultDomain domain; + if (mark == 0) { + domain = reg.getMembers(); + } else { + domain = reg.getOwners(); + } + domain.addPlayer(offPlayer.getUniqueId()); + } + } else if (mode == ChangeMode.REMOVE) { + OfflinePlayer[] toRemove = Arrays.copyOf(delta, delta.length, OfflinePlayer[].class); + for (OfflinePlayer offPlayer : toRemove) { + DefaultDomain domain; + if (mark == 0) { + domain = reg.getMembers(); + } else { + domain = reg.getOwners(); + } + domain.removePlayer(offPlayer.getUniqueId()); + } + } + } +} diff --git a/src/org/mcstats/Metrics.java b/src/org/mcstats/Metrics.java new file mode 100644 index 0000000..73aa145 --- /dev/null +++ b/src/org/mcstats/Metrics.java @@ -0,0 +1,785 @@ +/* + * Copyright 2011-2013 Tyler Blair. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and contributors and should not be interpreted as representing official policies, + * either expressed or implied, of anybody else. + */ +package org.mcstats; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.scheduler.BukkitTask; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + +public class Metrics { + + /** + * The current revision number + */ + private final static int REVISION = 7; + + /** + * The base url of the metrics domain + */ + private static final String BASE_URL = "http://report.mcstats.org"; + + /** + * The url used to report a server's status + */ + private static final String REPORT_URL = "/plugin/%s"; + + /** + * Interval of time to ping (in minutes) + */ + private static final int PING_INTERVAL = 15; + + /** + * The plugin this metrics submits for + */ + private final Plugin plugin; + + /** + * All of the custom graphs to submit to metrics + */ + private final Set graphs = Collections.synchronizedSet(new HashSet()); + + /** + * The plugin configuration file + */ + private final YamlConfiguration configuration; + + /** + * The plugin configuration file + */ + private final File configurationFile; + + /** + * Unique server id + */ + private final String guid; + + /** + * Debug mode + */ + private final boolean debug; + + /** + * Lock for synchronization + */ + private final Object optOutLock = new Object(); + + /** + * The scheduled task + */ + private volatile BukkitTask task = null; + + public Metrics(final Plugin plugin) throws IOException { + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null"); + } + + this.plugin = plugin; + + // load the config + configurationFile = getConfigFile(); + configuration = YamlConfiguration.loadConfiguration(configurationFile); + + // add some defaults + configuration.addDefault("opt-out", false); + configuration.addDefault("guid", UUID.randomUUID().toString()); + configuration.addDefault("debug", false); + + // Do we need to create the file? + if (configuration.get("guid", null) == null) { + configuration.options().header("http://mcstats.org").copyDefaults(true); + configuration.save(configurationFile); + } + + // Load the guid then + guid = configuration.getString("guid"); + debug = configuration.getBoolean("debug", false); + } + + /** + * Construct and create a Graph that can be used to separate specific plotters to their own graphs on the metrics + * website. Plotters can be added to the graph object returned. + * + * @param name The name of the graph + * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given + */ + public Graph createGraph(final String name) { + if (name == null) { + throw new IllegalArgumentException("Graph name cannot be null"); + } + + // Construct the graph object + final Graph graph = new Graph(name); + + // Now we can add our graph + graphs.add(graph); + + // and return back + return graph; + } + + /** + * Add a Graph object to BukkitMetrics that represents data for the plugin that should be sent to the backend + * + * @param graph The name of the graph + */ + public void addGraph(final Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + + graphs.add(graph); + } + + /** + * Start measuring statistics. This will immediately create an async repeating task as the plugin and send the + * initial data to the metrics backend, and then after that it will post in increments of PING_INTERVAL * 1200 + * ticks. + * + * @return True if statistics measuring is running, otherwise false. + */ + public boolean start() { + synchronized (optOutLock) { + // Did we opt out? + if (isOptOut()) { + return false; + } + + // Is metrics already running? + if (task != null) { + return true; + } + + // Begin hitting the server with glorious data + task = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() { + + private boolean firstPost = true; + + public void run() { + try { + // This has to be synchronized or it can collide with the disable method. + synchronized (optOutLock) { + // Disable Task, if it is running and the server owner decided to opt-out + if (isOptOut() && task != null) { + task.cancel(); + task = null; + // Tell all plotters to stop gathering information. + for (Graph graph : graphs) { + graph.onOptOut(); + } + } + } + + // We use the inverse of firstPost because if it is the first time we are posting, + // it is not a interval ping, so it evaluates to FALSE + // Each time thereafter it will evaluate to TRUE, i.e PING! + postPlugin(!firstPost); + + // After the first post we set firstPost to false + // Each post thereafter will be a ping + firstPost = false; + } catch (IOException e) { + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage()); + } + } + } + }, 0, PING_INTERVAL * 1200); + + return true; + } + } + + /** + * Has the server owner denied plugin metrics? + * + * @return true if metrics should be opted out of it + */ + public boolean isOptOut() { + synchronized (optOutLock) { + try { + // Reload the metrics file + configuration.load(getConfigFile()); + } catch (IOException ex) { + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + } + return true; + } catch (InvalidConfigurationException ex) { + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + } + return true; + } + return configuration.getBoolean("opt-out", false); + } + } + + /** + * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. + * + * @throws java.io.IOException + */ + public void enable() throws IOException { + // This has to be synchronized or it can collide with the check in the task. + synchronized (optOutLock) { + // Check if the server owner has already set opt-out, if not, set it. + if (isOptOut()) { + configuration.set("opt-out", false); + configuration.save(configurationFile); + } + + // Enable Task, if it is not running + if (task == null) { + start(); + } + } + } + + /** + * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task. + * + * @throws java.io.IOException + */ + public void disable() throws IOException { + // This has to be synchronized or it can collide with the check in the task. + synchronized (optOutLock) { + // Check if the server owner has already set opt-out, if not, set it. + if (!isOptOut()) { + configuration.set("opt-out", true); + configuration.save(configurationFile); + } + + // Disable Task, if it is running + if (task != null) { + task.cancel(); + task = null; + } + } + } + + /** + * Gets the File object of the config file that should be used to store data such as the GUID and opt-out status + * + * @return the File object for the config file + */ + public File getConfigFile() { + // I believe the easiest way to get the base folder (e.g craftbukkit set via -P) for plugins to use + // is to abuse the plugin object we already have + // plugin.getDataFolder() => base/plugins/PluginA/ + // pluginsFolder => base/plugins/ + // The base is not necessarily relative to the startup directory. + File pluginsFolder = plugin.getDataFolder().getParentFile(); + + // return => base/plugins/PluginMetrics/config.yml + return new File(new File(pluginsFolder, "PluginMetrics"), "config.yml"); + } + + /** + * Gets the online player (backwards compatibility) + * + * @return online player amount + */ + private int getOnlinePlayers() { + try { + Method onlinePlayerMethod = Server.class.getMethod("getOnlinePlayers"); + if(onlinePlayerMethod.getReturnType().equals(Collection.class)) { + return ((Collection)onlinePlayerMethod.invoke(Bukkit.getServer())).size(); + } else { + return ((Player[])onlinePlayerMethod.invoke(Bukkit.getServer())).length; + } + } catch (Exception ex) { + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + } + } + + return 0; + } + + /** + * Generic method that posts a plugin to the metrics website + */ + private void postPlugin(final boolean isPing) throws IOException { + // Server software specific section + PluginDescriptionFile description = plugin.getDescription(); + String pluginName = description.getName(); + boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE if online mode is enabled + String pluginVersion = description.getVersion(); + String serverVersion = Bukkit.getVersion(); + int playersOnline = this.getOnlinePlayers(); + + // END server software specific section -- all code below does not use any code outside of this class / Java + + // Construct the post data + StringBuilder json = new StringBuilder(1024); + json.append('{'); + + // The plugin's description file containg all of the plugin data such as name, version, author, etc + appendJSONPair(json, "guid", guid); + appendJSONPair(json, "plugin_version", pluginVersion); + appendJSONPair(json, "server_version", serverVersion); + appendJSONPair(json, "players_online", Integer.toString(playersOnline)); + + // New data as of R6 + String osname = System.getProperty("os.name"); + String osarch = System.getProperty("os.arch"); + String osversion = System.getProperty("os.version"); + String java_version = System.getProperty("java.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + // normalize os arch .. amd64 -> x86_64 + if (osarch.equals("amd64")) { + osarch = "x86_64"; + } + + appendJSONPair(json, "osname", osname); + appendJSONPair(json, "osarch", osarch); + appendJSONPair(json, "osversion", osversion); + appendJSONPair(json, "cores", Integer.toString(coreCount)); + appendJSONPair(json, "auth_mode", onlineMode ? "1" : "0"); + appendJSONPair(json, "java_version", java_version); + + // If we're pinging, append it + if (isPing) { + appendJSONPair(json, "ping", "1"); + } + + if (graphs.size() > 0) { + synchronized (graphs) { + json.append(','); + json.append('"'); + json.append("graphs"); + json.append('"'); + json.append(':'); + json.append('{'); + + boolean firstGraph = true; + + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + Graph graph = iter.next(); + + StringBuilder graphJson = new StringBuilder(); + graphJson.append('{'); + + for (Plotter plotter : graph.getPlotters()) { + appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue())); + } + + graphJson.append('}'); + + if (!firstGraph) { + json.append(','); + } + + json.append(escapeJSON(graph.getName())); + json.append(':'); + json.append(graphJson); + + firstGraph = false; + } + + json.append('}'); + } + } + + // close json + json.append('}'); + + // Create the url + URL url = new URL(BASE_URL + String.format(REPORT_URL, urlEncode(pluginName))); + + // Connect to the website + URLConnection connection; + + // Mineshafter creates a socks proxy, so we can safely bypass it + // It does not reroute POST requests so we need to go around it + if (isMineshafterPresent()) { + connection = url.openConnection(Proxy.NO_PROXY); + } else { + connection = url.openConnection(); + } + + + byte[] uncompressed = json.toString().getBytes(); + byte[] compressed = gzip(json.toString()); + + // Headers + connection.addRequestProperty("User-Agent", "MCStats/" + REVISION); + connection.addRequestProperty("Content-Type", "application/json"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", Integer.toString(compressed.length)); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + + connection.setDoOutput(true); + + if (debug) { + System.out.println("[Metrics] Prepared request for " + pluginName + " uncompressed=" + uncompressed.length + " compressed=" + compressed.length); + } + + // Write the data + OutputStream os = connection.getOutputStream(); + os.write(compressed); + os.flush(); + + // Now read the response + final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String response = reader.readLine(); + + // close resources + os.close(); + reader.close(); + + if (response == null || response.startsWith("ERR") || response.startsWith("7")) { + if (response == null) { + response = "null"; + } else if (response.startsWith("7")) { + response = response.substring(response.startsWith("7,") ? 2 : 1); + } + + throw new IOException(response); + } else { + // Is this the first update this hour? + if (response.equals("1") || response.contains("This is your first update this hour")) { + synchronized (graphs) { + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + final Graph graph = iter.next(); + + for (Plotter plotter : graph.getPlotters()) { + plotter.reset(); + } + } + } + } + } + } + + /** + * GZip compress a string of bytes + * + * @param input + * @return + */ + public static byte[] gzip(String input) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzos = null; + + try { + gzos = new GZIPOutputStream(baos); + gzos.write(input.getBytes("UTF-8")); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (gzos != null) try { + gzos.close(); + } catch (IOException ignore) { + } + } + + return baos.toByteArray(); + } + + /** + * Check if mineshafter is present. If it is, we need to bypass it to send POST requests + * + * @return true if mineshafter is installed on the server + */ + private boolean isMineshafterPresent() { + try { + Class.forName("mineshafter.MineServer"); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Appends a json encoded key/value pair to the given string builder. + * + * @param json + * @param key + * @param value + * @throws UnsupportedEncodingException + */ + private static void appendJSONPair(StringBuilder json, String key, String value) throws UnsupportedEncodingException { + boolean isValueNumeric = false; + + try { + if (value.equals("0") || !value.endsWith("0")) { + Double.parseDouble(value); + isValueNumeric = true; + } + } catch (NumberFormatException e) { + isValueNumeric = false; + } + + if (json.charAt(json.length() - 1) != '{') { + json.append(','); + } + + json.append(escapeJSON(key)); + json.append(':'); + + if (isValueNumeric) { + json.append(value); + } else { + json.append(escapeJSON(value)); + } + } + + /** + * Escape a string to create a valid JSON string + * + * @param text + * @return + */ + private static String escapeJSON(String text) { + StringBuilder builder = new StringBuilder(); + + builder.append('"'); + for (int index = 0; index < text.length(); index++) { + char chr = text.charAt(index); + + switch (chr) { + case '"': + case '\\': + builder.append('\\'); + builder.append(chr); + break; + case '\b': + builder.append("\\b"); + break; + case '\t': + builder.append("\\t"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + default: + if (chr < ' ') { + String t = "000" + Integer.toHexString(chr); + builder.append("\\u" + t.substring(t.length() - 4)); + } else { + builder.append(chr); + } + break; + } + } + builder.append('"'); + + return builder.toString(); + } + + /** + * Encode text as UTF-8 + * + * @param text the text to encode + * @return the encoded text, as UTF-8 + */ + private static String urlEncode(final String text) throws UnsupportedEncodingException { + return URLEncoder.encode(text, "UTF-8"); + } + + /** + * Represents a custom graph on the website + */ + public static class Graph { + + /** + * The graph's name, alphanumeric and spaces only :) If it does not comply to the above when submitted, it is + * rejected + */ + private final String name; + + /** + * The set of plotters that are contained within this graph + */ + private final Set plotters = new LinkedHashSet(); + + private Graph(final String name) { + this.name = name; + } + + /** + * Gets the graph's name + * + * @return the Graph's name + */ + public String getName() { + return name; + } + + /** + * Add a plotter to the graph, which will be used to plot entries + * + * @param plotter the plotter to add to the graph + */ + public void addPlotter(final Plotter plotter) { + plotters.add(plotter); + } + + /** + * Remove a plotter from the graph + * + * @param plotter the plotter to remove from the graph + */ + public void removePlotter(final Plotter plotter) { + plotters.remove(plotter); + } + + /** + * Gets an unmodifiable set of the plotter objects in the graph + * + * @return an unmodifiable {@link java.util.Set} of the plotter objects + */ + public Set getPlotters() { + return Collections.unmodifiableSet(plotters); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Graph)) { + return false; + } + + final Graph graph = (Graph) object; + return graph.name.equals(name); + } + + /** + * Called when the server owner decides to opt-out of BukkitMetrics while the server is running. + */ + protected void onOptOut() { + } + } + + /** + * Interface used to collect custom data for a plugin + */ + public static abstract class Plotter { + + /** + * The plot's name + */ + private final String name; + + /** + * Construct a plotter with the default plot name + */ + public Plotter() { + this("Default"); + } + + /** + * Construct a plotter with a specific plot name + * + * @param name the name of the plotter to use, which will show up on the website + */ + public Plotter(final String name) { + this.name = name; + } + + /** + * Get the current value for the plotted point. Since this function defers to an external function it may or may + * not return immediately thus cannot be guaranteed to be thread friendly or safe. This function can be called + * from any thread so care should be taken when accessing resources that need to be synchronized. + * + * @return the current value for the point to be plotted. + */ + public abstract int getValue(); + + /** + * Get the column name for the plotted point + * + * @return the plotted point's column name + */ + public String getColumnName() { + return name; + } + + /** + * Called after the website graphs have been updated + */ + public void reset() { + } + + @Override + public int hashCode() { + return getColumnName().hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Plotter)) { + return false; + } + + final Plotter plotter = (Plotter) object; + return plotter.name.equals(name) && plotter.getValue() == getValue(); + } + } +} \ No newline at end of file