diff --git a/.gitignore b/.gitignore index 05a8456..9684bcc 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ local.properties .buildpath # IntelliJ IDEA -*.iml \ No newline at end of file +*.iml +.idea +dependency-reduced-pom.xml \ No newline at end of file diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/AreaShop.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/AreaShop.java index bc6c22c..8418243 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/AreaShop.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/AreaShop.java @@ -3,8 +3,6 @@ package nl.evolutioncoding.areashop; import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldguard.bukkit.WorldGuardPlugin; import net.milkbowl.vault.economy.Economy; -import nl.evolutioncoding.areashop.Updater.UpdateResult; -import nl.evolutioncoding.areashop.Updater.UpdateType; import nl.evolutioncoding.areashop.features.DebugFeature; import nl.evolutioncoding.areashop.features.Feature; import nl.evolutioncoding.areashop.features.SignDisplayFeature; @@ -12,17 +10,21 @@ import nl.evolutioncoding.areashop.features.WorldGuardRegionFlagsFeature; import nl.evolutioncoding.areashop.interfaces.AreaShopInterface; import nl.evolutioncoding.areashop.interfaces.WorldEditInterface; import nl.evolutioncoding.areashop.interfaces.WorldGuardInterface; +import nl.evolutioncoding.areashop.lib.Metrics; +import nl.evolutioncoding.areashop.lib.Updater; +import nl.evolutioncoding.areashop.lib.Updater.UpdateResult; +import nl.evolutioncoding.areashop.lib.Updater.UpdateType; import nl.evolutioncoding.areashop.listeners.PlayerLoginLogoutListener; import nl.evolutioncoding.areashop.listeners.SignBreakListener; import nl.evolutioncoding.areashop.listeners.SignChangeListener; import nl.evolutioncoding.areashop.listeners.SignClickListener; import nl.evolutioncoding.areashop.managers.CommandManager; import nl.evolutioncoding.areashop.managers.FileManager; -import nl.evolutioncoding.areashop.managers.LanguageManager; import nl.evolutioncoding.areashop.managers.SignLinkerManager; +import nl.evolutioncoding.areashop.messages.LanguageManager; +import nl.evolutioncoding.areashop.messages.Message; import nl.evolutioncoding.areashop.regions.GeneralRegion; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; @@ -35,8 +37,8 @@ import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitRunnable; import java.util.HashSet; +import java.util.List; import java.util.Set; -import java.util.logging.Logger; /** * Main class for the AreaShop plugin @@ -55,7 +57,7 @@ public final class AreaShop extends JavaPlugin implements AreaShopInterface { private CommandManager commandManager = null; private SignLinkerManager signLinkerManager = null; private boolean debug = false; - private String chatprefix = null; + private List chatprefix = null; private Updater updater = null; private boolean updateAvailable = false; private boolean ready = false; @@ -335,7 +337,7 @@ public final class AreaShop extends JavaPlugin implements AreaShopInterface { * Set the chatprefix to use in the chat (loaded from config normally) * @param chatprefix The string to use in front of chat messages (supports formatting codes like &1) */ - public void setChatprefix(String chatprefix) { + public void setChatprefix(List chatprefix) { this.chatprefix = chatprefix; } @@ -408,7 +410,7 @@ public final class AreaShop extends JavaPlugin implements AreaShopInterface { * Get the current chatPrefix * @return The current chatPrefix */ - public String getChatPrefix() { + public List getChatPrefix() { return chatprefix; } @@ -549,50 +551,13 @@ public final class AreaShop extends JavaPlugin implements AreaShopInterface { }.runTaskLater(this, 20L); } } - - /** - * Method to send a message to a CommandSender, using chatprefix if it is a player - * @param target The CommandSender you wan't to send the message to (e.g. a player) - * @param key The key to get the translation - * @param prefix Specify if the message should have a prefix - * @param params The parameters to inject into the message string - */ - public void configurableMessage(Object target, String key, boolean prefix, Object... params) { - if(target == null) { - return; - } - String langString = Utils.applyColors(languageManager.getLang(key, params)); - if(langString == null || langString.equals("")) { - // Do nothing, message is not available or disabled - } else { - if(target instanceof Player) { - if(prefix) { - ((Player)target).sendMessage(Utils.applyColors(chatprefix)+langString); - } else { - ((Player)target).sendMessage(langString); - } - } else if(target instanceof CommandSender) { - if(!getConfig().getBoolean("useColorsInConsole")) { - langString = ChatColor.stripColor(langString); - } - ((CommandSender)target).sendMessage(langString); - } - else if(target instanceof Logger) { - if(!getConfig().getBoolean("useColorsInConsole")) { - langString = ChatColor.stripColor(langString); - } - ((Logger)target).info(langString); - } else { - langString = ChatColor.stripColor(langString); - this.getLogger().info("Could not send message, target is wrong: " + langString); - } - } + + public void messageNoPrefix(Object target, String key, Object... replacements) { + Message.fromKey(key).replacements(replacements).send(target); } - public void messageNoPrefix(Object target, String key, Object... params) { - configurableMessage(target, key, false, params); - } - public void message(Object target, String key, Object... params) { - configurableMessage(target, key, true, params); + + public void message(Object target, String key, Object... replacements) { + Message.fromKey(key).prefix().replacements(replacements).send(target); } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/Utils.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/Utils.java index 59cebd0..9691190 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/Utils.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/Utils.java @@ -5,6 +5,7 @@ import com.sk89q.worldedit.bukkit.selections.CuboidSelection; import com.sk89q.worldedit.bukkit.selections.Selection; import com.sk89q.worldguard.protection.managers.RegionManager; import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import nl.evolutioncoding.areashop.messages.Message; import nl.evolutioncoding.areashop.regions.BuyRegion; import nl.evolutioncoding.areashop.regions.GeneralRegion; import nl.evolutioncoding.areashop.regions.GeneralRegion.RegionType; @@ -157,6 +158,47 @@ public class Utils { return milliseconds/50; } + /** + * Convert milliseconds to a human readable format + * @param milliseconds The amount of milliseconds to convert + * @return A formatted string based on the language file + */ + public static String millisToHumanFormat(long milliseconds) { + long timeLeft = milliseconds+500; + // To seconds + timeLeft = timeLeft/1000; + if(timeLeft <= 0) { + return Message.fromKey("timeleft-ended").getPlain(); + } else if(timeLeft == 1) { + return Message.fromKey("timeleft-second").replacements(timeLeft).getPlain(); + } else if(timeLeft <= 120) { + return Message.fromKey("timeleft-seconds").replacements(timeLeft).getPlain(); + } + // To minutes + timeLeft = timeLeft/60; + if(timeLeft <= 120) { + return Message.fromKey("timeleft-minutes").replacements(timeLeft).getPlain(); + } + // To hours + timeLeft = timeLeft/60; + if(timeLeft <= 48) { + return Message.fromKey("timeleft-hours").replacements(timeLeft).getPlain(); + } + // To days + timeLeft = timeLeft/24; + if(timeLeft <= 60) { + return Message.fromKey("timeleft-days").replacements(timeLeft).getPlain(); + } + // To months + timeLeft = timeLeft/30; + if(timeLeft <= 24) { + return Message.fromKey("timeleft-months").replacements(timeLeft).getPlain(); + } + // To years + timeLeft = timeLeft/12; + return Message.fromKey("timeleft-years").replacements(timeLeft).getPlain(); + } + private static final BlockFace[] facings = {BlockFace.NORTH, BlockFace.NORTH_EAST, BlockFace.EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH, BlockFace.SOUTH_WEST, BlockFace.WEST, BlockFace.NORTH_WEST}; /** diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddCommand.java index 766bdb1..27b3938 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddCommand.java @@ -32,7 +32,7 @@ public class AddCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.createrent") || target.hasPermission("areashop.createbuy")) { - return plugin.getLanguageManager().getLang("help-add"); + return "help-add"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddfriendCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddfriendCommand.java index bb26eee..4d10635 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddfriendCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddfriendCommand.java @@ -27,9 +27,9 @@ public class AddfriendCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.addfriendall")) { - return plugin.getLanguageManager().getLang("help-addFriendAll"); + return "help-addFriendAll"; } else if(target.hasPermission("areashop.addfriend")) { - return plugin.getLanguageManager().getLang("help-addFriend"); + return "help-addFriend"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddsignCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddsignCommand.java index 4088a7f..65b947a 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddsignCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/AddsignCommand.java @@ -29,7 +29,7 @@ public class AddsignCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.addsign")) { - return plugin.getLanguageManager().getLang("help-addsign"); + return "help-addsign"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/BuyCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/BuyCommand.java index 05dff5e..161a9b9 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/BuyCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/BuyCommand.java @@ -23,7 +23,7 @@ public class BuyCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.buy")) { - return plugin.getLanguageManager().getLang("help-buy"); + return "help-buy"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/CommandAreaShop.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/CommandAreaShop.java index 6a7c275..0074dcf 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/CommandAreaShop.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/CommandAreaShop.java @@ -49,9 +49,9 @@ public abstract class CommandAreaShop { public abstract String getCommandStart(); /** - * Returns the correct help string for the reciever + * Returns the correct help string key to be used on the help page * @param target The CommandSender that the help message is for - * @return The help message according to the permissions of the reciever + * @return The help message key according to the permissions of the reciever */ public abstract String getHelp(CommandSender target); diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelCommand.java index 8f82c23..62b3024 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelCommand.java @@ -28,7 +28,7 @@ public class DelCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.destroyrent") || target.hasPermission("areashop.destroybuy")) { - return plugin.getLanguageManager().getLang("help-del"); + return "help-del"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelfriendCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelfriendCommand.java index 6123eb5..96206fd 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelfriendCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelfriendCommand.java @@ -27,9 +27,9 @@ public class DelfriendCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.delfriendall")) { - return plugin.getLanguageManager().getLang("help-delFriendAll"); + return "help-delFriendAll"; } else if(target.hasPermission("areashop.delfriend")) { - return plugin.getLanguageManager().getLang("help-delFriend"); + return "help-delFriend"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelsignCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelsignCommand.java index c1b806f..e8a4081 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelsignCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/DelsignCommand.java @@ -25,7 +25,7 @@ public class DelsignCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.delsign")) { - return plugin.getLanguageManager().getLang("help-delsign"); + return "help-delsign"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/FindCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/FindCommand.java index a909f65..836575a 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/FindCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/FindCommand.java @@ -2,6 +2,7 @@ package nl.evolutioncoding.areashop.commands; import nl.evolutioncoding.areashop.AreaShop; import nl.evolutioncoding.areashop.Utils; +import nl.evolutioncoding.areashop.messages.Message; import nl.evolutioncoding.areashop.regions.BuyRegion; import nl.evolutioncoding.areashop.regions.RegionGroup; import nl.evolutioncoding.areashop.regions.RentRegion; @@ -26,7 +27,7 @@ public class FindCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.find")) { - return plugin.getLanguageManager().getLang("help-find"); + return "help-find"; } return null; } @@ -84,9 +85,9 @@ public class FindCommand extends CommandAreaShop { if(!results.isEmpty()) { // Draw a random one BuyRegion region = results.get(new Random().nextInt(results.size())); - String onlyInGroup = ""; + Message onlyInGroup = Message.none(); if(group != null) { - onlyInGroup = plugin.getLanguageManager().getLang("find-onlyInGroup", args[3]); + onlyInGroup = Message.fromKey("find-onlyInGroup").replacements(args[3]); } if(maxPriceSet) { plugin.message(player, "find-successMax", "buy", Utils.formatCurrency(maxPrice), onlyInGroup, region); @@ -95,9 +96,9 @@ public class FindCommand extends CommandAreaShop { } region.teleportPlayer(player, region.getBooleanSetting("general.findTeleportToSign"), false); } else { - String onlyInGroup = ""; + Message onlyInGroup = Message.none(); if(group != null) { - onlyInGroup = plugin.getLanguageManager().getLang("find-onlyInGroup", args[3]); + onlyInGroup = Message.fromKey("find-onlyInGroup").replacements(args[3]); } if(maxPriceSet) { plugin.message(player, "find-noneFoundMax", "buy", Utils.formatCurrency(maxPrice), onlyInGroup); @@ -118,9 +119,9 @@ public class FindCommand extends CommandAreaShop { if(!results.isEmpty()) { // Draw a random one RentRegion region = results.get(new Random().nextInt(results.size())); - String onlyInGroup = ""; + Message onlyInGroup = Message.none(); if(group != null) { - onlyInGroup = plugin.getLanguageManager().getLang("find-onlyInGroup", args[3]); + onlyInGroup = Message.fromKey("find-onlyInGroup").replacements(args[3]); } if(maxPriceSet) { plugin.message(player, "find-successMax", "rent", Utils.formatCurrency(maxPrice), onlyInGroup, region); @@ -129,9 +130,9 @@ public class FindCommand extends CommandAreaShop { } region.teleportPlayer(player, region.getBooleanSetting("general.findTeleportToSign"), false); } else { - String onlyInGroup = ""; + Message onlyInGroup = Message.none(); if(group != null) { - onlyInGroup = plugin.getLanguageManager().getLang("find-onlyInGroup", args[3]); + onlyInGroup = Message.fromKey("find-onlyInGroup").replacements(args[3]); } if(maxPriceSet) { plugin.message(player, "find-noneFoundMax", "rent", Utils.formatCurrency(maxPrice), onlyInGroup); diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupaddCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupaddCommand.java index 2b7d348..c21a23b 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupaddCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupaddCommand.java @@ -25,7 +25,7 @@ public class GroupaddCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.groupadd")) { - return plugin.getLanguageManager().getLang("help-groupadd"); + return "help-groupadd"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupdelCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupdelCommand.java index 2437abd..74fef5e 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupdelCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupdelCommand.java @@ -25,7 +25,7 @@ public class GroupdelCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.groupdel")) { - return plugin.getLanguageManager().getLang("help-groupdel"); + return "help-groupdel"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupinfoCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupinfoCommand.java index a1f8845..ffe3346 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupinfoCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GroupinfoCommand.java @@ -22,7 +22,7 @@ public class GroupinfoCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.groupinfo")) { - return plugin.getLanguageManager().getLang("help-groupinfo"); + return "help-groupinfo"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GrouplistCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GrouplistCommand.java index 4fd8c2d..ab2457c 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GrouplistCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/GrouplistCommand.java @@ -21,7 +21,7 @@ public class GrouplistCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.grouplist")) { - return plugin.getLanguageManager().getLang("help-grouplist"); + return "help-grouplist"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/HelpCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/HelpCommand.java index 93aac98..79eceae 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/HelpCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/HelpCommand.java @@ -17,7 +17,7 @@ public class HelpCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.help")) { - return plugin.getLanguageManager().getLang("help-help"); + return "help-help"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/InfoCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/InfoCommand.java index a76804b..e9f9e8a 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/InfoCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/InfoCommand.java @@ -2,6 +2,7 @@ package nl.evolutioncoding.areashop.commands; import nl.evolutioncoding.areashop.AreaShop; import nl.evolutioncoding.areashop.Utils; +import nl.evolutioncoding.areashop.messages.Message; import nl.evolutioncoding.areashop.regions.BuyRegion; import nl.evolutioncoding.areashop.regions.GeneralRegion; import nl.evolutioncoding.areashop.regions.RegionGroup; @@ -27,7 +28,7 @@ public class InfoCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.info")) { - return plugin.getLanguageManager().getLang("help-info"); + return "help-info"; } return null; } @@ -225,7 +226,7 @@ public class InfoCommand extends CommandAreaShop { Location teleport = rent.getTeleportLocation(); if(teleport == null) { if(rent.isRented()) { - plugin.messageNoPrefix(sender, "info-regionNoTeleport", rent, plugin.getLanguageManager().getLang("info-regionTeleportHint", rent)); + plugin.messageNoPrefix(sender, "info-regionNoTeleport", rent, Message.fromKey("info-regionTeleportHint").replacements(rent)); } else { plugin.messageNoPrefix(sender, "info-regionNoTeleport", rent, ""); } @@ -235,17 +236,17 @@ public class InfoCommand extends CommandAreaShop { } List signLocations = new ArrayList<>(); for(Location location : rent.getSignLocations()) { - signLocations.add(plugin.getLanguageManager().getLang("info-regionSignLocation", location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ())); + signLocations.add(Message.fromKey("info-regionSignLocation").replacements(location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ()).getPlain()); } if(!signLocations.isEmpty()) { - plugin.messageNoPrefix(sender, "info-regionSigns", Utils.createCommaSeparatedList(signLocations)); + plugin.messageNoPrefix(sender, "info-regionSigns", signLocations.toArray()); } if(sender.hasPermission("areashop.groupinfo") && !rent.getGroupNames().isEmpty()) { plugin.messageNoPrefix(sender, "info-regionGroups", Utils.createCommaSeparatedList(rent.getGroupNames())); } if(rent.isRestoreEnabled()) { if(sender.hasPermission("areashop.setrestore")) { - plugin.messageNoPrefix(sender, "info-regionRestoringRent", rent, plugin.getLanguageManager().getLang("info-regionRestoringProfile", rent.getRestoreProfile())); + plugin.messageNoPrefix(sender, "info-regionRestoringRent", rent, Message.fromKey("info-regionRestoringProfile").replacements(rent.getRestoreProfile())); } else { plugin.messageNoPrefix(sender, "info-regionRestoringRent", rent, ""); } @@ -284,7 +285,7 @@ public class InfoCommand extends CommandAreaShop { Location teleport = buy.getTeleportLocation(); if(teleport == null) { if(buy.isSold()) { - plugin.messageNoPrefix(sender, "info-regionNoTeleport", buy, plugin.getLanguageManager().getLang("info-regionTeleportHint", buy)); + plugin.messageNoPrefix(sender, "info-regionNoTeleport", buy, Message.fromKey("info-regionTeleportHint").replacements(buy)); } else { plugin.messageNoPrefix(sender, "info-regionNoTeleport", buy, ""); } @@ -294,17 +295,17 @@ public class InfoCommand extends CommandAreaShop { } List signLocations = new ArrayList<>(); for(Location location : buy.getSignLocations()) { - signLocations.add(plugin.getLanguageManager().getLang("info-regionSignLocation", location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ())); + signLocations.add(Message.fromKey("info-regionSignLocation").replacements(location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ()).getPlain()); } if(!signLocations.isEmpty()) { - plugin.messageNoPrefix(sender, "info-regionSigns", Utils.createCommaSeparatedList(signLocations)); + plugin.messageNoPrefix(sender, "info-regionSigns", signLocations.toArray()); } if(sender.hasPermission("areashop.groupinfo") && !buy.getGroupNames().isEmpty()) { plugin.messageNoPrefix(sender, "info-regionGroups", Utils.createCommaSeparatedList(buy.getGroupNames())); } if(buy.isRestoreEnabled()) { if(sender.hasPermission("areashop.setrestore")) { - plugin.messageNoPrefix(sender, "info-regionRestoringBuy", buy, plugin.getLanguageManager().getLang("info-regionRestoringProfile", buy.getRestoreProfile())); + plugin.messageNoPrefix(sender, "info-regionRestoringBuy", buy, Message.fromKey("info-regionRestoringProfile").replacements(buy.getRestoreProfile())); } else { plugin.messageNoPrefix(sender, "info-regionRestoringBuy", buy, ""); } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/LinksignsCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/LinksignsCommand.java index 095c2f1..d5e6342 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/LinksignsCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/LinksignsCommand.java @@ -23,7 +23,7 @@ public class LinksignsCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.linksigns")) { - return plugin.getLanguageManager().getLang("help-linksigns"); + return "help-linksigns"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/MeCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/MeCommand.java index 22c372f..c822cbe 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/MeCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/MeCommand.java @@ -24,7 +24,7 @@ public class MeCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.me")) { - return plugin.getLanguageManager().getLang("help-me"); + return "help-me"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/ReloadCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/ReloadCommand.java index 3016dec..6e87428 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/ReloadCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/ReloadCommand.java @@ -17,7 +17,7 @@ public class ReloadCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.reload")) { - return plugin.getLanguageManager().getLang("help-reload"); + return "help-reload"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/RentCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/RentCommand.java index d4a33f0..b18c884 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/RentCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/RentCommand.java @@ -23,7 +23,7 @@ public class RentCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.rent")) { - return plugin.getLanguageManager().getLang("help-rent"); + return "help-rent"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/ResellCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/ResellCommand.java index 5d232e4..954dbe9 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/ResellCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/ResellCommand.java @@ -23,9 +23,9 @@ public class ResellCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.resellall")) { - return plugin.getLanguageManager().getLang("help-resellAll"); + return "help-resellAll"; } else if(target.hasPermission("areashop.resell")) { - return plugin.getLanguageManager().getLang("help-resell"); + return "help-resell"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SchematiceventCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SchematiceventCommand.java index f1a2127..bd05d5c 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SchematiceventCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SchematiceventCommand.java @@ -24,7 +24,7 @@ public class SchematiceventCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.schematicevents")) { - return plugin.getLanguageManager().getLang("help-schemevent"); + return "help-schemevent"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SellCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SellCommand.java index ba5534f..495fb69 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SellCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SellCommand.java @@ -23,9 +23,9 @@ public class SellCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.sell")) { - return plugin.getLanguageManager().getLang("help-sell"); + return "help-sell"; } else if(target.hasPermission("areashop.sellown")) { - return plugin.getLanguageManager().getLang("help-sellOwn"); + return "help-sellOwn"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetdurationCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetdurationCommand.java index ecba082..54cbad2 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetdurationCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetdurationCommand.java @@ -23,7 +23,7 @@ public class SetdurationCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.setduration")) { - return plugin.getLanguageManager().getLang("help-setduration"); + return "help-setduration"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetlandlordCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetlandlordCommand.java index 6438ba7..cf265ac 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetlandlordCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetlandlordCommand.java @@ -25,7 +25,7 @@ public class SetlandlordCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.setlandlord")) { - return plugin.getLanguageManager().getLang("help-setlandlord"); + return "help-setlandlord"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetownerCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetownerCommand.java index f6a8815..3524d7c 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetownerCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetownerCommand.java @@ -29,7 +29,7 @@ public class SetownerCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.setownerrent") || target.hasPermission("areashop.setownerbuy")) { - return plugin.getLanguageManager().getLang("help-setowner"); + return "help-setowner"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetpriceCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetpriceCommand.java index 9281315..da8fd50 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetpriceCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetpriceCommand.java @@ -25,7 +25,7 @@ public class SetpriceCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.setprice")) { - return plugin.getLanguageManager().getLang("help-setprice"); + return "help-setprice"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetrestoreCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetrestoreCommand.java index aeaa668..ac353b9 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetrestoreCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetrestoreCommand.java @@ -21,7 +21,7 @@ public class SetrestoreCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.setrestore")) { - return plugin.getLanguageManager().getLang("help-setrestore"); + return "help-setrestore"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetteleportCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetteleportCommand.java index 1519b85..f2f358d 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetteleportCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/SetteleportCommand.java @@ -26,9 +26,9 @@ public class SetteleportCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.teleportall")) { - return plugin.getLanguageManager().getLang("help-setteleportAll"); + return "help-setteleportAll"; } else if(target.hasPermission("areashop.teleport")) { - return plugin.getLanguageManager().getLang("help-setteleport"); + return "help-setteleport"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/StackCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/StackCommand.java index 02d235e..f15fd07 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/StackCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/StackCommand.java @@ -6,6 +6,7 @@ import com.sk89q.worldguard.protection.managers.RegionManager; import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; import nl.evolutioncoding.areashop.AreaShop; import nl.evolutioncoding.areashop.Utils; +import nl.evolutioncoding.areashop.messages.Message; import nl.evolutioncoding.areashop.regions.BuyRegion; import nl.evolutioncoding.areashop.regions.GeneralRegion.RegionEvent; import nl.evolutioncoding.areashop.regions.RegionGroup; @@ -33,7 +34,7 @@ public class StackCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.stack")) { - return plugin.getLanguageManager().getLang("help-stack"); + return "help-stack"; } return null; } @@ -124,9 +125,9 @@ public class StackCommand extends CommandAreaShop { } else { type = "buy"; } - String groupsMessage = ""; + Message groupsMessage = Message.none(); if(group != null) { - groupsMessage = plugin.getLanguageManager().getLang("stack-addToGroup", group.getName()); + groupsMessage = Message.fromKey("stack-addToGroup").replacements(group.getName()); } plugin.message(player, "stack-accepted", amount, type, gap, namePrefix, groupsMessage); plugin.message(player, "stack-addStart", amount, regionsPerTick*20); diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/StopresellCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/StopresellCommand.java index 866a6aa..dbb87f1 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/StopresellCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/StopresellCommand.java @@ -23,9 +23,9 @@ public class StopresellCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.stopresellall")) { - return plugin.getLanguageManager().getLang("help-stopResellAll"); + return "help-stopResellAll"; } else if(target.hasPermission("areashop.stopresell")) { - return plugin.getLanguageManager().getLang("help-stopResell"); + return "help-stopResell"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/TeleportCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/TeleportCommand.java index b635b3e..5dc4cb8 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/TeleportCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/TeleportCommand.java @@ -22,9 +22,9 @@ public class TeleportCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.teleportall")) { - return plugin.getLanguageManager().getLang("help-teleportAll"); + return "help-teleportAll"; } else if(target.hasPermission("areashop.teleport")) { - return plugin.getLanguageManager().getLang("help-teleport"); + return "help-teleport"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/UnrentCommand.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/UnrentCommand.java index 86c7dcd..b7f97b1 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/UnrentCommand.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/commands/UnrentCommand.java @@ -23,9 +23,9 @@ public class UnrentCommand extends CommandAreaShop { @Override public String getHelp(CommandSender target) { if(target.hasPermission("areashop.unrent")) { - return plugin.getLanguageManager().getLang("help-unrent"); + return "help-unrent"; } else if(target.hasPermission("areashop.unrentown")) { - return plugin.getLanguageManager().getLang("help-unrentOwn"); + return "help-unrentOwn"; } return null; } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/features/WorldGuardRegionFlagsFeature.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/features/WorldGuardRegionFlagsFeature.java index d1d50ed..7a77cac 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/features/WorldGuardRegionFlagsFeature.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/features/WorldGuardRegionFlagsFeature.java @@ -8,6 +8,7 @@ import com.sk89q.worldguard.protection.flags.RegionGroupFlag; import com.sk89q.worldguard.protection.regions.ProtectedRegion; import nl.evolutioncoding.areashop.AreaShop; import nl.evolutioncoding.areashop.events.notify.RegionUpdateEvent; +import nl.evolutioncoding.areashop.messages.Message; import nl.evolutioncoding.areashop.regions.GeneralRegion; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.event.EventHandler; @@ -49,8 +50,7 @@ public class WorldGuardRegionFlagsFeature extends Feature implements Listener { } // Loop through all flags that are set in the config for(String flagName : flagNames) { - String value = flags.getString(flagName); - value = region.applyAllReplacements(value); + String value = Message.fromString(flags.getString(flagName)).replacements(region).getPlain(); // In the config normal Bukkit color codes are used, those only need to be translated on 5.X WorldGuard versions if(plugin.getWorldGuard().getDescription().getVersion().startsWith("5.")) { value = translateBukkitToWorldGuardColors(value); diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/Metrics.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/lib/Metrics.java similarity index 96% rename from AreaShop/src/main/java/nl/evolutioncoding/areashop/Metrics.java rename to AreaShop/src/main/java/nl/evolutioncoding/areashop/lib/Metrics.java index 316f71c..806975e 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/Metrics.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/lib/Metrics.java @@ -1,743 +1,743 @@ -/* - * 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 nl.evolutioncoding.areashop; - -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.*; -import java.lang.reflect.Method; -import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; -import java.util.*; -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. - */ - 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. - */ - 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 - */ - 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. - */ - 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 - */ - 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(); - } - } +/* + * 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 nl.evolutioncoding.areashop.lib; + +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.*; +import java.lang.reflect.Method; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.*; +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. + */ + 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. + */ + 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 + */ + 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. + */ + 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 + */ + 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 diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/Updater.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/lib/Updater.java similarity index 97% rename from AreaShop/src/main/java/nl/evolutioncoding/areashop/Updater.java rename to AreaShop/src/main/java/nl/evolutioncoding/areashop/lib/Updater.java index f6289cf..3de46a4 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/Updater.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/lib/Updater.java @@ -1,746 +1,747 @@ -package nl.evolutioncoding.areashop; - -import java.io.*; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.util.Enumeration; -import java.util.logging.Level; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import org.apache.commons.lang.exception.ExceptionUtils; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitRunnable; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.JSONValue; - -/** - * Check for updates on BukkitDev for a given plugin, and download the updates if needed. - *

- * VERY, VERY IMPORTANT: Because there are no standards for adding auto-update toggles in your plugin's config, this system provides NO CHECK WITH YOUR CONFIG to make sure the user has allowed auto-updating. - *
- * It is a BUKKIT POLICY that you include a boolean value in your config that prevents the auto-updater from running AT ALL. - *
- * If you fail to include this option in your config, your plugin will be REJECTED when you attempt to submit it to dev.bukkit.org. - *

- * An example of a good configuration option would be something similar to 'auto-update: true' - if this value is set to false you may NOT run the auto-updater. - *
- * If you are unsure about these rules, please read the plugin submission guidelines: http://goo.gl/8iU5l - * - * @author Gravity - * @version 2.3 - */ - -public class Updater { - - /* Constants */ - - // Remote file's title - private static final String TITLE_VALUE = "name"; - // Remote file's download link - private static final String LINK_VALUE = "downloadUrl"; - // Remote file's release type - private static final String TYPE_VALUE = "releaseType"; - // Remote file's build version - private static final String VERSION_VALUE = "gameVersion"; - // Path to GET - private static final String QUERY = "/servermods/files?projectIds="; - // Slugs will be appended to this to get to the project's RSS feed - private static final String HOST = "https://api.curseforge.com"; - // User-agent when querying Curse - private static final String USER_AGENT = "Updater (by Gravity)"; - // Used for locating version numbers in file names - private static final String DELIMETER = "^V|[\\s_-]V"; - // If the version number contains one of these, don't update. - private static final String[] NO_UPDATE_TAG = { "-DEV", "-PRE", "-SNAPSHOT" }; - // Used for downloading files - private static final int BYTE_SIZE = 1024; - // Config key for api key - private static final String API_KEY_CONFIG_KEY = "api-key"; - // Config key for disabling Updater - private static final String DISABLE_CONFIG_KEY = "disable"; - // Default api key value in config - private static final String API_KEY_DEFAULT = "PUT_API_KEY_HERE"; - // Default disable value in config - private static final boolean DISABLE_DEFAULT = false; - - /* User-provided variables */ - - // Plugin running Updater - private final Plugin plugin; - // Type of update check to run - private final UpdateType type; - // Whether to announce file downloads - private final boolean announce; - // The plugin file (jar) - private final File file; - // The folder that downloads will be placed in - private final File updateFolder; - // The provided callback (if any) - private final UpdateCallback callback; - // Project's Curse ID - private int id = -1; - // BukkitDev ServerMods API key - private String apiKey = null; - - /* Collected from Curse API */ - - private String versionName; - private String versionLink; - private String versionType; - private String versionGameVersion; - - /* Update process variables */ - - // Connection to RSS - private URL url; - // Updater thread - private Thread thread; - // Used for determining the outcome of the update process - private Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; - - /** - * Gives the developer the result of the update process. Can be obtained by called {@link #getResult()} - */ - public enum UpdateResult { - /** - * The updater found an update, and has readied it to be loaded the next time the server restarts/reloads. - */ - SUCCESS, - /** - * The updater did not find an update, and nothing was downloaded. - */ - NO_UPDATE, - /** - * The server administrator has disabled the updating system. - */ - DISABLED, - /** - * The updater found an update, but was unable to download it. - */ - FAIL_DOWNLOAD, - /** - * For some reason, the updater was unable to contact dev.bukkit.org to download the file. - */ - FAIL_DBO, - /** - * When running the version check, the file on DBO did not contain a recognizable version. - */ - FAIL_NOVERSION, - /** - * The id provided by the plugin running the updater was invalid and doesn't exist on DBO. - */ - FAIL_BADID, - /** - * The server administrator has improperly configured their API key in the configuration. - */ - FAIL_APIKEY, - /** - * The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded. - */ - UPDATE_AVAILABLE - } - - /** - * Allows the developer to specify the type of update that will be run. - */ - public enum UpdateType { - /** - * Run a version check, and then if the file is out of date, download the newest version. - */ - DEFAULT, - /** - * Don't run a version check, just find the latest update and download it. - */ - NO_VERSION_CHECK, - /** - * Get information about the version and the download size, but don't actually download anything. - */ - NO_DOWNLOAD - } - - /** - * Represents the various release types of a file on BukkitDev. - */ - public enum ReleaseType { - /** - * An "alpha" file. - */ - ALPHA, - /** - * A "beta" file. - */ - BETA, - /** - * A "release" file. - */ - RELEASE - } - - /** - * Initialize the updater. - * - * @param plugin The plugin that is checking for an update. - * @param id The dev.bukkit.org id of the project. - * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. - * @param type Specify the type of update this will be. See {@link UpdateType} - * @param announce True if the program should announce the progress of new updates in console. - */ - public Updater(Plugin plugin, int id, File file, UpdateType type, boolean announce) { - this(plugin, id, file, type, null, announce); - } - - /** - * Initialize the updater with the provided callback. - * - * @param plugin The plugin that is checking for an update. - * @param id The dev.bukkit.org id of the project. - * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. - * @param type Specify the type of update this will be. See {@link UpdateType} - * @param callback The callback instance to notify when the Updater has finished - */ - public Updater(Plugin plugin, int id, File file, UpdateType type, UpdateCallback callback) { - this(plugin, id, file, type, callback, false); - } - - /** - * Initialize the updater with the provided callback. - * - * @param plugin The plugin that is checking for an update. - * @param id The dev.bukkit.org id of the project. - * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. - * @param type Specify the type of update this will be. See {@link UpdateType} - * @param callback The callback instance to notify when the Updater has finished - * @param announce True if the program should announce the progress of new updates in console. - */ - public Updater(Plugin plugin, int id, File file, UpdateType type, UpdateCallback callback, boolean announce) { - this.plugin = plugin; - this.type = type; - this.announce = announce; - this.file = file; - this.id = id; - this.updateFolder = this.plugin.getServer().getUpdateFolderFile(); - this.callback = callback; - - final File pluginFile = this.plugin.getDataFolder().getParentFile(); - final File updaterFile = new File(pluginFile, "Updater"); - final File updaterConfigFile = new File(updaterFile, "config.yml"); - - YamlConfiguration config = new YamlConfiguration(); - config.options().header("This configuration file affects all plugins using the Updater system (version 2+ - http://forums.bukkit.org/threads/96681/ )" + '\n' - + "If you wish to use your API key, read http://wiki.bukkit.org/ServerMods_API and place it below." + '\n' - + "Some updating systems will not adhere to the disabled value, but these may be turned off in their plugin's configuration."); - config.addDefault(API_KEY_CONFIG_KEY, API_KEY_DEFAULT); - config.addDefault(DISABLE_CONFIG_KEY, DISABLE_DEFAULT); - - if (!updaterFile.exists()) { - this.fileIOOrError(updaterFile, updaterFile.mkdir(), true); - } - - boolean createFile = !updaterConfigFile.exists(); - try { - if (createFile) { - this.fileIOOrError(updaterConfigFile, updaterConfigFile.createNewFile(), true); - config.options().copyDefaults(true); - config.save(updaterConfigFile); - } else { - config.load(updaterConfigFile); - } - } catch (final Exception e) { - final String message; - if (createFile) { - message = "The updater could not create configuration at " + updaterFile.getAbsolutePath(); - } else { - message = "The updater could not load configuration at " + updaterFile.getAbsolutePath(); - } - this.plugin.getLogger().log(Level.SEVERE, message, e); - } - - if (config.getBoolean(DISABLE_CONFIG_KEY)) { - this.result = UpdateResult.DISABLED; - return; - } - - String key = config.getString(API_KEY_CONFIG_KEY); - if (API_KEY_DEFAULT.equalsIgnoreCase(key) || "".equals(key)) { - key = null; - } - - this.apiKey = key; - - try { - this.url = new URL(Updater.HOST + Updater.QUERY + this.id); - } catch (final MalformedURLException e) { - this.plugin.getLogger().log(Level.SEVERE, "The project ID provided for updating, " + this.id + " is invalid.", e); - this.result = UpdateResult.FAIL_BADID; - } - - if (this.result != UpdateResult.FAIL_BADID) { - this.thread = new Thread(new UpdateRunnable()); - this.thread.start(); - } else { - runUpdater(); - } - } - - /** - * Get the result of the update process. - * - * @return result of the update process. - * @see UpdateResult - */ - public Updater.UpdateResult getResult() { - this.waitForThread(); - return this.result; - } - - /** - * Get the latest version's release type. - * - * @return latest version's release type. - * @see ReleaseType - */ - public ReleaseType getLatestType() { - this.waitForThread(); - if (this.versionType != null) { - for (ReleaseType type : ReleaseType.values()) { - if (this.versionType.equalsIgnoreCase(type.name())) { - return type; - } - } - } - return null; - } - - /** - * Get the latest version's game version (such as "CB 1.2.5-R1.0"). - * - * @return latest version's game version. - */ - public String getLatestGameVersion() { - this.waitForThread(); - return this.versionGameVersion; - } - - /** - * Get the latest version's name (such as "Project v1.0"). - * - * @return latest version's name. - */ - public String getLatestName() { - this.waitForThread(); - return this.versionName; - } - - /** - * Get the latest version's direct file link. - * - * @return latest version's file link. - */ - public String getLatestFileLink() { - this.waitForThread(); - return this.versionLink; - } - - /** - * As the result of Updater output depends on the thread's completion, it is necessary to wait for the thread to finish - * before allowing anyone to check the result. - */ - private void waitForThread() { - if ((this.thread != null) && this.thread.isAlive()) { - try { - this.thread.join(); - } catch (final InterruptedException e) { - this.plugin.getLogger().log(Level.SEVERE, null, e); - } - } - } - - /** - * Save an update from dev.bukkit.org into the server's update folder. - * - * @param file the name of the file to save it as. - */ - private void saveFile(String file) { - final File folder = this.updateFolder; - - deleteOldFiles(); - if (!folder.exists()) { - this.fileIOOrError(folder, folder.mkdir(), true); - } - downloadFile(); - - // Check to see if it's a zip file, if it is, unzip it. - final File dFile = new File(folder.getAbsolutePath(), file); - if (dFile.getName().endsWith(".zip")) { - // Unzip - this.unzip(dFile.getAbsolutePath()); - } - if (this.announce) { - this.plugin.getLogger().info("Finished updating."); - } - } - - /** - * Download a file and save it to the specified folder. - */ - private void downloadFile() { - BufferedInputStream in = null; - FileOutputStream fout = null; - try { - URL fileUrl = new URL(this.versionLink); - final int fileLength = fileUrl.openConnection().getContentLength(); - in = new BufferedInputStream(fileUrl.openStream()); - fout = new FileOutputStream(new File(this.updateFolder, file.getName())); - - final byte[] data = new byte[Updater.BYTE_SIZE]; - int count; - if (this.announce) { - this.plugin.getLogger().info("About to download a new update: " + this.versionName); - } - long downloaded = 0; - while ((count = in.read(data, 0, Updater.BYTE_SIZE)) != -1) { - downloaded += count; - fout.write(data, 0, count); - final int percent = (int) ((downloaded * 100) / fileLength); - if (this.announce && ((percent % 10) == 0)) { - this.plugin.getLogger().info("Downloading update: " + percent + "% of " + fileLength + " bytes."); - } - } - } catch (Exception ex) { - this.plugin.getLogger().log(Level.WARNING, "The auto-updater tried to download a new update, but was unsuccessful.", ex); - this.result = Updater.UpdateResult.FAIL_DOWNLOAD; - } finally { - try { - if (in != null) { - in.close(); - } - } catch (final IOException ex) { - this.plugin.getLogger().log(Level.SEVERE, null, ex); - } - try { - if (fout != null) { - fout.close(); - } - } catch (final IOException ex) { - this.plugin.getLogger().log(Level.SEVERE, null, ex); - } - } - } - - /** - * Remove possibly leftover files from the update folder. - */ - private void deleteOldFiles() { - //Just a quick check to make sure we didn't leave any files from last time... - File[] list = listFilesOrError(this.updateFolder); - for (final File xFile : list) { - if (xFile.getName().endsWith(".zip")) { - this.fileIOOrError(xFile, xFile.mkdir(), true); - } - } - } - - /** - * Part of Zip-File-Extractor, modified by Gravity for use with Updater. - * - * @param file the location of the file to extract. - */ - private void unzip(String file) { - final File fSourceZip = new File(file); - try { - final String zipPath = file.substring(0, file.length() - 4); - ZipFile zipFile = new ZipFile(fSourceZip); - Enumeration e = zipFile.entries(); - while (e.hasMoreElements()) { - ZipEntry entry = e.nextElement(); - File destinationFilePath = new File(zipPath, entry.getName()); - this.fileIOOrError(destinationFilePath.getParentFile(), destinationFilePath.getParentFile().mkdirs(), true); - if (!entry.isDirectory()) { - final BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry)); - int b; - final byte[] buffer = new byte[Updater.BYTE_SIZE]; - final FileOutputStream fos = new FileOutputStream(destinationFilePath); - final BufferedOutputStream bos = new BufferedOutputStream(fos, Updater.BYTE_SIZE); - while ((b = bis.read(buffer, 0, Updater.BYTE_SIZE)) != -1) { - bos.write(buffer, 0, b); - } - bos.flush(); - bos.close(); - bis.close(); - final String name = destinationFilePath.getName(); - if (name.endsWith(".jar") && this.pluginExists(name)) { - File output = new File(this.updateFolder, name); - this.fileIOOrError(output, destinationFilePath.renameTo(output), true); - } - } - } - zipFile.close(); - - // Move any plugin data folders that were included to the right place, Bukkit won't do this for us. - moveNewZipFiles(zipPath); - - } catch (final IOException e) { - this.plugin.getLogger().log(Level.SEVERE, "The auto-updater tried to unzip a new update file, but was unsuccessful.", e); - this.result = Updater.UpdateResult.FAIL_DOWNLOAD; - } finally { - this.fileIOOrError(fSourceZip, fSourceZip.delete(), false); - } - } - - /** - * Find any new files extracted from an update into the plugin's data directory. - * @param zipPath path of extracted files. - */ - private void moveNewZipFiles(String zipPath) { - File[] list = listFilesOrError(new File(zipPath)); - for (final File dFile : list) { - if (dFile.isDirectory() && this.pluginExists(dFile.getName())) { - // Current dir - final File oFile = new File(this.plugin.getDataFolder().getParent(), dFile.getName()); - // List of existing files in the new dir - final File[] dList = listFilesOrError(dFile); - // List of existing files in the current dir - final File[] oList = listFilesOrError(oFile); - for (File cFile : dList) { - // Loop through all the files in the new dir - boolean found = false; - for (final File xFile : oList) { - // Loop through all the contents in the current dir to see if it exists - if (xFile.getName().equals(cFile.getName())) { - found = true; - break; - } - } - if (!found) { - // Move the new file into the current dir - File output = new File(oFile, cFile.getName()); - this.fileIOOrError(output, cFile.renameTo(output), true); - } else { - // This file already exists, so we don't need it anymore. - this.fileIOOrError(cFile, cFile.delete(), false); - } - } - } - this.fileIOOrError(dFile, dFile.delete(), false); - } - File zip = new File(zipPath); - this.fileIOOrError(zip, zip.delete(), false); - } - - /** - * Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip. - * - * @param name a name to check for inside the plugins folder. - * @return true if a file inside the plugins folder is named this. - */ - private boolean pluginExists(String name) { - File[] plugins = listFilesOrError(new File("plugins")); - for (final File file : plugins) { - if (file.getName().equals(name)) { - return true; - } - } - return false; - } - - /** - * Check to see if the program should continue by evaluating whether the plugin is already updated, or shouldn't be updated. - * - * @return true if the version was located and is not the same as the remote's newest. - */ - private boolean versionCheck() { - final String title = this.versionName; - if (this.type != UpdateType.NO_VERSION_CHECK) { - final String localVersion = this.plugin.getDescription().getVersion(); - if (title.split(DELIMETER).length == 2) { - // Get the newest file's version number - final String remoteVersion = title.split(DELIMETER)[1].split(" ")[0]; - - if (this.hasTag(localVersion) || !this.shouldUpdate(localVersion, remoteVersion)) { - // We already have the latest version, or this build is tagged for no-update - this.result = Updater.UpdateResult.NO_UPDATE; - return false; - } - } else { - // The file's name did not contain the string 'vVersion' - final String authorInfo = this.plugin.getDescription().getAuthors().isEmpty() ? "" : " (" + this.plugin.getDescription().getAuthors().get(0) + ")"; - this.plugin.getLogger().warning("The author of this plugin" + authorInfo + " has misconfigured their Auto Update system"); - this.plugin.getLogger().warning("File versions should follow the format 'PluginName vVERSION'"); - this.plugin.getLogger().warning("Please notify the author of this error."); - this.result = Updater.UpdateResult.FAIL_NOVERSION; - return false; - } - } - return true; - } - - /** - * If you wish to run mathematical versioning checks, edit this method. - *

- * With default behavior, Updater will NOT verify that a remote version available on BukkitDev - * which is not this version is indeed an "update". - * If a version is present on BukkitDev that is not the version that is currently running, - * Updater will assume that it is a newer version. - * This is because there is no standard versioning scheme, and creating a calculation that can - * determine whether a new update is actually an update is sometimes extremely complicated. - *

- *

- * Updater will call this method from {@link #versionCheck()} before deciding whether - * the remote version is actually an update. - * If you have a specific versioning scheme with which a mathematical determination can - * be reliably made to decide whether one version is higher than another, you may - * revise this method, using the local and remote version parameters, to execute the - * appropriate check. - *

- *

- * Returning a value of false will tell the update process that this is NOT a new version. - * Without revision, this method will always consider a remote version at all different from - * that of the local version a new update. - *

- * @param localVersion the current version - * @param remoteVersion the remote version - * @return true if Updater should consider the remote version an update, false if not. - */ - public boolean shouldUpdate(String localVersion, String remoteVersion) { - return !localVersion.equalsIgnoreCase(remoteVersion); - } - - /** - * Evaluate whether the version number is marked showing that it should not be updated by this program. - * - * @param version a version number to check for tags in. - * @return true if updating should be disabled. - */ - private boolean hasTag(String version) { - for (final String string : Updater.NO_UPDATE_TAG) { - if (version.contains(string)) { - return true; - } - } - return false; - } - - /** - * Make a connection to the BukkitDev API and request the newest file's details. - * - * @return true if successful. - */ - private boolean read() { - try { - final URLConnection conn = this.url.openConnection(); - conn.setConnectTimeout(5000); - - if (this.apiKey != null) { - conn.addRequestProperty("X-API-Key", this.apiKey); - } - conn.addRequestProperty("User-Agent", Updater.USER_AGENT); - - conn.setDoOutput(true); - - final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); - final String response = reader.readLine(); - - final JSONArray array = (JSONArray) JSONValue.parse(response); - - if (array.isEmpty()) { - this.plugin.getLogger().warning("The updater could not find any files for the project id " + this.id); - this.result = UpdateResult.FAIL_BADID; - return false; - } - - JSONObject latestUpdate = (JSONObject) array.get(array.size() - 1); - this.versionName = (String) latestUpdate.get(Updater.TITLE_VALUE); - this.versionLink = (String) latestUpdate.get(Updater.LINK_VALUE); - this.versionType = (String) latestUpdate.get(Updater.TYPE_VALUE); - this.versionGameVersion = (String) latestUpdate.get(Updater.VERSION_VALUE); - - return true; - } catch (final IOException e) { - if (e.getMessage().contains("HTTP response code: 403")) { - this.plugin.getLogger().severe("dev.bukkit.org rejected the API key provided in plugins/Updater/config.yml"); - this.plugin.getLogger().severe("Please double-check your configuration to ensure it is correct."); - this.result = UpdateResult.FAIL_APIKEY; - } else { - this.plugin.getLogger().severe("The updater could not contact dev.bukkit.org for updating."); - this.plugin.getLogger().severe("If you have not recently modified your configuration and this is the first time you are seeing this message, the site may be experiencing temporary downtime."); - this.result = UpdateResult.FAIL_DBO; - } - AreaShop.debug(ExceptionUtils.getStackTrace(e)); - return false; - } - } - - /** - * Perform a file operation and log any errors if it fails. - * @param file file operation is performed on. - * @param result result of file operation. - * @param create true if a file is being created, false if deleted. - */ - private void fileIOOrError(File file, boolean result, boolean create) { - if (!result) { - this.plugin.getLogger().severe("The updater could not " + (create ? "create" : "delete") + " file at: " + file.getAbsolutePath()); - } - } - - private File[] listFilesOrError(File folder) { - File[] contents = folder.listFiles(); - if (contents == null) { - this.plugin.getLogger().severe("The updater could not access files at: " + this.updateFolder.getAbsolutePath()); - return new File[0]; - } else { - return contents; - } - } - - /** - * Called on main thread when the Updater has finished working, regardless - * of result. - */ - public interface UpdateCallback { - /** - * Called when the updater has finished working. - * @param updater The updater instance - */ - void onFinish(Updater updater); - } - - private class UpdateRunnable implements Runnable { - @Override - public void run() { - runUpdater(); - } - } - - private void runUpdater() { - if (this.url != null && (this.read() && this.versionCheck())) { - // Obtain the results of the project's file feed - if ((this.versionLink != null) && (this.type != UpdateType.NO_DOWNLOAD)) { - String name = this.file.getName(); - // If it's a zip file, it shouldn't be downloaded as the plugin's name - if (this.versionLink.endsWith(".zip")) { - name = this.versionLink.substring(this.versionLink.lastIndexOf("/") + 1); - } - this.saveFile(name); - } else { - this.result = UpdateResult.UPDATE_AVAILABLE; - } - } - - if (this.callback != null) { - new BukkitRunnable() { - @Override - public void run() { - runCallback(); - } - }.runTask(this.plugin); - } - } - - private void runCallback() { - this.callback.onFinish(this); - } +package nl.evolutioncoding.areashop.lib; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.logging.Level; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import nl.evolutioncoding.areashop.AreaShop; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; + +/** + * Check for updates on BukkitDev for a given plugin, and download the updates if needed. + *

+ * VERY, VERY IMPORTANT: Because there are no standards for adding auto-update toggles in your plugin's config, this system provides NO CHECK WITH YOUR CONFIG to make sure the user has allowed auto-updating. + *
+ * It is a BUKKIT POLICY that you include a boolean value in your config that prevents the auto-updater from running AT ALL. + *
+ * If you fail to include this option in your config, your plugin will be REJECTED when you attempt to submit it to dev.bukkit.org. + *

+ * An example of a good configuration option would be something similar to 'auto-update: true' - if this value is set to false you may NOT run the auto-updater. + *
+ * If you are unsure about these rules, please read the plugin submission guidelines: http://goo.gl/8iU5l + * + * @author Gravity + * @version 2.3 + */ + +public class Updater { + + /* Constants */ + + // Remote file's title + private static final String TITLE_VALUE = "name"; + // Remote file's download link + private static final String LINK_VALUE = "downloadUrl"; + // Remote file's release type + private static final String TYPE_VALUE = "releaseType"; + // Remote file's build version + private static final String VERSION_VALUE = "gameVersion"; + // Path to GET + private static final String QUERY = "/servermods/files?projectIds="; + // Slugs will be appended to this to get to the project's RSS feed + private static final String HOST = "https://api.curseforge.com"; + // User-agent when querying Curse + private static final String USER_AGENT = "Updater (by Gravity)"; + // Used for locating version numbers in file names + private static final String DELIMETER = "^V|[\\s_-]V"; + // If the version number contains one of these, don't update. + private static final String[] NO_UPDATE_TAG = { "-DEV", "-PRE", "-SNAPSHOT" }; + // Used for downloading files + private static final int BYTE_SIZE = 1024; + // Config key for api key + private static final String API_KEY_CONFIG_KEY = "api-key"; + // Config key for disabling Updater + private static final String DISABLE_CONFIG_KEY = "disable"; + // Default api key value in config + private static final String API_KEY_DEFAULT = "PUT_API_KEY_HERE"; + // Default disable value in config + private static final boolean DISABLE_DEFAULT = false; + + /* User-provided variables */ + + // Plugin running Updater + private final Plugin plugin; + // Type of update check to run + private final UpdateType type; + // Whether to announce file downloads + private final boolean announce; + // The plugin file (jar) + private final File file; + // The folder that downloads will be placed in + private final File updateFolder; + // The provided callback (if any) + private final UpdateCallback callback; + // Project's Curse ID + private int id = -1; + // BukkitDev ServerMods API key + private String apiKey = null; + + /* Collected from Curse API */ + + private String versionName; + private String versionLink; + private String versionType; + private String versionGameVersion; + + /* Update process variables */ + + // Connection to RSS + private URL url; + // Updater thread + private Thread thread; + // Used for determining the outcome of the update process + private Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; + + /** + * Gives the developer the result of the update process. Can be obtained by called {@link #getResult()} + */ + public enum UpdateResult { + /** + * The updater found an update, and has readied it to be loaded the next time the server restarts/reloads. + */ + SUCCESS, + /** + * The updater did not find an update, and nothing was downloaded. + */ + NO_UPDATE, + /** + * The server administrator has disabled the updating system. + */ + DISABLED, + /** + * The updater found an update, but was unable to download it. + */ + FAIL_DOWNLOAD, + /** + * For some reason, the updater was unable to contact dev.bukkit.org to download the file. + */ + FAIL_DBO, + /** + * When running the version check, the file on DBO did not contain a recognizable version. + */ + FAIL_NOVERSION, + /** + * The id provided by the plugin running the updater was invalid and doesn't exist on DBO. + */ + FAIL_BADID, + /** + * The server administrator has improperly configured their API key in the configuration. + */ + FAIL_APIKEY, + /** + * The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded. + */ + UPDATE_AVAILABLE + } + + /** + * Allows the developer to specify the type of update that will be run. + */ + public enum UpdateType { + /** + * Run a version check, and then if the file is out of date, download the newest version. + */ + DEFAULT, + /** + * Don't run a version check, just find the latest update and download it. + */ + NO_VERSION_CHECK, + /** + * Get information about the version and the download size, but don't actually download anything. + */ + NO_DOWNLOAD + } + + /** + * Represents the various release types of a file on BukkitDev. + */ + public enum ReleaseType { + /** + * An "alpha" file. + */ + ALPHA, + /** + * A "beta" file. + */ + BETA, + /** + * A "release" file. + */ + RELEASE + } + + /** + * Initialize the updater. + * + * @param plugin The plugin that is checking for an update. + * @param id The dev.bukkit.org id of the project. + * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param type Specify the type of update this will be. See {@link UpdateType} + * @param announce True if the program should announce the progress of new updates in console. + */ + public Updater(Plugin plugin, int id, File file, UpdateType type, boolean announce) { + this(plugin, id, file, type, null, announce); + } + + /** + * Initialize the updater with the provided callback. + * + * @param plugin The plugin that is checking for an update. + * @param id The dev.bukkit.org id of the project. + * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param type Specify the type of update this will be. See {@link UpdateType} + * @param callback The callback instance to notify when the Updater has finished + */ + public Updater(Plugin plugin, int id, File file, UpdateType type, UpdateCallback callback) { + this(plugin, id, file, type, callback, false); + } + + /** + * Initialize the updater with the provided callback. + * + * @param plugin The plugin that is checking for an update. + * @param id The dev.bukkit.org id of the project. + * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param type Specify the type of update this will be. See {@link UpdateType} + * @param callback The callback instance to notify when the Updater has finished + * @param announce True if the program should announce the progress of new updates in console. + */ + public Updater(Plugin plugin, int id, File file, UpdateType type, UpdateCallback callback, boolean announce) { + this.plugin = plugin; + this.type = type; + this.announce = announce; + this.file = file; + this.id = id; + this.updateFolder = this.plugin.getServer().getUpdateFolderFile(); + this.callback = callback; + + final File pluginFile = this.plugin.getDataFolder().getParentFile(); + final File updaterFile = new File(pluginFile, "Updater"); + final File updaterConfigFile = new File(updaterFile, "config.yml"); + + YamlConfiguration config = new YamlConfiguration(); + config.options().header("This configuration file affects all plugins using the Updater system (version 2+ - http://forums.bukkit.org/threads/96681/ )" + '\n' + + "If you wish to use your API key, read http://wiki.bukkit.org/ServerMods_API and place it below." + '\n' + + "Some updating systems will not adhere to the disabled value, but these may be turned off in their plugin's configuration."); + config.addDefault(API_KEY_CONFIG_KEY, API_KEY_DEFAULT); + config.addDefault(DISABLE_CONFIG_KEY, DISABLE_DEFAULT); + + if (!updaterFile.exists()) { + this.fileIOOrError(updaterFile, updaterFile.mkdir(), true); + } + + boolean createFile = !updaterConfigFile.exists(); + try { + if (createFile) { + this.fileIOOrError(updaterConfigFile, updaterConfigFile.createNewFile(), true); + config.options().copyDefaults(true); + config.save(updaterConfigFile); + } else { + config.load(updaterConfigFile); + } + } catch (final Exception e) { + final String message; + if (createFile) { + message = "The updater could not create configuration at " + updaterFile.getAbsolutePath(); + } else { + message = "The updater could not load configuration at " + updaterFile.getAbsolutePath(); + } + this.plugin.getLogger().log(Level.SEVERE, message, e); + } + + if (config.getBoolean(DISABLE_CONFIG_KEY)) { + this.result = UpdateResult.DISABLED; + return; + } + + String key = config.getString(API_KEY_CONFIG_KEY); + if (API_KEY_DEFAULT.equalsIgnoreCase(key) || "".equals(key)) { + key = null; + } + + this.apiKey = key; + + try { + this.url = new URL(Updater.HOST + Updater.QUERY + this.id); + } catch (final MalformedURLException e) { + this.plugin.getLogger().log(Level.SEVERE, "The project ID provided for updating, " + this.id + " is invalid.", e); + this.result = UpdateResult.FAIL_BADID; + } + + if (this.result != UpdateResult.FAIL_BADID) { + this.thread = new Thread(new UpdateRunnable()); + this.thread.start(); + } else { + runUpdater(); + } + } + + /** + * Get the result of the update process. + * + * @return result of the update process. + * @see UpdateResult + */ + public Updater.UpdateResult getResult() { + this.waitForThread(); + return this.result; + } + + /** + * Get the latest version's release type. + * + * @return latest version's release type. + * @see ReleaseType + */ + public ReleaseType getLatestType() { + this.waitForThread(); + if (this.versionType != null) { + for (ReleaseType type : ReleaseType.values()) { + if (this.versionType.equalsIgnoreCase(type.name())) { + return type; + } + } + } + return null; + } + + /** + * Get the latest version's game version (such as "CB 1.2.5-R1.0"). + * + * @return latest version's game version. + */ + public String getLatestGameVersion() { + this.waitForThread(); + return this.versionGameVersion; + } + + /** + * Get the latest version's name (such as "Project v1.0"). + * + * @return latest version's name. + */ + public String getLatestName() { + this.waitForThread(); + return this.versionName; + } + + /** + * Get the latest version's direct file link. + * + * @return latest version's file link. + */ + public String getLatestFileLink() { + this.waitForThread(); + return this.versionLink; + } + + /** + * As the result of Updater output depends on the thread's completion, it is necessary to wait for the thread to finish + * before allowing anyone to check the result. + */ + private void waitForThread() { + if ((this.thread != null) && this.thread.isAlive()) { + try { + this.thread.join(); + } catch (final InterruptedException e) { + this.plugin.getLogger().log(Level.SEVERE, null, e); + } + } + } + + /** + * Save an update from dev.bukkit.org into the server's update folder. + * + * @param file the name of the file to save it as. + */ + private void saveFile(String file) { + final File folder = this.updateFolder; + + deleteOldFiles(); + if (!folder.exists()) { + this.fileIOOrError(folder, folder.mkdir(), true); + } + downloadFile(); + + // Check to see if it's a zip file, if it is, unzip it. + final File dFile = new File(folder.getAbsolutePath(), file); + if (dFile.getName().endsWith(".zip")) { + // Unzip + this.unzip(dFile.getAbsolutePath()); + } + if (this.announce) { + this.plugin.getLogger().info("Finished updating."); + } + } + + /** + * Download a file and save it to the specified folder. + */ + private void downloadFile() { + BufferedInputStream in = null; + FileOutputStream fout = null; + try { + URL fileUrl = new URL(this.versionLink); + final int fileLength = fileUrl.openConnection().getContentLength(); + in = new BufferedInputStream(fileUrl.openStream()); + fout = new FileOutputStream(new File(this.updateFolder, file.getName())); + + final byte[] data = new byte[Updater.BYTE_SIZE]; + int count; + if (this.announce) { + this.plugin.getLogger().info("About to download a new update: " + this.versionName); + } + long downloaded = 0; + while ((count = in.read(data, 0, Updater.BYTE_SIZE)) != -1) { + downloaded += count; + fout.write(data, 0, count); + final int percent = (int) ((downloaded * 100) / fileLength); + if (this.announce && ((percent % 10) == 0)) { + this.plugin.getLogger().info("Downloading update: " + percent + "% of " + fileLength + " bytes."); + } + } + } catch (Exception ex) { + this.plugin.getLogger().log(Level.WARNING, "The auto-updater tried to download a new update, but was unsuccessful.", ex); + this.result = Updater.UpdateResult.FAIL_DOWNLOAD; + } finally { + try { + if (in != null) { + in.close(); + } + } catch (final IOException ex) { + this.plugin.getLogger().log(Level.SEVERE, null, ex); + } + try { + if (fout != null) { + fout.close(); + } + } catch (final IOException ex) { + this.plugin.getLogger().log(Level.SEVERE, null, ex); + } + } + } + + /** + * Remove possibly leftover files from the update folder. + */ + private void deleteOldFiles() { + //Just a quick check to make sure we didn't leave any files from last time... + File[] list = listFilesOrError(this.updateFolder); + for (final File xFile : list) { + if (xFile.getName().endsWith(".zip")) { + this.fileIOOrError(xFile, xFile.mkdir(), true); + } + } + } + + /** + * Part of Zip-File-Extractor, modified by Gravity for use with Updater. + * + * @param file the location of the file to extract. + */ + private void unzip(String file) { + final File fSourceZip = new File(file); + try { + final String zipPath = file.substring(0, file.length() - 4); + ZipFile zipFile = new ZipFile(fSourceZip); + Enumeration e = zipFile.entries(); + while (e.hasMoreElements()) { + ZipEntry entry = e.nextElement(); + File destinationFilePath = new File(zipPath, entry.getName()); + this.fileIOOrError(destinationFilePath.getParentFile(), destinationFilePath.getParentFile().mkdirs(), true); + if (!entry.isDirectory()) { + final BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry)); + int b; + final byte[] buffer = new byte[Updater.BYTE_SIZE]; + final FileOutputStream fos = new FileOutputStream(destinationFilePath); + final BufferedOutputStream bos = new BufferedOutputStream(fos, Updater.BYTE_SIZE); + while ((b = bis.read(buffer, 0, Updater.BYTE_SIZE)) != -1) { + bos.write(buffer, 0, b); + } + bos.flush(); + bos.close(); + bis.close(); + final String name = destinationFilePath.getName(); + if (name.endsWith(".jar") && this.pluginExists(name)) { + File output = new File(this.updateFolder, name); + this.fileIOOrError(output, destinationFilePath.renameTo(output), true); + } + } + } + zipFile.close(); + + // Move any plugin data folders that were included to the right place, Bukkit won't do this for us. + moveNewZipFiles(zipPath); + + } catch (final IOException e) { + this.plugin.getLogger().log(Level.SEVERE, "The auto-updater tried to unzip a new update file, but was unsuccessful.", e); + this.result = Updater.UpdateResult.FAIL_DOWNLOAD; + } finally { + this.fileIOOrError(fSourceZip, fSourceZip.delete(), false); + } + } + + /** + * Find any new files extracted from an update into the plugin's data directory. + * @param zipPath path of extracted files. + */ + private void moveNewZipFiles(String zipPath) { + File[] list = listFilesOrError(new File(zipPath)); + for (final File dFile : list) { + if (dFile.isDirectory() && this.pluginExists(dFile.getName())) { + // Current dir + final File oFile = new File(this.plugin.getDataFolder().getParent(), dFile.getName()); + // List of existing files in the new dir + final File[] dList = listFilesOrError(dFile); + // List of existing files in the current dir + final File[] oList = listFilesOrError(oFile); + for (File cFile : dList) { + // Loop through all the files in the new dir + boolean found = false; + for (final File xFile : oList) { + // Loop through all the contents in the current dir to see if it exists + if (xFile.getName().equals(cFile.getName())) { + found = true; + break; + } + } + if (!found) { + // Move the new file into the current dir + File output = new File(oFile, cFile.getName()); + this.fileIOOrError(output, cFile.renameTo(output), true); + } else { + // This file already exists, so we don't need it anymore. + this.fileIOOrError(cFile, cFile.delete(), false); + } + } + } + this.fileIOOrError(dFile, dFile.delete(), false); + } + File zip = new File(zipPath); + this.fileIOOrError(zip, zip.delete(), false); + } + + /** + * Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip. + * + * @param name a name to check for inside the plugins folder. + * @return true if a file inside the plugins folder is named this. + */ + private boolean pluginExists(String name) { + File[] plugins = listFilesOrError(new File("plugins")); + for (final File file : plugins) { + if (file.getName().equals(name)) { + return true; + } + } + return false; + } + + /** + * Check to see if the program should continue by evaluating whether the plugin is already updated, or shouldn't be updated. + * + * @return true if the version was located and is not the same as the remote's newest. + */ + private boolean versionCheck() { + final String title = this.versionName; + if (this.type != UpdateType.NO_VERSION_CHECK) { + final String localVersion = this.plugin.getDescription().getVersion(); + if (title.split(DELIMETER).length == 2) { + // Get the newest file's version number + final String remoteVersion = title.split(DELIMETER)[1].split(" ")[0]; + + if (this.hasTag(localVersion) || !this.shouldUpdate(localVersion, remoteVersion)) { + // We already have the latest version, or this build is tagged for no-update + this.result = Updater.UpdateResult.NO_UPDATE; + return false; + } + } else { + // The file's name did not contain the string 'vVersion' + final String authorInfo = this.plugin.getDescription().getAuthors().isEmpty() ? "" : " (" + this.plugin.getDescription().getAuthors().get(0) + ")"; + this.plugin.getLogger().warning("The author of this plugin" + authorInfo + " has misconfigured their Auto Update system"); + this.plugin.getLogger().warning("File versions should follow the format 'PluginName vVERSION'"); + this.plugin.getLogger().warning("Please notify the author of this error."); + this.result = Updater.UpdateResult.FAIL_NOVERSION; + return false; + } + } + return true; + } + + /** + * If you wish to run mathematical versioning checks, edit this method. + *

+ * With default behavior, Updater will NOT verify that a remote version available on BukkitDev + * which is not this version is indeed an "update". + * If a version is present on BukkitDev that is not the version that is currently running, + * Updater will assume that it is a newer version. + * This is because there is no standard versioning scheme, and creating a calculation that can + * determine whether a new update is actually an update is sometimes extremely complicated. + *

+ *

+ * Updater will call this method from {@link #versionCheck()} before deciding whether + * the remote version is actually an update. + * If you have a specific versioning scheme with which a mathematical determination can + * be reliably made to decide whether one version is higher than another, you may + * revise this method, using the local and remote version parameters, to execute the + * appropriate check. + *

+ *

+ * Returning a value of false will tell the update process that this is NOT a new version. + * Without revision, this method will always consider a remote version at all different from + * that of the local version a new update. + *

+ * @param localVersion the current version + * @param remoteVersion the remote version + * @return true if Updater should consider the remote version an update, false if not. + */ + public boolean shouldUpdate(String localVersion, String remoteVersion) { + return !localVersion.equalsIgnoreCase(remoteVersion); + } + + /** + * Evaluate whether the version number is marked showing that it should not be updated by this program. + * + * @param version a version number to check for tags in. + * @return true if updating should be disabled. + */ + private boolean hasTag(String version) { + for (final String string : Updater.NO_UPDATE_TAG) { + if (version.contains(string)) { + return true; + } + } + return false; + } + + /** + * Make a connection to the BukkitDev API and request the newest file's details. + * + * @return true if successful. + */ + private boolean read() { + try { + final URLConnection conn = this.url.openConnection(); + conn.setConnectTimeout(5000); + + if (this.apiKey != null) { + conn.addRequestProperty("X-API-Key", this.apiKey); + } + conn.addRequestProperty("User-Agent", Updater.USER_AGENT); + + conn.setDoOutput(true); + + final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + final String response = reader.readLine(); + + final JSONArray array = (JSONArray) JSONValue.parse(response); + + if (array.isEmpty()) { + this.plugin.getLogger().warning("The updater could not find any files for the project id " + this.id); + this.result = UpdateResult.FAIL_BADID; + return false; + } + + JSONObject latestUpdate = (JSONObject) array.get(array.size() - 1); + this.versionName = (String) latestUpdate.get(Updater.TITLE_VALUE); + this.versionLink = (String) latestUpdate.get(Updater.LINK_VALUE); + this.versionType = (String) latestUpdate.get(Updater.TYPE_VALUE); + this.versionGameVersion = (String) latestUpdate.get(Updater.VERSION_VALUE); + + return true; + } catch (final IOException e) { + if (e.getMessage().contains("HTTP response code: 403")) { + this.plugin.getLogger().severe("dev.bukkit.org rejected the API key provided in plugins/Updater/config.yml"); + this.plugin.getLogger().severe("Please double-check your configuration to ensure it is correct."); + this.result = UpdateResult.FAIL_APIKEY; + } else { + this.plugin.getLogger().severe("The updater could not contact dev.bukkit.org for updating."); + this.plugin.getLogger().severe("If you have not recently modified your configuration and this is the first time you are seeing this message, the site may be experiencing temporary downtime."); + this.result = UpdateResult.FAIL_DBO; + } + AreaShop.debug(ExceptionUtils.getStackTrace(e)); + return false; + } + } + + /** + * Perform a file operation and log any errors if it fails. + * @param file file operation is performed on. + * @param result result of file operation. + * @param create true if a file is being created, false if deleted. + */ + private void fileIOOrError(File file, boolean result, boolean create) { + if (!result) { + this.plugin.getLogger().severe("The updater could not " + (create ? "create" : "delete") + " file at: " + file.getAbsolutePath()); + } + } + + private File[] listFilesOrError(File folder) { + File[] contents = folder.listFiles(); + if (contents == null) { + this.plugin.getLogger().severe("The updater could not access files at: " + this.updateFolder.getAbsolutePath()); + return new File[0]; + } else { + return contents; + } + } + + /** + * Called on main thread when the Updater has finished working, regardless + * of result. + */ + public interface UpdateCallback { + /** + * Called when the updater has finished working. + * @param updater The updater instance + */ + void onFinish(Updater updater); + } + + private class UpdateRunnable implements Runnable { + @Override + public void run() { + runUpdater(); + } + } + + private void runUpdater() { + if (this.url != null && (this.read() && this.versionCheck())) { + // Obtain the results of the project's file feed + if ((this.versionLink != null) && (this.type != UpdateType.NO_DOWNLOAD)) { + String name = this.file.getName(); + // If it's a zip file, it shouldn't be downloaded as the plugin's name + if (this.versionLink.endsWith(".zip")) { + name = this.versionLink.substring(this.versionLink.lastIndexOf("/") + 1); + } + this.saveFile(name); + } else { + this.result = UpdateResult.UPDATE_AVAILABLE; + } + } + + if (this.callback != null) { + new BukkitRunnable() { + @Override + public void run() { + runCallback(); + } + }.runTask(this.plugin); + } + } + + private void runCallback() { + this.callback.onFinish(this); + } } \ No newline at end of file diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/CommandManager.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/CommandManager.java index 5ea562e..1dada87 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/CommandManager.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/CommandManager.java @@ -1,14 +1,11 @@ package nl.evolutioncoding.areashop.managers; import nl.evolutioncoding.areashop.AreaShop; -import nl.evolutioncoding.areashop.Utils; import nl.evolutioncoding.areashop.commands.*; -import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.List; @@ -78,22 +75,17 @@ public class CommandManager implements CommandExecutor, TabCompleter { public void showHelp(CommandSender target) { // Add all messages to a list ArrayList messages = new ArrayList<>(); - messages.add(plugin.getConfig().getString("chatPrefix") + plugin.getLanguageManager().getLang("help-header")); - messages.add(plugin.getConfig().getString("chatPrefix") + plugin.getLanguageManager().getLang("help-alias")); + plugin.message(target, "help-header"); + plugin.message(target, "help-alias"); for(CommandAreaShop command : commands) { String help = command.getHelp(target); if(help != null && help.length() != 0) { messages.add(help); } } - // Send the messages to the target for(String message : messages) { - if(!plugin.getConfig().getBoolean("useColorsInConsole") && !(target instanceof Player)) { - target.sendMessage(ChatColor.stripColor(Utils.applyColors(message))); - } else { - target.sendMessage(Utils.applyColors(message)); - } + plugin.messageNoPrefix(target, message); } } diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/FileManager.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/FileManager.java index fef36ea..ee8d993 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/FileManager.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/FileManager.java @@ -819,7 +819,13 @@ public class FileManager { config.addDefaults(YamlConfiguration.loadConfiguration(normal)); // Set the debug and chatprefix variables plugin.setDebug(this.getConfig().getBoolean("debug")); - plugin.setChatprefix(this.getConfig().getString("chatPrefix")); + if(getConfig().isList("chatPrefix")) { + plugin.setChatprefix(getConfig().getStringList("chatPrefix")); + } else { + ArrayList list = new ArrayList<>(); + list.add(getConfig().getString("chatPrefix")); + plugin.setChatprefix(list); + } } } catch(IOException e) { plugin.getLogger().warning("Something went wrong while reading the config.yml file: " + configFile.getAbsolutePath()); diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/LanguageManager.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/LanguageManager.java deleted file mode 100644 index 45f755d..0000000 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/managers/LanguageManager.java +++ /dev/null @@ -1,199 +0,0 @@ -package nl.evolutioncoding.areashop.managers; - -import com.google.common.base.Charsets; -import nl.evolutioncoding.areashop.AreaShop; -import nl.evolutioncoding.areashop.regions.GeneralRegion; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.*; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class LanguageManager { - private AreaShop plugin = null; - private String languages[] = {"EN", "NL", "DE", "CS", "FR", "FI", "PL"}; - private HashMap currentLanguage, defaultLanguage; - - /** - * Constructor - * @param plugin The AreaShop plugin - */ - public LanguageManager(AreaShop plugin) { - this.plugin = plugin; - startup(); - } - - /** - * Save the default language files and open the current and backup language file - */ - public void startup() { - this.saveDefaults(); - this.loadLanguage(); - } - - /** - * Saves the default language files if not already present - */ - public void saveDefaults() { - // Create the language folder if it not exists - File langFolder; - langFolder = new File(plugin.getDataFolder() + File.separator + AreaShop.languageFolder); - if(!langFolder.exists()) { - if(!langFolder.mkdirs()) { - plugin.getLogger().warning("Could not create language directory: " + langFolder.getAbsolutePath()); - return; - } - } - - // Create the language files, overwrites if a file already exists - // Overriding is necessary because otherwise with an update the new lang - // files would not be used, when translating your own use another - // file name as the default - File langFile; - for(String language : languages) { - langFile = new File(plugin.getDataFolder() + File.separator + AreaShop.languageFolder + File.separator + language + ".yml"); - try( - InputStream input = plugin.getResource(AreaShop.languageFolder + "/" + language + ".yml"); - OutputStream output = new FileOutputStream(langFile) - ) { - if(input == null) { - plugin.getLogger().warning("Could not save default language to the '" + AreaShop.languageFolder + "' folder: " + language + ".yml"); - continue; - } - int read; - byte[] bytes = new byte[1024]; - while ((read = input.read(bytes)) != -1) { - output.write(bytes, 0, read); - } - input.close(); - output.close(); - } catch(IOException e) { - plugin.getLogger().warning("Something went wrong saving a default language file: " + langFile.getPath()); - } - } - - } - - /** - * Loads the current language file specified in the config - */ - public void loadLanguage() { - Map map; - Set set; - YamlConfiguration ymlFile; - - // Save the current language file to the HashMap - currentLanguage = new HashMap<>(); - File file = new File(plugin.getDataFolder() + File.separator + AreaShop.languageFolder + File.separator + plugin.getConfig().getString("language") + ".yml"); - - try( - InputStreamReader reader = new InputStreamReader(new FileInputStream(file), Charsets.UTF_8) - ) { - ymlFile = YamlConfiguration.loadConfiguration(reader); - map = ymlFile.getValues(true); - set = map.keySet(); - for(String key : set) { - if(map.get(key) instanceof String) { - currentLanguage.put(key, (String)map.get(key)); - } - } - } catch(IOException e) { - plugin.getLogger().warning("Could not load set language file: " + file.getAbsolutePath()); - } - - // Save the default strings to the HashMap - defaultLanguage = new HashMap<>(); - File standard = new File(plugin.getDataFolder() + File.separator + AreaShop.languageFolder + "/" + languages[0]+ ".yml"); - try( - InputStreamReader reader = new InputStreamReader(new FileInputStream(standard), Charsets.UTF_8) - ) { - ymlFile = YamlConfiguration.loadConfiguration(reader); - map = ymlFile.getValues(true); - set = map.keySet(); - for(String key : set) { - if(map.get(key) instanceof String) { - defaultLanguage.put(key, (String)map.get(key)); - } - } - } catch(IOException e) { - plugin.getLogger().warning("Could not load default language file: " + file.getAbsolutePath()); - } - } - - /** - * Function to get the string in the language that has been set - * @param key Key to the language string - * @param params The replacements for the %1% tags - * @return String The language string specified with the key - */ - public String getLang(String key, Object... params) { - String result; - - // Get the language string - if(currentLanguage.containsKey(key)) { - result = currentLanguage.get(key); - } else { - result = defaultLanguage.get(key); - } - - if(result == null) { - plugin.getLogger().info("Wrong key for getting translation: " + key + ", please contact the author about this"); - } else { - // Replace all tags like %0% and if given a GeneralRegion apply all replacements - int number=0; - for(Object param : params) { - if(param != null) { - if(param instanceof GeneralRegion) { - result = ((GeneralRegion)param).applyAllReplacements(result); - } else { - result = result.replace("%" + number + "%", param.toString()); - number++; - } - } - } - } - - return result; - } -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/ArrayWrapper.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/ArrayWrapper.java new file mode 100644 index 0000000..6d8fa08 --- /dev/null +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/ArrayWrapper.java @@ -0,0 +1,107 @@ +package nl.evolutioncoding.areashop.messages; + +import org.apache.commons.lang.Validate; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; + +/** + * Class made by glan3b (Github: https://github.com/glen3b) for the Fanciful project (Github: https://github.com/mkremins/fanciful) + */ + +/** + * Represents a wrapper around an array class of an arbitrary reference type, + * which properly implements "value" hash code and equality functions. + *

+ * This class is intended for use as a key to a map. + *

+ * @param The type of elements in the array. + * @author Glen Husman + * @see Arrays + */ +public final class ArrayWrapper { + + /** + * Creates an array wrapper with some elements. + * @param elements The elements of the array. + */ + public ArrayWrapper(E... elements) { + setArray(elements); + } + + private E[] _array; + + /** + * Retrieves a reference to the wrapped array instance. + * @return The array wrapped by this instance. + */ + public E[] getArray() { + return _array; + } + + /** + * Set this wrapper to wrap a new array instance. + * @param array The new wrapped array. + */ + public void setArray(E[] array) { + Validate.notNull(array, "The array must not be null."); + _array = array; + } + + /** + * Determines if this object has a value equivalent to another object. + * @see Arrays#equals(Object[], Object[]) + */ + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object other) { + if(!(other instanceof ArrayWrapper)) { + return false; + } + return Arrays.equals(_array, ((ArrayWrapper)other)._array); + } + + /** + * Gets the hash code represented by this objects value. + * @return This object's hash code. + * @see Arrays#hashCode(Object[]) + */ + @Override + public int hashCode() { + return Arrays.hashCode(_array); + } + + /** + * Converts an iterable element collection to an array of elements. + * The iteration order of the specified object will be used as the array element order. + * @param list The iterable of objects which will be converted to an array. + * @param c The type of the elements of the array. + * @return An array of elements in the specified iterable. + */ + @SuppressWarnings("unchecked") + public static T[] toArray(Iterable list, Class c) { + int size = -1; + if(list instanceof Collection) { + @SuppressWarnings("rawtypes") + Collection coll = (Collection)list; + size = coll.size(); + } + + + if(size < 0) { + size = 0; + // Ugly hack: Count it ourselves + for(@SuppressWarnings("unused") T element : list) { + size++; + } + } + + T[] result = (T[])Array.newInstance(c, size); + int i = 0; + for(T element : list) { // Assumes iteration order is consistent + result[i++] = element; // Assign array element at index THEN increment counter + } + return result; + } +} \ No newline at end of file diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/FancyMessageFormat.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/FancyMessageFormat.java new file mode 100644 index 0000000..e0bc977 --- /dev/null +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/FancyMessageFormat.java @@ -0,0 +1,853 @@ +package nl.evolutioncoding.areashop.messages; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * FancyMessageFormat converter, a library that enables to convert + * messages in the FancyMessageFormat to Minecraft's bulky tellraw + * format. + * @author NLThijs48 | http://wiefferink.me + * @author Tobias aka Phoenix | http://www.phoenix-iv.de + */ +public class FancyMessageFormat { + + private static final char TAG_BEFORE = '['; + private static final char TAG_AFTER = ']'; + private static final char END_TAG_INDICATOR = '/'; + + /** + * The special character that prefixes all basic chat formatting codes. + */ + private static final char SIMPLE_FORMAT_PREFIX_CHAR = '\u00A7'; + + /** + * Resets all previous chat colors or formats. + */ + private static final char SIMPLE_FORMAT_RESET_CHAR = 'r'; + + /** + * Lookup table for all continuous tags (marked by []) + */ + private static final HashMap BRACKET_TAG_LIST = new HashMap<>(); + + /** + * Lookup table for all interactive tags + */ + private static final HashMap INTERACTIVE_TAG_LIST = new HashMap<>(); + + static { + // Enlist all possible tags + // (They go into a HashMap for lookup purposes) + cacheTags(BRACKET_TAG_LIST, Color.class); + cacheTags(BRACKET_TAG_LIST, FormatType.class); + cacheTags(BRACKET_TAG_LIST, FormatCloseTag.class); + cacheTags(BRACKET_TAG_LIST, ControlTag.class); + // Interactive tags + cacheTags(INTERACTIVE_TAG_LIST, ClickType.class); + cacheTags(INTERACTIVE_TAG_LIST, HoverType.class); + } + + + /** + * Puts all constants in the given Tag class into the given lookup table. + */ + private static void cacheTags(HashMap tagList, Class tags) { + for(Tag tag : tags.getEnumConstants()) { + for(String key : tag.getTags()) { + tagList.put(key, tag); + } + } + } + + + // ------------------------------------------------------------------------------------------ + // ------------------------------- Public / Interface ------------------------------- + // ------------------------------------------------------------------------------------------ + + + /** + * Parses the given FancyMessageFormat message to a JSON array that can be + * used with the tellraw command and the like. + */ + public static String convertToJSON(final String message) { + return convertToJSON(Collections.singleton(message)); + } + + /** + * Parses the given FancyMessageFormat message to a JSON array that can be + * used with the tellraw command and the like. + * @param inputLines Input message split at line breaks. + */ + public static String convertToJSON(final Iterable inputLines) { + ArrayList lines = cleanInputString(inputLines); + + LinkedList message = parse(lines); + StringBuilder sb = new StringBuilder(); + if(message.size() == 1) { + sb.append(message.getFirst().toJSON()); + } else if(message.size() > 0) { + sb.append("{text=\"\",extra:["); + for(InteractiveMessagePart messagePart : message) { + sb.append(messagePart.toJSON()); + sb.append(','); + } + sb.deleteCharAt(sb.length()-1); + sb.append("]}"); + } + return sb.toString(); + } + + + /** + * Parses the given FancyMessageFormat message to a String containing control characters + * for formatting that can be used for console outputs, but also for normal player + * messages. + *

+ * The returned message will only contain colors, bold, italic, underlining and 'magic' + * characters. Hovers and other advanced tellraw tags will be skipped. + * @param message Input message split at line breaks. + */ + public static String convertToConsole(final String message) { + return convertToConsole(Collections.singleton(message)); + } + + /** + * Parses the given FancyMessageFormat message to a String containing control characters + * for formatting that can be used for console outputs, but also for normal player + * messages. + *

+ * The returned message will only contain colors, bold, italic, underlining and 'magic' + * characters. Hovers and other advanced tellraw tags will be skipped. + */ + public static String convertToConsole(final Iterable inputLines) { + if(inputLines == null) { + return null; + } + LinkedList parts = parse(inputLines, false); + StringBuilder result = new StringBuilder(); + for(InteractiveMessagePart part : parts) { + result.append(part.toSimpleString()); + } + return result.toString(); + } + + + /** + * Insert a message at the specified position + * @param message The current message + * @param insert The message to insert + * @param line The line number to insert at + * @param start The start of the variable to replace + * @param end The end of the variable to replace + */ + public static void insertMessage(List message, List insert, int line, int start, int end) { + String lineContent = message.remove(line); + if(isTaggedInteractive(lineContent)) { + lineContent = lineContent.replace("", ""); + message.add(line, lineContent.substring(0, start)+convertToConsole(insert)+lineContent.substring(end)); + return; + } + // Find interactive lines meant for this message + List interactives = new ArrayList<>(); + int index = line; + while(index < message.size() && isTaggedInteractive(message.get(index))) { + interactives.add(message.get(index)); + index++; + } + // Split the line and add the parts + int at = line; + if(start > 0) { + message.add(line, lineContent.substring(0, start)); + at++; + message.addAll(at, interactives); + at += interactives.size(); + } + message.addAll(at, insert); + at += insert.size(); + message.addAll(at, interactives); + at += interactives.size(); + if(end < (lineContent.length()-1)) { + message.add(at, lineContent.substring(end)); + at++; + message.addAll(at, interactives); + } + } + + + // ------------------------------------------------------------------------------------------ + // ------------------------------- Private functions ------------------------------- + // ------------------------------------------------------------------------------------------ + + + /** + *

    + *
  • Splits lines at line breaks (creating a new line in the Array). + *
  • Removes empty lines at the beginning. + *
  • Removes lines with properties in front of the first text-line. + *
+ */ + private static ArrayList cleanInputString(Iterable inputLines) { + // Split lines at line breaks + // In the end we will have a list with one line per element + ArrayList lines = new ArrayList<>(); + for(String line : inputLines) { + lines.addAll(Arrays.asList(line.split("\\r?\\n"))); + } + + // Remove any special lines at the start (a real text line should be first) + while(!lines.isEmpty() && isTaggedInteractive(lines.get(0))) { + lines.remove(0); + } + + return lines; + } + + + private static LinkedList parse(Iterable inputLines) { + return parse(inputLines, true); + } + + private static LinkedList parse(final Iterable inputLines, boolean doInteractives) { + LinkedList message = new LinkedList<>(); + + Color currentColor = null; + Set currentFormatting = new HashSet<>(); + + lineLoop: + for(String line : inputLines) { + InteractiveMessagePart messagePart; + TaggedContent interactiveTag = getInteractiveTag(line); + boolean isTextLine = interactiveTag == null; + if(!doInteractives && !isTextLine) { + continue; + } + boolean isHoverLine = false; + + if(isTextLine) { + messagePart = new InteractiveMessagePart(); + message.add(messagePart); + } else /* if Interactive formatting */ { + messagePart = message.getLast(); + Tag tag = interactiveTag.tag; + if(tag instanceof ClickType) { + messagePart.clickType = (ClickType)interactiveTag.tag; + messagePart.clickContent = interactiveTag.subsequentContent; + } else if(tag instanceof HoverType) { + line = interactiveTag.subsequentContent; + isHoverLine = true; + if(messagePart.hoverType != tag) { + // Hover type changed + messagePart.hoverContent = new LinkedList<>(); + messagePart.hoverType = (HoverType)tag; + } + // Add hover content below + } + } + + if(isTextLine || isHoverLine) { + // Parse inline tags + + Color currentLineColor = currentColor; + Set currentLineFormatting = currentFormatting; + LinkedList targetList = messagePart.content; + boolean parseBreak = true; + if(isHoverLine) { + // Reset - use own + currentLineColor = null; + currentLineFormatting = new HashSet<>(); + targetList = messagePart.hoverContent; + parseBreak = false; + + // Add line break after previous hover line + if(!targetList.isEmpty()) { + targetList.getLast().text += '\n'; + } + } + + // Split into pieces at places where formatting changes + while(!line.isEmpty()) { + String textToAdd; + TaggedContent nextTag = getNextTag(line, parseBreak); + boolean tagged = nextTag != null; + + if(!tagged) { + textToAdd = line; + line = ""; + } else { + textToAdd = nextTag.precedingContent; + line = nextTag.subsequentContent; + } + + if(!textToAdd.isEmpty()) { + // Add a text part with the correct formatting + TextMessagePart part = new TextMessagePart(); + part.text = textToAdd; + part.formatTypes = new HashSet<>(currentLineFormatting); + part.color = currentLineColor; + targetList.add(part); + } + + // Handle the change in formatting if a Tag has been detected (this needs to be after creating the InteractiveMessagePart) + if(tagged) { + // Handle the formatting tag + Tag tag = nextTag.tag; + if(tag instanceof Color) { + currentLineColor = (Color)tag; + } else if(tag instanceof FormatType) { + currentLineFormatting.add((FormatType)tag); + } else if(tag instanceof FormatCloseTag) { + currentLineFormatting.remove(((FormatCloseTag)tag).closes); + } else if(tag == ControlTag.BREAK) { + targetList.getLast().text += '\n'; + continue lineLoop; + } else if(tag == ControlTag.RESET) { + currentLineFormatting.clear(); + currentLineColor = Color.WHITE; + } + } + } + + if(!isHoverLine) { + // Adapt global attributes + currentColor = currentLineColor; + if(messagePart.content.size() == 0) { // Prevent interactive parts without content + message.removeLast(); + } + } + } + } + return message; + } + + + /** + * Searches and returns the first continuous tag found in the given String. + * @return The tag (plus its preceding and subsequent content) if found. + * Null if nothing is found. + */ + private static TaggedContent getNextTag(String line, boolean parseBreak) { + Pattern pattern = Pattern.compile("\\[[a-zA-Z1-9]\\]|&[1-9abcdeflonskr]"); + Matcher matcher = pattern.matcher(line); + // TODO Fix for escape things, and something with parseBreak? + if(matcher.find()) { + Tag tag = null; + if(matcher.group().startsWith("&")) { + for(Color color : Color.class.getEnumConstants()) { + if(color.getNativeFormattingCode() == matcher.group().charAt(1)) { + tag = color; + } + } + for(FormatType format : FormatType.class.getEnumConstants()) { + if(format.getNativeFormattingCode() == matcher.group().charAt(1)) { + tag = format; + } + } + if(matcher.group().charAt(1) == 'r') { + tag = ControlTag.RESET; + } + } else { + tag = BRACKET_TAG_LIST.get(matcher.group().substring(1, matcher.group().length()-1).toLowerCase()); + } + return new TaggedContent(line.substring(0, matcher.start()), tag, line.substring(matcher.end())); + } + return null; + + /* + for(int startIndex = 0; startIndex < line.length(); startIndex++) { + int start = line.indexOf(TAG_BEFORE, startIndex); + if(start != -1) { + int end = line.indexOf(TAG_AFTER, start); + if(end != -1) { + String inBetween = line.substring(start+1, end).toLowerCase(); + if(BRACKET_TAG_LIST.containsKey(inBetween)) { + Tag tag = BRACKET_TAG_LIST.get(inBetween); + if(tag == ControlTag.ESCAPE) { + // Ignore next char + line = line.substring(0, start)+line.substring(end+1); + startIndex = start; + } else if(!parseBreak && tag == ControlTag.ESCAPE) { + // Ignore break + startIndex = end+1; + } else { + String previousContent = line.substring(0, start); + String subsequentContent = line.substring(end+1); + return new TaggedContent(previousContent, tag, subsequentContent); + } + } else { + startIndex = start; + } + } else { + return null; + } + } else { + return null; + } + } + return null; + */ + } + + + /** + * If the given line defines an interactive property (e.g. "hover: myText") + * the tag / property will be returned. Otherwise null is returned. + */ + private static TaggedContent getInteractiveTag(String line) { + for(int index = 0; index < line.length(); index++) { + char c = line.charAt(index); + if(c == ' ' || c == '\t') { + // Ignore (Skip spacing) + } else { + int end = line.indexOf(": ", index); + if(end != -1) { + String inBetween = line.substring(index, end).toLowerCase(); + if(INTERACTIVE_TAG_LIST.containsKey(inBetween)) { + Tag tag = INTERACTIVE_TAG_LIST.get(inBetween); + String subsequentContent = line.substring(end+2); + return new TaggedContent(null, tag, subsequentContent); + } + } + return null; + } + } + return null; + } + + + /** + * Check if a line is an advanced declaration like hover or command + * @param line The line to check + * @return true if the line is interactive, false when it is a text line + */ + public static boolean isTaggedInteractive(String line) { + return getInteractiveTag(line) != null; + } + + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. + * @param string A String + * @return A String correctly formatted for insertion in a JSON text. + */ + /* + * Copyright (c) 2002 JSON.org + * Licensed under the Apache License, Version 2.0 + */ + private static String quoteStringJson(String string) { + if(string == null || string.length() == 0) { + return "\"\""; + } + + char c; + int i; + int len = string.length(); + StringBuilder sb = new StringBuilder(len+4); + String t; + + sb.append('"'); + for(i = 0; i < len; i += 1) { + c = string.charAt(i); + switch(c) { + case '\\': + case '"': + sb.append('\\'); + sb.append(c); + break; + case '/': + sb.append('\\'); + sb.append(c); + break; + case '\b': + sb.append("\\b"); + break; + case '\t': + sb.append("\\t"); + break; + case '\n': + sb.append("\\n"); + break; + case '\f': + sb.append("\\f"); + break; + case '\r': + sb.append("\\r"); + break; + default: + if(c < ' ') { + t = "000"+Integer.toHexString(c); + sb.append("\\u"); + sb.append(t.substring(t.length()-4)); + } else { + sb.append(c); + } + } + } + sb.append('"'); + return sb.toString(); + } + + + // ------------------------------------------------------------------------------------------ + // ------------------------------- Helper classes ------------------------------- + // ------------------------------------------------------------------------------------------ + + + private static class TaggedContent { + final String precedingContent; + final Tag tag; + final String subsequentContent; + + public TaggedContent(String pre, Tag tag, String sub) { + this.precedingContent = pre; + this.tag = tag; + this.subsequentContent = sub; + } + } + + + /** + * Holds a string with basic (non-interactive) formatting. + */ + private static class TextMessagePart { + String text = ""; + Color color = null; + Set formatTypes = new HashSet<>(); + + /** + * Get a simple colored/formatted string + * @return String with formatting + */ + String toSimpleString() { + StringBuilder sb = new StringBuilder(); + // Color + if(color != null) { + sb.append(SIMPLE_FORMAT_PREFIX_CHAR); + sb.append(color.getNativeFormattingCode()); + } + // Formatting + for(FormatType format : formatTypes) { + sb.append(SIMPLE_FORMAT_PREFIX_CHAR); + sb.append(format.getNativeFormattingCode()); + } + // Text + sb.append(text); + return sb.toString(); + } + + /** + * Get a JSON component for this message part + * @return This part formatted in JSON + */ + String toJSON() { + StringBuilder sb = new StringBuilder(); + sb.append('{'); + sb.append("text:").append(quoteStringJson(text)); + if(color != null && color != Color.WHITE) { + sb.append(",color:").append(color.jsonValue); + } + for(FormatType formatting : formatTypes) { + sb.append(','); + sb.append(formatting.jsonKey).append(':'); + sb.append("true"); + } + sb.append('}'); + return sb.toString(); + } + + boolean hasFormatting() { + return !(color == Color.WHITE && formatTypes.isEmpty()); + } + + @Override + public String toString() { + return "TextMessagePart(text:"+text+", color:"+color+", formatting:"+formatTypes.toString()+")"; + } + } + + + /** + * Holds a string with interactive formatting. + */ + private static class InteractiveMessagePart { + + LinkedList content = new LinkedList<>(); + + // Click + ClickType clickType = null; + String clickContent = ""; + + // Hover + HoverType hoverType = null; + LinkedList hoverContent = null; + + /** + * Get a simple colored/formatted string + * @return String with formatting + */ + String toSimpleString() { + StringBuilder sb = new StringBuilder(); + for(TextMessagePart part : content) { + sb.append(part.toSimpleString()); + } + return sb.toString(); + } + + /** + * Get a JSON component for this message part + * @return This part formatted in JSON + */ + String toJSON() { + StringBuilder sb = new StringBuilder(); + if(content.size() == 1) { + // Add attributes to TextMessagePart object + sb.append(content.getFirst().toJSON()); + sb.deleteCharAt(sb.length()-1); + } else { + sb.append('{'); + sb.append("text=\"\",extra:["); + for(TextMessagePart textPart : content) { + sb.append(textPart.toJSON()); + sb.append(','); + } + sb.deleteCharAt(sb.length()-1); + sb.append(']'); + } + if(clickType != null) { + sb.append(','); + sb.append("clickEvent:{"); + sb.append("action:").append(clickType.getJsonKey()).append(','); + sb.append("value:").append(quoteStringJson(clickContent)); + sb.append('}'); + } + if(hoverType != null) { + sb.append(','); + sb.append("hoverEvent:{"); + sb.append("action:").append(hoverType.getJsonKey()).append(','); + sb.append("value:"); + if(hoverContent.size() == 1) { + TextMessagePart hoverPart = hoverContent.getFirst(); + if(hoverPart.hasFormatting()) { + sb.append(hoverPart.toJSON()); + } else { + sb.append(quoteStringJson(hoverPart.text)); + } + } else { + sb.append('['); + for(TextMessagePart hoverPart : hoverContent) { + sb.append(hoverPart.toJSON()); + sb.append(','); + } + sb.deleteCharAt(sb.length()-1); + sb.append(']'); + } + sb.append('}'); + } + sb.append('}'); + return sb.toString(); + } + + @Override + public String toString() { + return "InteractiveMessagePart(textMessageParts:"+content+", clickType:"+clickType+", clickContent:"+clickContent+", hoverType:"+hoverType+", hoverContent:"+hoverContent+")"; + } + } + + + // --------------------------------------- Tags --------------------------------------- + + + interface Tag { + /** + * Tag text(s) used in the FancyMessageFormat (The text between '[' and ']') + */ + String[] getTags(); + } + + + /** + * Indicates formatting that is applied until explicitly stopped. + * Can also be used in simple Minecraft messages (Non-JSON). + */ + interface ContinuousTag extends Tag { + /** + * The character that defines upcoming formatting in a native (non-JSON) Minecraft message. + */ + char getNativeFormattingCode(); + } + + + /** + * Indicates formatting that allows cursor interaction. Requires the + * Minecraft JSON / tellraw format. + */ + interface InteractiveMessageTag extends Tag { + String getJsonKey(); + } + + + enum Color implements ContinuousTag { + WHITE('f'), + BLACK('0'), + BLUE('9'), + DARK_BLUE('1'), + GREEN('a'), + DARK_GREEN('2'), + AQUA('b'), + DARK_AQUA('3'), + RED('c'), + DARK_RED('4'), + LIGHT_PURPLE('d'), + DARK_PURPLE('5'), + YELLOW('e'), + GOLD('6'), + GRAY('7'), + DARK_GRAY('8'); + + final char bytecode; + final String jsonValue; + final String[] tags; + + Color(char bytecode) { + this.bytecode = bytecode; + this.jsonValue = this.name().toLowerCase(); + this.tags = new String[]{this.name().toLowerCase()}; + } + + @Override + public String[] getTags() { + return tags; + } + + @Override + public char getNativeFormattingCode() { + return bytecode; + } + } + + + enum FormatType implements ContinuousTag { + BOLD('l', "bold", "b", "bold"), + ITALIC('o', "italic", "i", "italic"), + UNDERLINE('n', "underlined", "u", "underline"), + STRIKETHROUGH('s', "strikethrough", "s", "strikethrough"), + OBFUSCATE('k', "obfuscated", "obfuscate"); + + final char bytecode; + final String jsonKey; + final String[] tags; + + FormatType(char bytecode, String jsonKey, String... tags) { + this.bytecode = bytecode; + this.jsonKey = jsonKey; + this.tags = tags; + } + + @Override + public String[] getTags() { + return tags; + } + + @Override + public char getNativeFormattingCode() { + return bytecode; + } + + } + + + enum FormatCloseTag implements Tag { + BOLD_END(FormatType.BOLD), + ITALIC_END(FormatType.ITALIC), + UNDERLINE_END(FormatType.UNDERLINE), + STRIKETHROUGH_END(FormatType.STRIKETHROUGH), + OBFUSCATE_END(FormatType.OBFUSCATE); + + /** + * Formatting that is stopped at this point + */ + final FormatType closes; + private final String[] tags; + + FormatCloseTag(FormatType openingTag) { + this.closes = openingTag; + + // Auto-generate close tags + tags = new String[closes.tags.length]; + for(int i = 0; i < tags.length; i++) { + tags[i] = END_TAG_INDICATOR+closes.tags[i]; + } + } + + @Override + public String[] getTags() { + return tags; + } + + } + + + enum ControlTag implements Tag { + BREAK("break"), + ESCAPE("esc"), + RESET("reset"); + + private final String[] tags; + + ControlTag(String... tags) { + this.tags = tags; + } + + @Override + public String[] getTags() { + return tags; + } + + } + + + /** + * Types of clicking + */ + enum ClickType implements InteractiveMessageTag { + LINK("open_url", "link"), + COMMAND("run_command", "command"), + SUGGEST("suggest_command", "suggest"); + + private final String jsonKey; + private final String[] tags; + + ClickType(String jsonKey, String... tags) { + this.jsonKey = jsonKey; + this.tags = tags; + } + + @Override + public String[] getTags() { + return tags; + } + + @Override + public String getJsonKey() { + return jsonKey; + } + } + + + enum HoverType implements InteractiveMessageTag { + HOVER; + + @Override + public String[] getTags() { + return new String[]{"hover"}; + } + + @Override + public String getJsonKey() { + return "show_text"; + } + } + +} \ No newline at end of file diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/FancyMessageSender.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/FancyMessageSender.java new file mode 100644 index 0000000..c679146 --- /dev/null +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/FancyMessageSender.java @@ -0,0 +1,91 @@ +package nl.evolutioncoding.areashop.messages; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.lang.reflect.*; +import java.util.logging.Level; + + +/** + * Methods written by the Fanciful project (Github: https://github.com/mkremins/fanciful) + */ + +public class FancyMessageSender { + + private static Constructor nmsPacketPlayOutChatConstructor; + + public static boolean sendJSON(Player player, String jsonString) { + try { + Object handle = Reflection.getHandle(player); + Object connection = Reflection.getField(handle.getClass(), "playerConnection").get(handle); + Reflection.getMethod(connection.getClass(), "sendPacket", Reflection.getNMSClass("Packet")).invoke(connection, createChatPacket(jsonString)); + return true; + } catch(IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + } catch(IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + } catch(InstantiationException e) { + Bukkit.getLogger().log(Level.WARNING, "Underlying class is abstract.", e); + } catch(InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + } catch(NoSuchMethodException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not find method.", e); + } catch(ClassNotFoundException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not find class.", e); + } + return false; + } + + // The ChatSerializer's instance of Gson + private static Object nmsChatSerializerGsonInstance; + private static Method fromJsonMethod; + + private static Object createChatPacket(String json) throws IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException { + if(nmsChatSerializerGsonInstance == null) { + // Find the field and its value, completely bypassing obfuscation + Class chatSerializerClazz; + + String version = Reflection.getVersion(); + double majorVersion = Double.parseDouble(version.replace('_', '.').substring(1, 4)); + int lesserVersion = Integer.parseInt(version.substring(6, 7)); + + if(majorVersion < 1.8 || (majorVersion == 1.8 && lesserVersion == 1)) { + chatSerializerClazz = Reflection.getNMSClass("ChatSerializer"); + } else { + chatSerializerClazz = Reflection.getNMSClass("IChatBaseComponent$ChatSerializer"); + } + + if(chatSerializerClazz == null) { + throw new ClassNotFoundException("Can't find the ChatSerializer class"); + } + + for(Field declaredField : chatSerializerClazz.getDeclaredFields()) { + if(Modifier.isFinal(declaredField.getModifiers()) && Modifier.isStatic(declaredField.getModifiers()) && declaredField.getType().getName().endsWith("Gson")) { + // We've found our field + declaredField.setAccessible(true); + nmsChatSerializerGsonInstance = declaredField.get(null); + fromJsonMethod = nmsChatSerializerGsonInstance.getClass().getMethod("fromJson", String.class, Class.class); + break; + } + } + } + + // Since the method is so simple, and all the obfuscated methods have the same name, it's easier to reimplement 'IChatBaseComponent a(String)' than to reflectively call it + // Of course, the implementation may change, but fuzzy matches might break with signature changes + Object serializedChatComponent = fromJsonMethod.invoke(nmsChatSerializerGsonInstance, json, Reflection.getNMSClass("IChatBaseComponent")); + if(nmsPacketPlayOutChatConstructor == null) { + try { + nmsPacketPlayOutChatConstructor = Reflection.getNMSClass("PacketPlayOutChat").getDeclaredConstructor(Reflection.getNMSClass("IChatBaseComponent")); + nmsPacketPlayOutChatConstructor.setAccessible(true); + } catch(NoSuchMethodException e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not find Minecraft method or constructor.", e); + } catch(SecurityException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access constructor.", e); + } + } + return nmsPacketPlayOutChatConstructor.newInstance(serializedChatComponent); + } + + +} diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/LanguageManager.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/LanguageManager.java new file mode 100644 index 0000000..1759d56 --- /dev/null +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/LanguageManager.java @@ -0,0 +1,164 @@ +package nl.evolutioncoding.areashop.messages; + +import com.google.common.base.Charsets; +import nl.evolutioncoding.areashop.AreaShop; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.*; +import java.util.*; + +import static nl.evolutioncoding.areashop.messages.Message.CHATLANGUAGEVARIABLE; + + +public class LanguageManager { + private AreaShop plugin = null; + private String languages[] = {"EN", "NL", "DE", "CS", "FR", "FI", "PL"}; + private Map> currentLanguage, defaultLanguage; + + /** + * Constructor + * @param plugin The AreaShop plugin + */ + public LanguageManager(AreaShop plugin) { + this.plugin = plugin; + startup(); + + } + + /** + * Save the default language files and open the current and backup language file + */ + public void startup() { + this.saveDefaults(); + currentLanguage = loadLanguage(plugin.getConfig().getString("language")); + defaultLanguage = loadLanguage(languages[0]); + } + + /** + * Saves the default language files if not already present + */ + public void saveDefaults() { + // Create the language folder if it not exists + File langFolder; + langFolder = new File(plugin.getDataFolder()+File.separator+AreaShop.languageFolder); + if(!langFolder.exists()) { + if(!langFolder.mkdirs()) { + plugin.getLogger().warning("Could not create language directory: "+langFolder.getAbsolutePath()); + return; + } + } + + // Create the language files, overwrites if a file already exists + // Overriding is necessary because otherwise with an update the new lang + // files would not be used, when translating your own use another + // file name as the default + File langFile; + for(String language : languages) { + langFile = new File(plugin.getDataFolder()+File.separator+AreaShop.languageFolder+File.separator+language+".yml"); + try( + InputStream input = plugin.getResource(AreaShop.languageFolder+"/"+language+".yml"); + OutputStream output = new FileOutputStream(langFile) + ) { + if(input == null) { + plugin.getLogger().warning("Could not save default language to the '"+AreaShop.languageFolder+"' folder: "+language+".yml"); + continue; + } + int read; + byte[] bytes = new byte[1024]; + while((read = input.read(bytes)) != -1) { + output.write(bytes, 0, read); + } + input.close(); + output.close(); + } catch(IOException e) { + plugin.getLogger().warning("Something went wrong saving a default language file: "+langFile.getPath()); + } + } + + } + + /** + * Loads the specified language + * @param key The language to load + */ + public Map> loadLanguage(String key) { + Map> result = new HashMap<>(); + + // Load the strings + File file = new File(plugin.getDataFolder()+File.separator+AreaShop.languageFolder+File.separator+key+".yml"); + try( + InputStreamReader reader = new InputStreamReader(new FileInputStream(file), Charsets.UTF_8) + ) { + YamlConfiguration ymlFile = YamlConfiguration.loadConfiguration(reader); + if(ymlFile.getKeys(false).isEmpty()) { + plugin.getLogger().warning("Language file "+key+".yml has zero messages."); + return result; + } + for(String messageKey : ymlFile.getKeys(false)) { + if(ymlFile.isList(messageKey)) { + result.put(messageKey, new ArrayList<>(ymlFile.getStringList(messageKey))); + } else { + result.put(messageKey, new ArrayList<>(Collections.singletonList(ymlFile.getString(messageKey)))); + } + } + } catch(IOException e) { + plugin.getLogger().warning("Could not load set language file: "+file.getAbsolutePath()); + } + return result; + } + + + /** + * Get the message for a certain key, without doing any processing + * @param key The key of the message to get + * @return The message as a list of strings + */ + public List getRawMessage(String key) { + List message; + if(key.equalsIgnoreCase(CHATLANGUAGEVARIABLE)) { + message = plugin.getChatPrefix(); + } else if(currentLanguage.containsKey(key)) { + message = currentLanguage.get(key); + } else { + message = defaultLanguage.get(key); + } + if(message == null) { + return new ArrayList<>(); + } + return new ArrayList<>(message); + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/Message.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/Message.java new file mode 100644 index 0000000..1d26515 --- /dev/null +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/Message.java @@ -0,0 +1,282 @@ +package nl.evolutioncoding.areashop.messages; + +import nl.evolutioncoding.areashop.AreaShop; +import nl.evolutioncoding.areashop.regions.GeneralRegion; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Message { + + public static final String VARIABLESTART = "%"; + public static final String VARIABLEEND = "%"; + public static final String LANGUAGEVARIABLE = "lang:"; + public static final String CHATLANGUAGEVARIABLE = "prefix"; + public static final int REPLACEMENTLIMIT = 50; + + private List message; + private Object[] replacements; + private String key = null; + + /** + * Internal use only + */ + private Message() { + message = new ArrayList<>(); + } + + /** + * Empty message object + * @return this + */ + public static Message none() { + return new Message(); + } + + /** + * Construct a message from a language key + * @param key The key of the message to use + * @return this + */ + public static Message fromKey(String key) { + return new Message().setMessageFromKey(key); + } + + /** + * Construct a message from a string + * @param message The message to use + * @return this + */ + public static Message fromString(String message) { + AreaShop.debug("fromString: "+message); + return new Message().setMessage(message); + } + + /** + * Construct a message from a string list + * @param message The message to use + * @return this + */ + public static Message fromString(List message) { + return new Message().setMessage(message); + } + + + /** + * Get the message with all replacements done + * @return Message as a list + */ + public List get() { + executeReplacements(); + return message; + } + + /** + * Get a plain string for the message (for example for using in the console) + * @return The message as simple string + */ + public String getPlain() { + executeReplacements(); + return FancyMessageFormat.convertToConsole(message); + } + + /** + * Add the default prefix to the message + * @param doIt true if the prefix should be added, otherwise false + * @return this + */ + public Message prefix(boolean doIt) { + if(doIt) { + message.add(0, VARIABLESTART+LANGUAGEVARIABLE+CHATLANGUAGEVARIABLE+VARIABLEEND); + } + return this; + } + + public Message prefix() { + return prefix(true); + } + + /** + * Set the replacements to apply to the message + * @param replacements The replacements to apply + * - GeneralRegion: All region replacements are applied + * - Message: Message is inserted + * - other: index tag is replaced, like %0% + * @return this + */ + public Message replacements(Object... replacements) { + this.replacements = replacements; + return this; + } + + + /** + * Send the message to a target + * @param target The target to send the message to (Player, CommandSender, Logger) + * @return this + */ + public Message send(Object target) { + if(message == null || message.size() == 0 || (message.size() == 1 && message.get(0).length() == 0)) { + return this; + } + executeReplacements(); + if(target instanceof Player) { + if(AreaShop.getInstance().getConfig().getBoolean("useFancyMessages")) { + FancyMessageSender.sendJSON((Player)target, FancyMessageFormat.convertToJSON(message)); + } else { + ((Player)target).sendMessage(FancyMessageFormat.convertToConsole(message)); + } + } else { + String plainMessage = FancyMessageFormat.convertToConsole(message); + if(!AreaShop.getInstance().getConfig().getBoolean("useColorsInConsole")) { + plainMessage = ChatColor.stripColor(plainMessage); + } + if(target instanceof CommandSender) { + ((CommandSender)target).sendMessage(plainMessage); + } else if(target instanceof Logger) { + ((Logger)target).info(plainMessage); + } else { + AreaShop.getInstance().getLogger().warning("Could not send message, target is wrong: "+plainMessage); + } + } + return this; + } + + + // INTERNAL METHODS + + /** + * Set the internal message + * @param message The message to set + * @return this + */ + private Message setMessage(List message) { + this.message = message; + if(this.message == null) { + this.message = new ArrayList<>(); + } + return this; + } + + /** + * Set the internal message with a key + * @param key The message key to get the message for + * @return this + */ + private Message setMessageFromKey(String key) { + this.key = key; + return this.setMessage(AreaShop.getInstance().getLanguageManager().getRawMessage(key)); + } + + /** + * Set the internal message with a string + * @param message The message to set + * @return this + */ + private Message setMessage(String message) { + List list = new ArrayList<>(); + list.add(message); + return this.setMessage(list); + } + + /** + * Apply all replacements to the message + */ + private void executeReplacements() { + // Replace variables until they are all gone, or when the limit is reached + Pattern variable = Pattern.compile(Pattern.quote(VARIABLESTART)+"[^%\\s]+"+Pattern.quote(VARIABLEEND)); + int round = 0; + + boolean shouldReplace = true; + while(shouldReplace) { + List original = new ArrayList<>(message); + + replaceLanguageVariables(); + replaceArgumentVariables(); + + shouldReplace = !message.equals(original); + round++; + if(round > REPLACEMENTLIMIT) { + AreaShop.getInstance().getLogger().warning("Reached replacement limit for message "+key+", probably has replacements loops, resulting message: "+message.toString()); + break; + } + } + } + + /** + * Replace argument variables in a message + * The arguments to apply as replacements: + * - If it is a GeneralRegion the replacements of the region will be applied + * - Else the parameter will replace its number surrounded with VARIABLESTART and VARIABLEEND + */ + private void replaceArgumentVariables() { + if(message == null || message.size() == 0 || replacements == null) { + return; + } + boolean result = false; + for(int i = 0; i < message.size(); i++) { + int number = 0; + for(Object param : replacements) { + if(param != null) { + if(param instanceof GeneralRegion) { + message.set(i, ((GeneralRegion)param).applyAllReplacements(message.get(i))); + } else if(param instanceof Message) { + Pattern variables = Pattern.compile(Pattern.quote(VARIABLESTART)+i+Pattern.quote(VARIABLEEND)); + Matcher matches = variables.matcher(message.get(i)); + if(matches.find()) { + String variable = matches.group(); + // insert message + FancyMessageFormat.insertMessage(message, ((Message)param).get(), i, matches.start(), matches.end()); + // Reset to start of the line, redo matching because the line changed and the inserted part might contain variables again + i--; + } + number++; + } else { + message.set(i, message.get(i).replace(VARIABLESTART+number+VARIABLEEND, param.toString())); + number++; + } + } + } + } + } + + /** + * Replace all language variables in a message + */ + private void replaceLanguageVariables() { + if(message == null || message.size() == 0) { + return; + } + Pattern variables = Pattern.compile(Pattern.quote(VARIABLESTART)+"lang:[^%\\s]+(\\|[^"+Pattern.quote(VARIABLEEND)+"]*)*"+Pattern.quote(VARIABLEEND)); // Variables cannot contain spaces and percent characters, and area enclosed by percent characters + for(int i = 0; i < message.size(); i++) { + Matcher matches = variables.matcher(message.get(i)); + if(matches.find()) { + String variable = matches.group(); + String key; + Object[] arguments = null; + if(variable.contains("|")) { + key = variable.substring(6, variable.indexOf("|")); + arguments = variable.substring(variable.indexOf("|")+1, variable.length()-1).split("\\|"); + } else { + key = variable.substring(6, variable.length()-1); + } + Message insert = Message.fromKey(key); + if(arguments != null) { + insert.replacements(arguments); + } + + // insert message + //List insert = AreaShop.getInstance().getLanguageManager().getRawMessage(variable.substring(6, variable.length()-1)); + FancyMessageFormat.insertMessage(message, insert.get(), i, matches.start(), matches.end()); + // Reset to start of the line, redo matching because the line changed and the inserted part might contain language tags again + i--; + } + } + } +} diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/Reflection.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/Reflection.java new file mode 100644 index 0000000..21edfe2 --- /dev/null +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/messages/Reflection.java @@ -0,0 +1,219 @@ +package nl.evolutioncoding.areashop.messages; + +import org.bukkit.Bukkit; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Class made by glan3b (Github: https://github.com/glen3b) for the Fanciful project (Github: https://github.com/mkremins/fanciful) + */ + + +/** + * A class containing static utility methods and caches which are intended as reflective conveniences. + * Unless otherwise noted, upon failure methods will return {@code null}. + */ +public final class Reflection { + + private static String _versionString; + + private Reflection() { + + } + + /** + * Gets the version string from the package name of the CraftBukkit server implementation. + * This is needed to bypass the JAR package name changing on each update. + * @return The version string of the OBC and NMS packages, including the trailing dot. + */ + public synchronized static String getVersion() { + if(_versionString == null) { + if(Bukkit.getServer() == null) { + // The server hasn't started, static initializer call? + return null; + } + String name = Bukkit.getServer().getClass().getPackage().getName(); + _versionString = name.substring(name.lastIndexOf('.')+1)+"."; + } + + return _versionString; + } + + /** + * Stores loaded classes from the {@code net.minecraft.server} package. + */ + private static final Map> _loadedNMSClasses = new HashMap>(); + /** + * Stores loaded classes from the {@code org.bukkit.craftbukkit} package (and subpackages). + */ + private static final Map> _loadedOBCClasses = new HashMap>(); + + /** + * Gets a {@link Class} object representing a type contained within the {@code net.minecraft.server} versioned package. + * The class instances returned by this method are cached, such that no lookup will be done twice (unless multiple threads are accessing this method simultaneously). + * @param className The name of the class, excluding the package, within NMS. + * @return The class instance representing the specified NMS class, or {@code null} if it could not be loaded. + */ + public synchronized static Class getNMSClass(String className) { + if(_loadedNMSClasses.containsKey(className)) { + return _loadedNMSClasses.get(className); + } + + String fullName = "net.minecraft.server."+getVersion()+className; + Class clazz = null; + try { + clazz = Class.forName(fullName); + } catch(Exception e) { + e.printStackTrace(); + _loadedNMSClasses.put(className, null); + return null; + } + _loadedNMSClasses.put(className, clazz); + return clazz; + } + + /** + * Gets a {@link Class} object representing a type contained within the {@code org.bukkit.craftbukkit} versioned package. + * The class instances returned by this method are cached, such that no lookup will be done twice (unless multiple threads are accessing this method simultaneously). + * @param className The name of the class, excluding the package, within OBC. This name may contain a subpackage name, such as {@code inventory.CraftItemStack}. + * @return The class instance representing the specified OBC class, or {@code null} if it could not be loaded. + */ + public synchronized static Class getOBCClass(String className) { + if(_loadedOBCClasses.containsKey(className)) { + return _loadedOBCClasses.get(className); + } + + String fullName = "org.bukkit.craftbukkit."+getVersion()+className; + Class clazz = null; + try { + clazz = Class.forName(fullName); + } catch(Exception e) { + e.printStackTrace(); + _loadedOBCClasses.put(className, null); + return null; + } + _loadedOBCClasses.put(className, clazz); + return clazz; + } + + /** + * Attempts to get the NMS handle of a CraftBukkit object. + *

+ * The only match currently attempted by this method is a retrieval by using a parameterless {@code getHandle()} method implemented by the runtime type of the specified object. + *

+ * @param obj The object for which to retrieve an NMS handle. + * @return The NMS handle of the specified object, or {@code null} if it could not be retrieved using {@code getHandle()}. + */ + public synchronized static Object getHandle(Object obj) { + try { + return getMethod(obj.getClass(), "getHandle").invoke(obj); + } catch(Exception e) { + e.printStackTrace(); + return null; + } + } + + private static final Map, Map> _loadedFields = new HashMap, Map>(); + + /** + * Retrieves a {@link Field} instance declared by the specified class with the specified name. + * Java access modifiers are ignored during this retrieval. No guarantee is made as to whether the field + * returned will be an instance or static field. + *

+ * A global caching mechanism within this class is used to store fields. Combined with synchronization, this guarantees that + * no field will be reflectively looked up twice. + *

+ *

+ * If a field is deemed suitable for return, {@link Field#setAccessible(boolean) setAccessible} will be invoked with an argument of {@code true} before it is returned. + * This ensures that callers do not have to check or worry about Java access modifiers when dealing with the returned instance. + *

+ * @param clazz The class which contains the field to retrieve. + * @param name The declared name of the field in the class. + * @return A field object with the specified name declared by the specified class. + * @see Class#getDeclaredField(String) + */ + public synchronized static Field getField(Class clazz, String name) { + Map loaded; + if(!_loadedFields.containsKey(clazz)) { + loaded = new HashMap(); + _loadedFields.put(clazz, loaded); + } else { + loaded = _loadedFields.get(clazz); + } + if(loaded.containsKey(name)) { + // If the field is loaded (or cached as not existing), return the relevant value, which might be null + return loaded.get(name); + } + try { + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + loaded.put(name, field); + return field; + } catch(Exception e) { + // Error loading + e.printStackTrace(); + // Cache field as not existing + loaded.put(name, null); + return null; + } + } + + /** + * Contains loaded methods in a cache. + * The map maps [types to maps of [method names to maps of [parameter types to method instances]]]. + */ + private static final Map, Map>, Method>>> _loadedMethods = new HashMap, Map>, Method>>>(); + + /** + * Retrieves a {@link Method} instance declared by the specified class with the specified name and argument types. + * Java access modifiers are ignored during this retrieval. No guarantee is made as to whether the field + * returned will be an instance or static field. + *

+ * A global caching mechanism within this class is used to store method. Combined with synchronization, this guarantees that + * no method will be reflectively looked up twice. + *

+ *

+ * If a method is deemed suitable for return, {@link Method#setAccessible(boolean) setAccessible} will be invoked with an argument of {@code true} before it is returned. + * This ensures that callers do not have to check or worry about Java access modifiers when dealing with the returned instance. + *

+ *

+ * This method does not search superclasses of the specified type for methods with the specified signature. + * Callers wishing this behavior should use {@link Class#getDeclaredMethod(String, Class...)}. + * @param clazz The class which contains the method to retrieve. + * @param name The declared name of the method in the class. + * @param args The formal argument types of the method. + * @return A method object with the specified name declared by the specified class. + */ + public synchronized static Method getMethod(Class clazz, String name, + Class... args) { + if(!_loadedMethods.containsKey(clazz)) { + _loadedMethods.put(clazz, new HashMap>, Method>>()); + } + + Map>, Method>> loadedMethodNames = _loadedMethods.get(clazz); + if(!loadedMethodNames.containsKey(name)) { + loadedMethodNames.put(name, new HashMap>, Method>()); + } + + Map>, Method> loadedSignatures = loadedMethodNames.get(name); + ArrayWrapper> wrappedArg = new ArrayWrapper>(args); + if(loadedSignatures.containsKey(wrappedArg)) { + return loadedSignatures.get(wrappedArg); + } + + for(Method m : clazz.getMethods()) { + if(m.getName().equals(name) && Arrays.equals(args, m.getParameterTypes())) { + m.setAccessible(true); + loadedSignatures.put(wrappedArg, m); + return m; + } + } + loadedSignatures.put(wrappedArg, null); + return null; + } + +} \ No newline at end of file diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/BuyRegion.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/BuyRegion.java index 5e6de44..13a0df6 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/BuyRegion.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/BuyRegion.java @@ -243,7 +243,7 @@ public class BuyRegion extends GeneralRegion { * @return String indicating the inactive time until unrent */ public String getFormattedInactiveTimeUntilSell() { - return this.millisToHumanFormat(getInactiveTimeUntilSell()); + return Utils.millisToHumanFormat(getInactiveTimeUntilSell()); } /** diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/GeneralRegion.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/GeneralRegion.java index b72bd30..c5221e0 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/GeneralRegion.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/GeneralRegion.java @@ -9,6 +9,7 @@ import nl.evolutioncoding.areashop.events.NotifyAreaShopEvent; import nl.evolutioncoding.areashop.events.notify.RegionUpdateEvent; import nl.evolutioncoding.areashop.interfaces.GeneralRegionInterface; import nl.evolutioncoding.areashop.managers.FileManager; +import nl.evolutioncoding.areashop.messages.Message; import org.apache.commons.lang.exception.ExceptionUtils; import org.bukkit.*; import org.bukkit.block.Block; @@ -23,8 +24,6 @@ import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public abstract class GeneralRegion implements GeneralRegionInterface, Comparable { YamlConfiguration config; @@ -549,23 +548,6 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl if(source == null || source.length() == 0) { return ""; } - // Apply language replacements - Pattern regex = Pattern.compile("%lang:[^% [-]]+%"); - Matcher matcher = regex.matcher(source); - while(matcher.find()) { - String match = matcher.group(); - String key = match.substring(6, match.length()-1); - String languageString; - if(key.equalsIgnoreCase("prefix")) { - languageString = plugin.getChatPrefix(); - } else { - languageString = plugin.getLanguageManager().getLang(key); - } - if(languageString != null) { - source = source.replace(match, languageString); - } - //AreaShop.debug("match=" + match + ", key=" + key + ", lanString=" + languageString + ", replaced=" + source); - } // Apply static replacements HashMap replacements = getAllReplacements(); for(String tag : replacements.keySet()) { @@ -781,7 +763,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl Object[] newParams = new Object[params.length + 1]; newParams[0] = this; System.arraycopy(params, 0, newParams, 1, params.length); - plugin.configurableMessage(target, key, prefix, newParams); + Message.fromKey(key).prefix(prefix).replacements(newParams).send(target); } public void messageNoPrefix(Object target, String key, Object... params) { @@ -892,47 +874,6 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl return result; } - /** - * Convert milliseconds to a human readable format - * @param milliseconds The amount of milliseconds to convert - * @return A formatted string based on the language file - */ - public String millisToHumanFormat(long milliseconds) { - long timeLeft = milliseconds + 500; - // To seconds - timeLeft = timeLeft/1000; - if(timeLeft <= 0) { - return plugin.getLanguageManager().getLang("timeleft-ended"); - } else if(timeLeft == 1) { - return plugin.getLanguageManager().getLang("timeleft-second", timeLeft); - } else if(timeLeft <= 120) { - return plugin.getLanguageManager().getLang("timeleft-seconds", timeLeft); - } - // To minutes - timeLeft = timeLeft/60; - if(timeLeft <= 120) { - return plugin.getLanguageManager().getLang("timeleft-minutes", timeLeft); - } - // To hours - timeLeft = timeLeft/60; - if(timeLeft <= 48) { - return plugin.getLanguageManager().getLang("timeleft-hours", timeLeft); - } - // To days - timeLeft = timeLeft/24; - if(timeLeft <= 60) { - return plugin.getLanguageManager().getLang("timeleft-days", timeLeft); - } - // To months - timeLeft = timeLeft/30; - if(timeLeft <= 24) { - return plugin.getLanguageManager().getLang("timeleft-months", timeLeft); - } - // To years - timeLeft = timeLeft/12; - return plugin.getLanguageManager().getLang("timeleft-years", timeLeft); - } - /** * Reset all flags of the region */ @@ -1123,7 +1064,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl if(safeLocation.getBlockY()>256 || safeLocation.getBlockY()<0) { continue; } - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1141,7 +1082,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl if(safeLocation.getBlockY()>256 || safeLocation.getBlockY()<0) { continue; } - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1159,7 +1100,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl if(safeLocation.getBlockY()>256 || safeLocation.getBlockY()<0) { continue; } - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1177,7 +1118,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl if(safeLocation.getBlockY()>256 || safeLocation.getBlockY()<0) { continue; } - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1195,7 +1136,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl } if(!done && !top) { safeLocation = startLocation.clone().add(0, radius, 0); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1206,7 +1147,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl // North for(int x=-r+1; x<=r && !done; x++) { safeLocation = startLocation.clone().add(x, radius, -r); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1216,7 +1157,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl // East for(int z=-r+1; z<=r && !done; z++) { safeLocation = startLocation.clone().add(r, radius, z); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1226,7 +1167,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl // South side for(int x=r-1; x>=-r && !done; x--) { safeLocation = startLocation.clone().add(x, radius, r); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1236,7 +1177,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl // West side for(int z=r-1; z>=-r && !done; z--) { safeLocation = startLocation.clone().add(-r, radius, z); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1254,7 +1195,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl } if(!done && !bottom) { safeLocation = startLocation.clone().add(0, -radius, 0); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1265,7 +1206,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl // North for(int x=-r+1; x<=r && !done; x++) { safeLocation = startLocation.clone().add(x, -radius, -r); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1275,7 +1216,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl // East for(int z=-r+1; z<=r && !done; z++) { safeLocation = startLocation.clone().add(r, -radius, z); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1285,7 +1226,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl // South side for(int x=r-1; x>=-r && !done; x--) { safeLocation = startLocation.clone().add(x, -radius, r); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; @@ -1295,7 +1236,7 @@ public abstract class GeneralRegion implements GeneralRegionInterface, Comparabl // West side for(int z=r-1; z>=-r && !done; z--) { safeLocation = startLocation.clone().add(-r, -radius, z); - if((insideRegion && region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ())) || !insideRegion) { + if(region.contains(safeLocation.getBlockX(), safeLocation.getBlockY(), safeLocation.getBlockZ()) || !insideRegion) { checked++; done = isSafe(safeLocation) || checked > maxTries; blocksInRegion = true; diff --git a/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/RentRegion.java b/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/RentRegion.java index 9d332ad..6c236f8 100644 --- a/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/RentRegion.java +++ b/AreaShop/src/main/java/nl/evolutioncoding/areashop/regions/RentRegion.java @@ -21,6 +21,8 @@ import java.util.Date; import java.util.HashMap; import java.util.UUID; +import static nl.evolutioncoding.areashop.Utils.millisToHumanFormat; + public class RentRegion extends GeneralRegion { private long warningsDoneUntil = Calendar.getInstance().getTimeInMillis(); @@ -154,7 +156,7 @@ public class RentRegion extends GeneralRegion { } result.put(AreaShop.tagMaxExtends, this.getMaxExtends()); result.put(AreaShop.tagExtendsLeft, getMaxExtends() - getTimesExtended()); - result.put(AreaShop.tagMaxRentTime, this.millisToHumanFormat(getMaxRentTime())); + result.put(AreaShop.tagMaxRentTime, millisToHumanFormat(getMaxRentTime())); result.put(AreaShop.tagMaxInactiveTime, this.getFormattedInactiveTimeUntilUnrent()); return result; } @@ -251,7 +253,7 @@ public class RentRegion extends GeneralRegion { * @return Time left on the rent, for example '29 days', '3 months', '1 second' */ public String getTimeLeftString() { - return millisToHumanFormat(getTimeLeft()); + return Utils.millisToHumanFormat(getTimeLeft()); } /** @@ -267,7 +269,7 @@ public class RentRegion extends GeneralRegion { * @return String indicating the inactive time until unrent */ public String getFormattedInactiveTimeUntilUnrent() { - return this.millisToHumanFormat(getInactiveTimeUntilUnrent()); + return Utils.millisToHumanFormat(getInactiveTimeUntilUnrent()); } /** diff --git a/AreaShop/src/main/resources/config.yml b/AreaShop/src/main/resources/config.yml index 9e2bb7d..330157e 100644 --- a/AreaShop/src/main/resources/config.yml +++ b/AreaShop/src/main/resources/config.yml @@ -7,7 +7,12 @@ # │ GENERAL: Options that influence the global state of the plugin │ # └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ## Chatprefix used for all messages in the chat, also changes the greeting messages. -chatPrefix: '&2[AreaShop]&r ' +chatPrefix: + - '&2[AreaShop]&r' + - ' hover: &fAreaShop region management plugin' + - ' hover: &fClick to check the available commands' + - ' command: /areashop help' + - ' ' ## The language file that should be used, check the 'lang' folder for build-in languages (use the filename without .yml here). ## More information can be found here: https://github.com/NLthijs48/AreaShop/wiki/Language-support. language: EN @@ -319,6 +324,8 @@ sendStats: true checkForUpdates: true ## Use colors when sending messages to console and log files. useColorsInConsole: false +## Use tellraw style messages +useFancyMessages: true ## Post error messages in the console when a command run from the config fails (from the 'runCommands' section for example). postCommandErrors: true ## Update all region flags and signs after starting the plugin (uses the 'regionsPerTick' setting from the 'update' section). diff --git a/AreaShop/src/main/resources/lang/EN.yml b/AreaShop/src/main/resources/lang/EN.yml index 2f3eb00..d97555d 100644 --- a/AreaShop/src/main/resources/lang/EN.yml +++ b/AreaShop/src/main/resources/lang/EN.yml @@ -1,4 +1,4 @@ -# ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ +# ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ # ║ Language file of the AreaShop plugin created by NLThijs48, Github can be found at https://github.com/NLthijs48/AreaShop ║ # ║ Language: English, Version: V2.2.2, Percentage translated: 100% (source), author: NLThijs48 ║ # ║ This file will be overwritten at each startup/reload of the plugin, if you want to change anything then do the following: ║ @@ -8,6 +8,11 @@ # ║ 4: Change the strings in the new file to your liking and save the file ║ # ║ 5: Use '/as reload' or reload/restart your server to see the changes ║ # ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ +command: + - "&6%0% &7-&r" + - " hover: &9&l" + - " command: %0%" + total-maximum: "You can't rent and buy more than %0% region(s) in total (you already have %1% in group '%2%')." general-notReady: "AreaShop has not fully loaded yet, please wait." @@ -29,45 +34,45 @@ cmd-automaticRegionOnlyByPlayer: "Automatically determining the region is only p help-header: "Help page, commands that you can execute." help-alias: "Command aliases: /areashop, /as." -help-help: "&6/as help &7-&r Shows this help page." -help-info: "&6/as info &7-&r Get info about current regions." -help-rent: "&6/as rent &7-&r Rent a region or extend your current rent." -help-buy: "&6/as buy &7-&r Buy a region." -help-unrent: "&6/as unrent &7-&r Unrent a region." -help-unrentOwn: "&6/as unrent &7-&r Unrent your own region." -help-sell: "&6/as sell &7-&r Sell a region." -help-sellOwn: "&6/as sell &7-&r Sell your own region." -help-reload: "&6/as reload &7-&r Reload all files and update the regions." -help-setrestore: "&6/as setrestore &7-&r Set restoring on/off and choose profile." -help-setprice: "&6/as setprice &7-&r Change the price of a region." -help-setduration: "&6/as setduration &7-&r Change the duration of a rent region." -help-teleport: "&6/as tp &7-&r Teleport to your bought/rented regions." -help-teleportAll: "&6/as tp &7-&r Teleport to a rent/buy region." -help-setteleport: "&6/as settp &7-&r Set teleport position for bought/rented regions." -help-setteleportAll: "&6/as settp &7-&r Set teleport position for a region." -help-find: "&6/as find &7-&r Find an empty buy or rent." -help-groupadd: "&6/as groupadd &7-&r Add a region to a group." -help-groupdel: "&6/as groupdel &7-&r Delete a region from a group." -help-grouplist: "&6/as grouplist &7-&r Display all groups currently registered." -help-groupinfo: "&6/as groupinfo &7-&r Display information about a group." -help-schemevent: "&6/as schemevent &7-&r Trigger a schematic event for a region." -help-add: "&6/as add &7-&r Register a region as rent or buy." -help-del: "&6/as del &7-&r Delete a registered region from AreaShop." -help-addsign: "&6/as addsign &7-&r Add a sign to an existing region." -help-delsign: "&6/as delsign &7-&r Delete the sign you are looking at." -help-me: "&6/as me &7-&r Check which regions you have (+expiration)." -help-setowner: "&6/as setowner &7-&r Set region owner or extend the rent." -help-resell: "&6/as resell &7-&r Put one of your regions into resell mode." -help-resellAll: "&6/as resell &7-&r Put a region into resell mode." -help-stopResell: "&6/as stopresell &7-&r Put your region back into sold mode." -help-stopResellAll: "&6/as stopresell &7-&r Put a region back into sold mode." -help-addFriend: "&6/as addfriend &7-&r Add a friend to your region." -help-addFriendAll: "&6/as addfriend &7-&r Add a friend to a region." -help-delFriend: "&6/as delfriend &7-&r Delete a friend from your region." -help-delFriendAll: "&6/as delfriend &7-&r Delete a friend from a region." -help-linksigns: "&6/as linksigns &7-&r Use bulk sign linking mode." -help-stack: "&6/as stack &7-&r Create multiple regions and add them." -help-setlandlord: "&6/as setlandlord &7-&r Set the landlord of a region." +help-help: "%lang:command|/as help% Shows this help page." +help-info: "%lang:command|/as info% Get info about current regions." +help-rent: "%lang:command|/as rent% Rent a region or extend your current rent." +help-buy: "%lang:command|/as buy% Buy a region." +help-unrent: "%lang:command|/as unrent% Unrent a region." +help-unrentOwn: "%lang:command|/as unrent% Unrent your own region." +help-sell: "%lang:command|/as sell% Sell a region." +help-sellOwn: "%lang:command|/as sell% Sell your own region." +help-reload: "%lang:command|/as reload% Reload all files and update the regions." +help-setrestore: "%lang:command|/as setrestore% Set restoring on/off and choose profile." +help-setprice: "%lang:command|/as setprice% Change the price of a region." +help-setduration: "%lang:command|/as setduration% Change the duration of a rent region." +help-teleport: "%lang:command|/as tp% Teleport to your bought/rented regions." +help-teleportAll: "%lang:command|/as tp% Teleport to a rent/buy region." +help-setteleport: "%lang:command|/as settp% Set teleport position for bought/rented regions." +help-setteleportAll: "%lang:command|/as settp% Set teleport position for a region." +help-find: "%lang:command|/as find% Find an empty buy or rent." +help-groupadd: "%lang:command|/as groupadd% Add a region to a group." +help-groupdel: "%lang:command|/as groupdel% Delete a region from a group." +help-grouplist: "%lang:command|/as grouplist% Display all groups currently registered." +help-groupinfo: "%lang:command|/as groupinfo% Display information about a group." +help-schemevent: "%lang:command|/as schemevent% Trigger a schematic event for a region." +help-add: "%lang:command|/as add% Register a region as rent or buy." +help-del: "%lang:command|/as del% Delete a registered region from AreaShop." +help-addsign: "%lang:command|/as addsign% Add a sign to an existing region." +help-delsign: "%lang:command|/as delsign% Delete the sign you are looking at." +help-me: "%lang:command|/as me% Check which regions you have (+expiration)." +help-setowner: "%lang:command|/as setowner% Set region owner or extend the rent." +help-resell: "%lang:command|/as resell% Put one of your regions into resell mode." +help-resellAll: "%lang:command|/as resell% Put a region into resell mode." +help-stopResell: "%lang:command|/as stopresell% Put your region back into sold mode." +help-stopResellAll: "%lang:command|/as stopresell% Put a region back into sold mode." +help-addFriend: "%lang:command|/as addfriend% Add a friend to your region." +help-addFriendAll: "%lang:command|/as addfriend% Add a friend to a region." +help-delFriend: "%lang:command|/as delfriend% Delete a friend from your region." +help-delFriendAll: "%lang:command|/as delfriend% Delete a friend from a region." +help-linksigns: "%lang:command|/as linksigns% Use bulk sign linking mode." +help-stack: "%lang:command|/as stack% Create multiple regions and add them." +help-setlandlord: "%lang:command|/as setlandlord% Set the landlord of a region." rent-help: "/as rent [regionname], the region you stand in will be used if not specified." rent-noPermission: "You don't have permission to rent a region." @@ -100,7 +105,7 @@ buy-succes: "You successfully bought %region%." buy-successResale: "You successfully bought %region% from %0%." buy-successSeller: "Your region %region% has been sold to %player% for %0%." buy-lowMoney: "You don't have enough money to buy this region (you have %0% and you need %price%)." -buy-lowMoneyResell: "You don't have enugh money to buy this region (you have %0% and you need %resellprice%)." +buy-lowMoneyResell: "You don't have enough money to buy this region (you have %0% and you need %resellprice%)." buy-yours: "You already own this region." buy-someoneElse: "Someone else already bought this region." buy-restrictedToWorld: "You need to be in the '%world%' world to buy this region (you are in '%0%')."