commit 37550361379bb5d929066d5f33f0422b0ec252f4 Author: KaiKikuchi Date: Sun Jul 5 16:05:58 2015 +0200 Version 0.9 Compatible with Bukkit 1.8.7 Improved items name display, especially for very long names. diff --git a/README.md b/README.md new file mode 100644 index 0000000..92e13a7 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +#QuickShop +QuickShop is a shop plugin, that allows players to sell items from a chest with no commands. It allows players to purchase any number of items easily. In fact, this plugin doesn't even have any commands that a player would ever need! + +##Support +Open a new issue here: https://github.com/KaiKikuchi/QuickShop/issues + +##Features +- Easy to use +- Togglable Display Item on top of chest +- NBT Data, Enchants, Tool Damage, Potion and Mob Egg support +- Unlimited chest support +- Blacklist support & bypass permissions +- Shops that buy items and sell items at the same time are possible (Using double chests) +- Herochat support +- Checks a player can open a chest before letting them create a shop! +- UUID support + +##Bukkit page +You can find the project page here: http://dev.bukkit.org/bukkit-plugins/quickshop-notlikeme/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..99e20fc --- /dev/null +++ b/pom.xml @@ -0,0 +1,56 @@ + + 4.0.0 + net.kaikk.mc + ${name} + ${version} + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + + + src/main/resources + true + + + + + + bukkit-repo + + https://hub.spigotmc.org/nexus/content/groups/public/ + + + sk89q-repo + http://maven.sk89q.com/repo/ + + + + + org.bukkit + bukkit + + + 1.8-R0.1-SNAPSHOT + jar + provided + + + com.sk89q + worldguard + 6.0.0-SNAPSHOT + + + + 0.9 + org.maxgamer.quickshop + QuickShop + + \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Command/QS.java b/src/main/java/org/maxgamer/quickshop/Command/QS.java new file mode 100644 index 0000000..3e2ce5f --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Command/QS.java @@ -0,0 +1,573 @@ +package org.maxgamer.quickshop.Command; + +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.block.Block; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.bukkit.util.BlockIterator; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Database.Database; +import org.maxgamer.quickshop.Database.MySQLCore; +import org.maxgamer.quickshop.Database.SQLiteCore; +import org.maxgamer.quickshop.Shop.ContainerShop; +import org.maxgamer.quickshop.Shop.Shop; +import org.maxgamer.quickshop.Shop.ShopType; +import org.maxgamer.quickshop.Shop.ShopChunk; +import org.maxgamer.quickshop.Util.MsgUtil; + +public class QS implements CommandExecutor { + QuickShop plugin; + + public QS(QuickShop plugin) { + this.plugin = plugin; + } + + private void setUnlimited(CommandSender sender) { + if (sender instanceof Player && sender.hasPermission("quickshop.unlimited")) { + BlockIterator bIt = new BlockIterator((Player) sender, 10); + while (bIt.hasNext()) { + Block b = bIt.next(); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null) { + shop.setUnlimited(!shop.isUnlimited()); + shop.update(); + sender.sendMessage(MsgUtil.getMessage("command.toggle-unlimited", (shop.isUnlimited() ? "unlimited" : "limited"))); + return; + } + } + sender.sendMessage(MsgUtil.getMessage("not-looking-at-shop")); + return; + } else { + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + } + + private void remove(CommandSender sender, String[] args) { + if (sender instanceof Player == false) { + sender.sendMessage(ChatColor.RED + "Only players may use that command."); + return; + } + if (!sender.hasPermission("quickshop.delete")) { + sender.sendMessage(ChatColor.RED + "You do not have permission to use that command. Try break the shop instead?"); + return; + } + Player p = (Player) sender; + BlockIterator bIt = new BlockIterator(p, 10); + while (bIt.hasNext()) { + Block b = bIt.next(); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null) { + if (shop.getOwner().equals(p.getUniqueId())) { + shop.delete(); + sender.sendMessage(ChatColor.GREEN + "Success. Deleted shop."); + } else { + p.sendMessage(ChatColor.RED + "That's not your shop!"); + } + return; + } + } + p.sendMessage(ChatColor.RED + "No shop found!"); + } + + private void export(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(ChatColor.RED + "Usage: /qs export mysql|sqlite"); + return; + } + String type = args[1].toLowerCase(); + if (type.startsWith("mysql")) { + if (plugin.getDB().getCore() instanceof MySQLCore) { + sender.sendMessage(ChatColor.RED + "Database is already MySQL"); + return; + } + ConfigurationSection cfg = plugin.getConfig().getConfigurationSection("database"); + String host = cfg.getString("host"); + String port = cfg.getString("port"); + String user = cfg.getString("user"); + String pass = cfg.getString("password"); + String name = cfg.getString("database"); + MySQLCore core = new MySQLCore(host, user, pass, name, port); + Database target; + try { + target = new Database(core); + QuickShop.instance.getDB().copyTo(target); + sender.sendMessage(ChatColor.GREEN + "Success - Exported to MySQL " + user + "@" + host + "." + name); + } catch (Exception e) { + e.printStackTrace(); + sender.sendMessage(ChatColor.RED + "Failed to export to MySQL " + user + "@" + host + "." + name + ChatColor.DARK_RED + " Reason: " + e.getMessage()); + } + return; + } + if (type.startsWith("sql") || type.contains("file")) { + if (plugin.getDB().getCore() instanceof SQLiteCore) { + sender.sendMessage(ChatColor.RED + "Database is already SQLite"); + return; + } + File file = new File(plugin.getDataFolder(), "shops.db"); + if (file.exists()) { + if (file.delete() == false) { + sender.sendMessage(ChatColor.RED + "Warning: Failed to delete old shops.db file. This may cause errors."); + } + } + SQLiteCore core = new SQLiteCore(file); + try { + Database target = new Database(core); + QuickShop.instance.getDB().copyTo(target); + sender.sendMessage(ChatColor.GREEN + "Success - Exported to SQLite: " + file.toString()); + } catch (Exception e) { + e.printStackTrace(); + sender.sendMessage(ChatColor.RED + "Failed to export to SQLite: " + file.toString() + " Reason: " + e.getMessage()); + } + return; + } + sender.sendMessage(ChatColor.RED + "No target given. Usage: /qs export mysql|sqlite"); + } + + private void setOwner(CommandSender sender, String[] args) { + if (sender instanceof Player && sender.hasPermission("quickshop.setowner")) { + if (args.length < 2) { + sender.sendMessage(MsgUtil.getMessage("command.no-owner-given")); + return; + } + BlockIterator bIt = new BlockIterator((Player) sender, 10); + while (bIt.hasNext()) { + Block b = bIt.next(); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null) { + @SuppressWarnings("deprecation") + OfflinePlayer p = this.plugin.getServer().getOfflinePlayer(args[1]); + shop.setOwner(p.getUniqueId()); + shop.update(); + sender.sendMessage(MsgUtil.getMessage("command.new-owner", this.plugin.getServer().getOfflinePlayer(shop.getOwner()).getName())); + return; + } + } + sender.sendMessage(MsgUtil.getMessage("not-looking-at-shop")); + return; + } else { + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + } + + private void refill(CommandSender sender, String[] args) { + if (sender instanceof Player && sender.hasPermission("quickshop.refill")) { + if (args.length < 2) { + sender.sendMessage(MsgUtil.getMessage("command.no-amount-given")); + return; + } + int add; + try { + add = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + sender.sendMessage(MsgUtil.getMessage("thats-not-a-number")); + return; + } + BlockIterator bIt = new BlockIterator((LivingEntity) (Player) sender, 10); + while (bIt.hasNext()) { + Block b = bIt.next(); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null) { + shop.add(shop.getItem(), add); + sender.sendMessage(MsgUtil.getMessage("refill-success")); + return; + } + } + sender.sendMessage(MsgUtil.getMessage("not-looking-at-shop")); + return; + } else { + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + } + + private void empty(CommandSender sender, String[] args) { + if (sender instanceof Player && sender.hasPermission("quickshop.refill")) { + BlockIterator bIt = new BlockIterator((LivingEntity) (Player) sender, 10); + while (bIt.hasNext()) { + Block b = bIt.next(); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null) { + if (shop instanceof ContainerShop) { + ContainerShop cs = (ContainerShop) shop; + cs.getInventory().clear(); + sender.sendMessage(MsgUtil.getMessage("empty-success")); + return; + } else { + sender.sendMessage(MsgUtil.getMessage("not-looking-at-shop")); + return; + } + } + } + sender.sendMessage(MsgUtil.getMessage("not-looking-at-shop")); + return; + } else { + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + } + + private void find(CommandSender sender, String[] args) { + if (sender instanceof Player && sender.hasPermission("quickshop.find")) { + if (args.length < 2) { + sender.sendMessage(MsgUtil.getMessage("command.no-type-given")); + return; + } + StringBuilder sb = new StringBuilder(args[1]); + for (int i = 2; i < args.length; i++) { + sb.append(" " + args[i]); + } + String lookFor = sb.toString(); + lookFor = lookFor.toLowerCase(); + Player p = (Player) sender; + Location loc = p.getEyeLocation().clone(); + double minDistance = plugin.getConfig().getInt("shop.find-distance"); + double minDistanceSquared = minDistance * minDistance; + int chunkRadius = (int) minDistance / 16 + 1; + Shop closest = null; + Chunk c = loc.getChunk(); + for (int x = -chunkRadius + c.getX(); x < chunkRadius + c.getX(); x++) { + for (int z = -chunkRadius + c.getZ(); z < chunkRadius + c.getZ(); z++) { + Chunk d = c.getWorld().getChunkAt(x, z); + HashMap inChunk = plugin.getShopManager().getShops(d); + if (inChunk == null) + continue; + for (Shop shop : inChunk.values()) { + if (shop.getDataName().toLowerCase().contains(lookFor) && shop.getLocation().distanceSquared(loc) < minDistanceSquared) { + closest = shop; + minDistanceSquared = shop.getLocation().distanceSquared(loc); + } + } + } + } + if (closest == null) { + sender.sendMessage(MsgUtil.getMessage("no-nearby-shop", args[1])); + return; + } + Location lookat = closest.getLocation().clone().add(0.5, 0.5, 0.5); + // Hack fix to make /qs find not used by /back + p.teleport(this.lookAt(loc, lookat).add(0, -1.62, 0), TeleportCause.UNKNOWN); + p.sendMessage(MsgUtil.getMessage("nearby-shop-this-way", "" + (int) Math.floor(Math.sqrt(minDistanceSquared)))); + return; + } else { + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + } + + private void setBuy(CommandSender sender) { + if (sender instanceof Player && sender.hasPermission("quickshop.create.buy")) { + BlockIterator bIt = new BlockIterator((LivingEntity) (Player) sender, 10); + while (bIt.hasNext()) { + Block b = bIt.next(); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null && shop.getOwner().equals(((Player) sender).getUniqueId())) { + shop.setShopType(ShopType.BUYING); + shop.setSignText(); + shop.update(); + sender.sendMessage(MsgUtil.getMessage("command.now-buying", shop.getDataName())); + return; + } + } + sender.sendMessage(MsgUtil.getMessage("not-looking-at-shop")); + return; + } + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + + private void setSell(CommandSender sender) { + if (sender instanceof Player && sender.hasPermission("quickshop.create.sell")) { + BlockIterator bIt = new BlockIterator((LivingEntity) (Player) sender, 10); + while (bIt.hasNext()) { + Block b = bIt.next(); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null && shop.getOwner().equals(((Player) sender).getUniqueId())) { + shop.setShopType(ShopType.SELLING); + shop.setSignText(); + shop.update(); + sender.sendMessage(MsgUtil.getMessage("command.now-selling", shop.getDataName())); + return; + } + } + sender.sendMessage(MsgUtil.getMessage("not-looking-at-shop")); + return; + } + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + + @SuppressWarnings("deprecation") + private void setPrice(CommandSender sender, String[] args) { + if (sender instanceof Player && sender.hasPermission("quickshop.create.changeprice")) { + Player p = (Player) sender; + if (args.length < 2) { + sender.sendMessage(MsgUtil.getMessage("no-price-given")); + return; + } + double price; + try { + price = Double.parseDouble(args[1]); + } catch (NumberFormatException e) { + sender.sendMessage(MsgUtil.getMessage("thats-not-a-number")); + return; + } + if (price < 0.01) { + sender.sendMessage(MsgUtil.getMessage("price-too-cheap")); + return; + } + double fee = 0; + if (plugin.priceChangeRequiresFee) { + fee = plugin.getConfig().getDouble("shop.fee-for-price-change"); + if (fee > 0 && plugin.getEcon().getBalance(p.getUniqueId()) < fee) { + sender.sendMessage(MsgUtil.getMessage("you-cant-afford-to-change-price", plugin.getEcon().format(fee))); + return; + } + } + BlockIterator bIt = new BlockIterator(p, 10); + // Loop through every block they're looking at upto 10 blocks away + while (bIt.hasNext()) { + Block b = bIt.next(); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null && (shop.getOwner().equals(((Player) sender).getUniqueId()) || sender.hasPermission("quickshop.other.price"))) { + if (shop.getPrice() == price) { + // Stop here if there isn't a price change + sender.sendMessage(MsgUtil.getMessage("no-price-change")); + return; + } + if (fee > 0) { + if (!plugin.getEcon().withdraw(p.getUniqueId(), fee)) { + sender.sendMessage(MsgUtil.getMessage("you-cant-afford-to-change-price", plugin.getEcon().format(fee))); + return; + } + sender.sendMessage(MsgUtil.getMessage("fee-charged-for-price-change", plugin.getEcon().format(fee))); + plugin.getEcon().deposit(plugin.getConfig().getString("tax-account"), fee); + } + // Update the shop + shop.setPrice(price); + shop.setSignText(); + shop.update(); + sender.sendMessage(MsgUtil.getMessage("price-is-now", plugin.getEcon().format(shop.getPrice()))); + // Chest shops can be double shops. + if (shop instanceof ContainerShop) { + ContainerShop cs = (ContainerShop) shop; + if (cs.isDoubleShop()) { + Shop nextTo = cs.getAttachedShop(); + if (cs.isSelling()) { + if (cs.getPrice() < nextTo.getPrice()) { + sender.sendMessage(MsgUtil.getMessage("buying-more-than-selling")); + } + } else { + // Buying + if (cs.getPrice() > nextTo.getPrice()) { + sender.sendMessage(MsgUtil.getMessage("buying-more-than-selling")); + } + } + } + } + return; + } + } + sender.sendMessage(MsgUtil.getMessage("not-looking-at-shop")); + return; + } + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + + private void clean(CommandSender sender) { + if (sender.hasPermission("quickshop.clean")) { + sender.sendMessage(MsgUtil.getMessage("command.cleaning")); + Iterator shIt = plugin.getShopManager().getShopIterator(); + int i = 0; + while (shIt.hasNext()) { + Shop shop = shIt.next(); + if (shop.getLocation().getWorld() != null && shop.isSelling() && shop.getRemainingStock() == 0 && shop instanceof ContainerShop) { + ContainerShop cs = (ContainerShop) shop; + if (cs.isDoubleShop()) + continue; + shIt.remove(); // Is selling, but has no stock, and is a + // chest shop, but is not a double shop. Can + // be deleted safely. + i++; + } + } + MsgUtil.clean(); + sender.sendMessage(MsgUtil.getMessage("command.cleaned", "" + i)); + return; + } + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + + private void reload(CommandSender sender) { + if (sender.hasPermission("quickshop.reload")) { + sender.sendMessage(MsgUtil.getMessage("command.reloading")); + Bukkit.getPluginManager().disablePlugin(plugin); + Bukkit.getPluginManager().enablePlugin(plugin); + return; + } + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) { + if (args.length > 0) { + String subArg = args[0].toLowerCase(); + if (subArg.equals("unlimited")) { + setUnlimited(sender); + return true; + } else if (subArg.equals("setowner")) { + setOwner(sender, args); + return true; + } else if (subArg.equals("find")) { + find(sender, args); + return true; + } else if (subArg.startsWith("buy")) { + setBuy(sender); + return true; + } else if (subArg.startsWith("sell")) { + setSell(sender); + return true; + } else if (subArg.startsWith("price")) { + setPrice(sender, args); + return true; + } else if (subArg.equals("remove")) { + remove(sender, args); + } else if (subArg.equals("refill")) { + refill(sender, args); + return true; + } else if (subArg.equals("empty")) { + empty(sender, args); + return true; + } else if (subArg.equals("clean")) { + clean(sender); + return true; + } else if (subArg.equals("reload")) { + reload(sender); + return true; + } else if (subArg.equals("export")) { + export(sender, args); + return true; + } else if (subArg.equals("info")) { + if (sender.hasPermission("quickshop.info")) { + int buying, selling, doubles, chunks, worlds; + buying = selling = doubles = chunks = worlds = 0; + int nostock = 0; + for (HashMap> inWorld : plugin.getShopManager().getShops().values()) { + worlds++; + for (HashMap inChunk : inWorld.values()) { + chunks++; + for (Shop shop : inChunk.values()) { + if (shop.isBuying()) { + buying++; + } else if (shop.isSelling()) { + selling++; + } + if (shop instanceof ContainerShop && ((ContainerShop) shop).isDoubleShop()) { + doubles++; + } else if (shop.isSelling() && shop.getRemainingStock() == 0) { + nostock++; + } + } + } + } + sender.sendMessage(ChatColor.RED + "QuickShop Statistics..."); + sender.sendMessage(ChatColor.GREEN + "" + (buying + selling) + " shops in " + chunks + " chunks spread over " + worlds + " worlds."); + sender.sendMessage(ChatColor.GREEN + "" + doubles + " double shops. "); + sender.sendMessage(ChatColor.GREEN + "" + nostock + " selling shops (excluding doubles) which will be removed by /qs clean."); + return true; + } + sender.sendMessage(MsgUtil.getMessage("no-permission")); + return true; + } + } else { + // Invalid arg given + sendHelp(sender); + return true; + } + // No args given + sendHelp(sender); + return true; + } + + /** + * Returns loc with modified pitch/yaw angles so it faces lookat + * + * @param loc + * The location a players head is + * @param lookat + * The location they should be looking + * @return The location the player should be facing to have their crosshairs + * on the location lookAt Kudos to bergerkiller for most of this + * function + */ + public Location lookAt(Location loc, Location lookat) { + // Clone the loc to prevent applied changes to the input loc + loc = loc.clone(); + // Values of change in distance (make it relative) + double dx = lookat.getX() - loc.getX(); + double dy = lookat.getY() - loc.getY(); + double dz = lookat.getZ() - loc.getZ(); + // Set yaw + if (dx != 0) { + // Set yaw start value based on dx + if (dx < 0) { + loc.setYaw((float) (1.5 * Math.PI)); + } else { + loc.setYaw((float) (0.5 * Math.PI)); + } + loc.setYaw((float) loc.getYaw() - (float) Math.atan(dz / dx)); + } else if (dz < 0) { + loc.setYaw((float) Math.PI); + } + // Get the distance from dx/dz + double dxz = Math.sqrt(Math.pow(dx, 2) + Math.pow(dz, 2)); + float pitch = (float) -Math.atan(dy / dxz); + // Set values, convert to degrees + // Minecraft yaw (vertical) angles are inverted (negative) + loc.setYaw(-loc.getYaw() * 180f / (float) Math.PI + 360); + // But pitch angles are normal + loc.setPitch(pitch * 180f / (float) Math.PI); + return loc; + } + + public void sendHelp(CommandSender s) { + s.sendMessage(MsgUtil.getMessage("command.description.title")); + if (s.hasPermission("quickshop.unlimited")) + s.sendMessage(ChatColor.GREEN + "/qs unlimited" + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.unlimited")); + if (s.hasPermission("quickshop.setowner")) + s.sendMessage(ChatColor.GREEN + "/qs setowner " + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.setowner")); + if (s.hasPermission("quickshop.create.buy")) + s.sendMessage(ChatColor.GREEN + "/qs buy" + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.buy")); + if (s.hasPermission("quickshop.create.sell")) + s.sendMessage(ChatColor.GREEN + "/qs sell" + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.sell")); + if (s.hasPermission("quickshop.create.changeprice")) + s.sendMessage(ChatColor.GREEN + "/qs price" + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.price")); + if (s.hasPermission("quickshop.clean")) + s.sendMessage(ChatColor.GREEN + "/qs clean" + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.clean")); + if (s.hasPermission("quickshop.find")) + s.sendMessage(ChatColor.GREEN + "/qs find " + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.find")); + if (s.hasPermission("quickshop.refill")) + s.sendMessage(ChatColor.GREEN + "/qs refill " + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.refill")); + if (s.hasPermission("quickshop.empty")) + s.sendMessage(ChatColor.GREEN + "/qs empty" + ChatColor.YELLOW + " - " + MsgUtil.getMessage("command.description.empty")); + if (s.hasPermission("quickshop.export")) + s.sendMessage(ChatColor.GREEN + "/qs export mysql|sqlite" + ChatColor.YELLOW + " - Exports the database to SQLite or MySQL"); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Database/BufferStatement.java b/src/main/java/org/maxgamer/quickshop/Database/BufferStatement.java new file mode 100644 index 0000000..461cfed --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Database/BufferStatement.java @@ -0,0 +1,74 @@ +package org.maxgamer.quickshop.Database; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Arrays; + +public class BufferStatement { + private Object[] values; + private String query; + private Exception stacktrace; + + /** + * Represents a PreparedStatement in a state before preparing it (E.g. No + * file I/O Required) + * + * @param query + * The query to execute. E.g. INSERT INTO accounts (user, passwd) + * VALUES (?, ?) + * @param values + * The values to replace ? with in + * query. These are in order. + */ + public BufferStatement(String query, Object... values) { + this.query = query; + this.values = values; + this.stacktrace = new Exception(); // For error handling + this.stacktrace.fillInStackTrace(); // We can declare where this + // statement came from. + } + + /** + * Returns a prepared statement using the given connection. Will try to + * return an empty statement if something went wrong. If that fails, returns + * null. + * + * This method escapes everything automatically. + * + * @param con + * The connection to prepare this on using + * con.prepareStatement(..) + * @return The prepared statement, ready for execution. + */ + public PreparedStatement prepareStatement(Connection con) throws SQLException { + PreparedStatement ps; + ps = con.prepareStatement(query); + for (int i = 1; i <= values.length; i++) { + ps.setObject(i, values[i - 1]); + } + return ps; + } + + /** + * Used for debugging. This stacktrace is recorded when the statement is + * created, so printing it to the screen will provide useful debugging + * information about where the query came from, if something went wrong + * while executing it. + * + * @return The stacktrace elements. + */ + public StackTraceElement[] getStackTrace() { + return stacktrace.getStackTrace(); + } + + /** + * @return A string representation of this statement. Returns + * "Query: " + query + ", values: " + + * Arrays.toString(values). + */ + @Override + public String toString() { + return "Query: " + query + ", values: " + Arrays.toString(values); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Database/Database.java b/src/main/java/org/maxgamer/quickshop/Database/Database.java new file mode 100644 index 0000000..b080bf4 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Database/Database.java @@ -0,0 +1,199 @@ +package org.maxgamer.quickshop.Database; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedList; +import java.util.List; + +public class Database { + private DatabaseCore core; + + /** + * Creates a new database and validates its connection. + * + * If the connection is invalid, this will throw a ConnectionException. + * + * @param core + * The core for the database, either MySQL or SQLite. + * @throws ConnectionException + * If the connection was invalid + */ + public Database(DatabaseCore core) throws ConnectionException { + try { + try { + if (!core.getConnection().isValid(10)) { + throw new ConnectionException("Database doesn not appear to be valid!"); + } + } catch (AbstractMethodError e) { + // You don't need to validate this core. + } + } catch (SQLException e) { + throw new ConnectionException(e.getMessage()); + } + this.core = core; + } + + /** + * Returns the database core object, that this database runs on. + * + * @return the database core object, that this database runs on. + */ + public DatabaseCore getCore() { + return core; + } + + /** + * Fetches the connection to this database for querying. Try to avoid doing + * this in the main thread. + * + * @return Fetches the connection to this database for querying. + */ + public Connection getConnection() { + return core.getConnection(); + } + + /** + * Executes the given statement either immediately, or soon. + * + * @param query + * The query + * @param objs + * The string values for each ? in the given query. + */ + public void execute(String query, Object... objs) { + BufferStatement bs = new BufferStatement(query, objs); + core.queue(bs); + } + + /** + * Returns true if the table exists + * + * @param table + * The table to check for + * @return True if the table is found + */ + public boolean hasTable(String table) throws SQLException { + ResultSet rs = getConnection().getMetaData().getTables(null, null, "%", null); + while (rs.next()) { + if (table.equalsIgnoreCase(rs.getString("TABLE_NAME"))) { + rs.close(); + return true; + } + } + rs.close(); + return false; + } + + /** + * Closes the database + */ + public void close() { + this.core.close(); + } + + /** + * Returns true if the given table has the given column + * + * @param table + * The table + * @param column + * The column + * @return True if the given table has the given column + * @throws SQLException + * If the database isn't connected + */ + public boolean hasColumn(String table, String column) throws SQLException { + if (!hasTable(table)) + return false; + String query = "SELECT * FROM " + table + " LIMIT 0,1"; + try { + PreparedStatement ps = this.getConnection().prepareStatement(query); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + rs.getString(column); // Throws an exception if it can't find + // that column + return true; + } + } catch (SQLException e) { + return false; + } + return false; // Uh, wtf. + } + + /** + * Represents a connection error, generally when the server can't connect to + * MySQL or something. + */ + public static class ConnectionException extends Exception { + private static final long serialVersionUID = 8348749992936357317L; + + public ConnectionException(String msg) { + super(msg); + } + } + + /** + * Copies the contents of this database into the given database. Does not + * delete the contents of this database, or change any settings. This may + * take a long time, and will print out progress reports to System.out + * + * This method does not create the tables in the new database. You need to + * do that yourself. + * + * @param db + * The database to copy data to + * @throws SQLException + * if an error occurs. + */ + public void copyTo(Database db) throws SQLException { + ResultSet rs = getConnection().getMetaData().getTables(null, null, "%", null); + List tables = new LinkedList(); + while (rs.next()) { + tables.add(rs.getString("TABLE_NAME")); + } + rs.close(); + core.flush(); + // For each table + for (String table : tables) { + if (table.toLowerCase().startsWith("sqlite_autoindex_")) + continue; + System.out.println("Copying " + table); + // Wipe the old records + db.getConnection().prepareStatement("DELETE FROM " + table).execute(); + // Fetch all the data from the existing database + rs = getConnection().prepareStatement("SELECT * FROM " + table).executeQuery(); + int n = 0; + // Build the query + String query = "INSERT INTO " + table + " VALUES ("; + // Append another placeholder for the value + query += "?"; + for (int i = 2; i <= rs.getMetaData().getColumnCount(); i++) { + // Add the rest of the placeholders and values. This is so we + // have (?, ?, ?) and not (?, ?, ?, ). + query += ", ?"; + } + // End the query + query += ")"; + PreparedStatement ps = db.getConnection().prepareStatement(query); + while (rs.next()) { + n++; + for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) { + ps.setObject(i, rs.getObject(i)); + } + ps.addBatch(); + if (n % 100 == 0) { + ps.executeBatch(); + System.out.println(n + " records copied..."); + } + } + ps.executeBatch(); + // Close the resultset of that table + rs.close(); + } + // Success! + db.getConnection().close(); + this.getConnection().close(); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Database/DatabaseCore.java b/src/main/java/org/maxgamer/quickshop/Database/DatabaseCore.java new file mode 100644 index 0000000..db44d0f --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Database/DatabaseCore.java @@ -0,0 +1,13 @@ +package org.maxgamer.quickshop.Database; + +import java.sql.Connection; + +public interface DatabaseCore { + public Connection getConnection(); + + public void queue(BufferStatement bs); + + public void flush(); + + public void close(); +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Database/DatabaseHelper.java b/src/main/java/org/maxgamer/quickshop/Database/DatabaseHelper.java new file mode 100644 index 0000000..d6ddefd --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Database/DatabaseHelper.java @@ -0,0 +1,62 @@ +package org.maxgamer.quickshop.Database; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +public class DatabaseHelper { + public static void setup(Database db) throws SQLException { + if (!db.hasTable("shops")) { + createShopsTable(db); + } + if (!db.hasTable("messages")) { + createMessagesTable(db); + } + checkColumns(db); + } + + /** + * Verifies that all required columns exist. + */ + public static void checkColumns(Database db) { + PreparedStatement ps = null; + try { + // V3.4.2 + ps = db.getConnection().prepareStatement("ALTER TABLE shops MODIFY COLUMN price double(32,2) NOT NULL AFTER owner"); + ps.execute(); + ps.close(); + } catch (SQLException e) { + } + try { + // V3.4.3 + ps = db.getConnection().prepareStatement("ALTER TABLE messages MODIFY COLUMN time BIGINT(32) NOT NULL AFTER message"); + ps.execute(); + ps.close(); + } catch (SQLException e) { + } + } + + /** + * Creates the database table 'shops'. + * + * @throws SQLException + * If the connection is invalid. + */ + public static void createShopsTable(Database db) throws SQLException { + Statement st = db.getConnection().createStatement(); + String createTable = "CREATE TABLE shops (" + "owner TEXT(32) NOT NULL, " + "price double(32, 2) NOT NULL, " + "itemConfig TEXT CHARSET utf8 NOT NULL, " + "x INTEGER(32) NOT NULL, " + "y INTEGER(32) NOT NULL, " + "z INTEGER(32) NOT NULL, " + "world VARCHAR(32) NOT NULL, " + "unlimited boolean, " + "type boolean, " + "PRIMARY KEY (x, y, z, world) " + ");"; + st.execute(createTable); + } + + /** + * Creates the database table 'messages' + * + * @throws SQLException + * If the connection is invalid + */ + public static void createMessagesTable(Database db) throws SQLException { + Statement st = db.getConnection().createStatement(); + String createTable = "CREATE TABLE messages (" + "owner TEXT(32) NOT NULL, " + "message TEXT(200) NOT NULL, " + "time BIGINT(32) NOT NULL " + ");"; + st.execute(createTable); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Database/MySQLCore.java b/src/main/java/org/maxgamer/quickshop/Database/MySQLCore.java new file mode 100644 index 0000000..f911c95 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Database/MySQLCore.java @@ -0,0 +1,85 @@ +package org.maxgamer.quickshop.Database; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Properties; + +public class MySQLCore implements DatabaseCore { + private String url; + /** The connection properties... user, pass, autoReconnect.. */ + private Properties info; + private static final int MAX_CONNECTIONS = 8; + private static ArrayList pool = new ArrayList(); + + public MySQLCore(String host, String user, String pass, String database, String port) { + info = new Properties(); + info.put("autoReconnect", "true"); + info.put("user", user); + info.put("password", pass); + info.put("useUnicode", "true"); + info.put("characterEncoding", "utf8"); + this.url = "jdbc:mysql://" + host + ":" + port + "/" + database; + for (int i = 0; i < MAX_CONNECTIONS; i++) + pool.add(null); + } + + /** + * Gets the database connection for executing queries on. + * + * @return The database connection + */ + public Connection getConnection() { + for (int i = 0; i < MAX_CONNECTIONS; i++) { + Connection connection = pool.get(i); + try { + // If we have a current connection, fetch it + if (connection != null && !connection.isClosed()) { + if (connection.isValid(10)) { + return connection; + } + // Else, it is invalid, so we return another connection. + } + connection = DriverManager.getConnection(this.url, info); + pool.set(i, connection); + return connection; + } catch (SQLException e) { + e.printStackTrace(); + } + } + return null; + } + + @Override + public void queue(BufferStatement bs) { + try { + Connection con = this.getConnection(); + while (con == null) { + try { + Thread.sleep(15); + } catch (InterruptedException e) { + } + // Try again + this.getConnection(); + } + PreparedStatement ps = bs.prepareStatement(con); + ps.execute(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + return; + } + } + + @Override + public void close() { + // Nothing, because queries are executed immediately for MySQL + } + + @Override + public void flush() { + // Nothing, because queries are executed immediately for MySQL + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Database/SQLiteCore.java b/src/main/java/org/maxgamer/quickshop/Database/SQLiteCore.java new file mode 100644 index 0000000..b7616d2 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Database/SQLiteCore.java @@ -0,0 +1,110 @@ +package org.maxgamer.quickshop.Database; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.LinkedList; + +public class SQLiteCore implements DatabaseCore { + private Connection connection; + private File dbFile; + private volatile Thread watcher; + private volatile LinkedList queue = new LinkedList(); + + public SQLiteCore(File dbFile) { + this.dbFile = dbFile; + } + + /** + * Gets the database connection for executing queries on. + * + * @return The database connection + */ + public Connection getConnection() { + try { + // If we have a current connection, fetch it + if (this.connection != null && !this.connection.isClosed()) { + return this.connection; + } + } catch (SQLException e) { + e.printStackTrace(); + } + if (this.dbFile.exists()) { + // So we need a new connection + try { + Class.forName("org.sqlite.JDBC"); + this.connection = DriverManager.getConnection("jdbc:sqlite:" + this.dbFile); + return this.connection; + } catch (ClassNotFoundException e) { + e.printStackTrace(); + return null; + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } else { + // So we need a new file too. + try { + // Create the file + this.dbFile.createNewFile(); + // Now we won't need a new file, just a connection. + // This will return that new connection. + return this.getConnection(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + } + + @Override + public void queue(BufferStatement bs) { + synchronized (queue) { + queue.add(bs); + } + if (watcher == null || !watcher.isAlive()) { + startWatcher(); + } + } + + @Override + public void flush() { + while (queue.isEmpty() == false) { + BufferStatement bs; + synchronized (queue) { + bs = queue.removeFirst(); + } + synchronized (dbFile) { + try { + PreparedStatement ps = bs.prepareStatement(getConnection()); + ps.execute(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + } + + @Override + public void close() { + flush(); + } + + private void startWatcher() { + watcher = new Thread() { + @Override + public void run() { + try { + Thread.sleep(30000); + } catch (InterruptedException e) { + } + flush(); + } + }; + watcher.start(); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Economy/Economy.java b/src/main/java/org/maxgamer/quickshop/Economy/Economy.java new file mode 100644 index 0000000..1b0e55a --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Economy/Economy.java @@ -0,0 +1,119 @@ +package org.maxgamer.quickshop.Economy; + +import java.util.UUID; + +public class Economy implements EconomyCore { + private EconomyCore core; + + public Economy(EconomyCore core) { + this.core = core; + } + + /** + * Checks that this economy is valid. Returns false if it is not valid. + * + * @return True if this economy will work, false if it will not. + */ + public boolean isValid() { + return core.isValid(); + } + + /** + * Deposits a given amount of money from thin air to the given username. + * + * @param name + * The exact (case insensitive) username to give money to + * @param amount + * The amount to give them + * @return True if success (Should be almost always) + */ + @Deprecated + public boolean deposit(String name, double amount) { + return core.deposit(name, amount); + } + + /** + * Withdraws a given amount of money from the given username and turns it to + * thin air. + * + * @param name + * The exact (case insensitive) username to take money from + * @param amount + * The amount to take from them + * @return True if success, false if they didn't have enough cash + */ + @Deprecated + public boolean withdraw(String name, double amount) { + return core.withdraw(name, amount); + } + + /** + * Transfers the given amount of money from Player1 to Player2 + * + * @param from + * The player who is paying money + * @param to + * The player who is receiving money + * @param amount + * The amount to transfer + * @return true if success (Payer had enough cash, receiver was able to + * receive the funds) + */ + @Deprecated + public boolean transfer(String from, String to, double amount) { + return core.transfer(from, to, amount); + } + + /** + * Fetches the balance of the given account name + * + * @param name + * The name of the account + * @return Their current balance. + */ + @Deprecated + public double getBalance(String name) { + return core.getBalance(name); + } + + /** + * Formats the given number... E.g. 50.5 becomes $50.5 Dollars, or 50 + * Dollars 5 Cents + * + * @param balance + * The given number + * @return The balance in human readable text. + */ + public String format(double balance) { + return core.format(balance); + } + @Deprecated + public boolean has(String name, double amount) { + return core.getBalance(name) >= amount; + } + + @Override + public String toString() { + return core.getClass().getName().split("_")[1]; + } + + @Override + public boolean deposit(UUID name, double amount) { + return core.deposit(name,amount); + } + + @Override + public boolean withdraw(UUID name, double amount) { + return core.withdraw(name, amount); + } + + @Override + public boolean transfer(UUID from, UUID to, double amount) { + return core.transfer(from, to, amount); + } + + @Override + public double getBalance(UUID name) { + return core.getBalance(name); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Economy/EconomyCore.java b/src/main/java/org/maxgamer/quickshop/Economy/EconomyCore.java new file mode 100644 index 0000000..7185ef3 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Economy/EconomyCore.java @@ -0,0 +1,127 @@ +package org.maxgamer.quickshop.Economy; + +import java.util.UUID; + +/** + * @author netherfoam Represents an economy. + */ +public interface EconomyCore { + /** + * Checks that this economy is valid. Returns false if it is not valid. + * + * @return True if this economy will work, false if it will not. + */ + public boolean isValid(); + + /** + * Deposits a given amount of money from thin air to the given username. + * + * @param name + * The exact (case insensitive) username to give money to + * @param amount + * The amount to give them + * @return True if success (Should be almost always) + */ + @Deprecated + public boolean deposit(String name, double amount); + + /** + * Withdraws a given amount of money from the given username and turns it to + * thin air. + * + * @param name + * The exact (case insensitive) username to take money from + * @param amount + * The amount to take from them + * @return True if success, false if they didn't have enough cash + */ + @Deprecated + public boolean withdraw(String name, double amount); + + /** + * Transfers the given amount of money from Player1 to Player2 + * + * @param from + * The player who is paying money + * @param to + * The player who is receiving money + * @param amount + * The amount to transfer + * @return true if success (Payer had enough cash, receiver was able to + * receive the funds) + */ + @Deprecated + public boolean transfer(String from, String to, double amount); + + /** + * Fetches the balance of the given account name + * + * @param name + * The name of the account + * @return Their current balance. + */ + @Deprecated + public double getBalance(String name); + + /** + * Formats the given number... E.g. 50.5 becomes $50.5 Dollars, or 50 + * Dollars 5 Cents + * + * @param balance + * The given number + * @return The balance in human readable text. + */ + public String format(double balance); + + + + + + + + /** + * Deposits a given amount of money from thin air to the given username. + * + * @param name + * The exact (case insensitive) username to give money to + * @param amount + * The amount to give them + * @return True if success (Should be almost always) + */ + public boolean deposit(UUID name, double amount); + + /** + * Withdraws a given amount of money from the given username and turns it to + * thin air. + * + * @param name + * The exact (case insensitive) username to take money from + * @param amount + * The amount to take from them + * @return True if success, false if they didn't have enough cash + */ + public boolean withdraw(UUID name, double amount); + + /** + * Transfers the given amount of money from Player1 to Player2 + * + * @param from + * The player who is paying money + * @param to + * The player who is receiving money + * @param amount + * The amount to transfer + * @return true if success (Payer had enough cash, receiver was able to + * receive the funds) + */ + public boolean transfer(UUID from, UUID to, double amount); + + /** + * Fetches the balance of the given account name + * + * @param name + * The name of the account + * @return Their current balance. + */ + public double getBalance(UUID name); +} diff --git a/src/main/java/org/maxgamer/quickshop/Economy/Economy_Vault.java b/src/main/java/org/maxgamer/quickshop/Economy/Economy_Vault.java new file mode 100644 index 0000000..150a0f4 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Economy/Economy_Vault.java @@ -0,0 +1,108 @@ +package org.maxgamer.quickshop.Economy; + +import java.util.UUID; + +import net.milkbowl.vault.economy.Economy; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.RegisteredServiceProvider; + +public class Economy_Vault implements EconomyCore { + private Economy vault; + + public Economy_Vault() { + setupEconomy(); + } + + private boolean setupEconomy() { + RegisteredServiceProvider economyProvider = Bukkit.getServicesManager().getRegistration(Economy.class); + if (economyProvider != null) { + this.vault = ((Economy) economyProvider.getProvider()); + } + return this.vault != null; + } + + @Override + public boolean isValid() { + return this.vault != null; + } + + @Override + @Deprecated + public boolean deposit(String name, double amount) { + return this.vault.depositPlayer(name, amount).transactionSuccess(); + } + + @Override + @Deprecated + public boolean withdraw(String name, double amount) { + return this.vault.withdrawPlayer(name, amount).transactionSuccess(); + } + + @Override + @Deprecated + public boolean transfer(String from, String to, double amount) { + if (this.vault.getBalance(from) >= amount) { + if (this.vault.withdrawPlayer(from, amount).transactionSuccess()) { + if (!this.vault.depositPlayer(to, amount).transactionSuccess()) { + this.vault.depositPlayer(from, amount); + return false; + } + return true; + } + return false; + } + return false; + } + + @Override + @Deprecated + public double getBalance(String name) { + return this.vault.getBalance(name); + } + + @Override + public String format(double balance) { + try { + return this.vault.format(balance); + } catch (NumberFormatException e) { + } + return "$" + balance; + } + + @Override + public boolean deposit(UUID name, double amount) { + OfflinePlayer p = Bukkit.getOfflinePlayer(name); + return this.vault.depositPlayer(p, amount).transactionSuccess(); + } + + @Override + public boolean withdraw(UUID name, double amount) { + OfflinePlayer p = Bukkit.getOfflinePlayer(name); + return this.vault.withdrawPlayer(p, amount).transactionSuccess(); + } + + @Override + public boolean transfer(UUID from, UUID to, double amount) { + OfflinePlayer pFrom = Bukkit.getOfflinePlayer(from); + OfflinePlayer pTo = Bukkit.getOfflinePlayer(to); + if (this.vault.getBalance(pFrom) >= amount) { + if (this.vault.withdrawPlayer(pFrom, amount).transactionSuccess()) { + if (!this.vault.depositPlayer(pTo, amount).transactionSuccess()) { + this.vault.depositPlayer(pFrom, amount); + return false; + } + return true; + } + return false; + } + return false; + } + + @Override + public double getBalance(UUID name) { + OfflinePlayer p = Bukkit.getOfflinePlayer(name); + return this.vault.getBalance(p); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Listeners/BlockListener.java b/src/main/java/org/maxgamer/quickshop/Listeners/BlockListener.java new file mode 100644 index 0000000..8785444 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Listeners/BlockListener.java @@ -0,0 +1,124 @@ +package org.maxgamer.quickshop.Listeners; + +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.DoubleChest; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.inventory.InventoryHolder; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Shop.Info; +import org.maxgamer.quickshop.Shop.Shop; +import org.maxgamer.quickshop.Shop.ShopAction; +import org.maxgamer.quickshop.Util.MsgUtil; +import org.maxgamer.quickshop.Util.Util; + +public class BlockListener implements Listener { + private QuickShop plugin; + + public BlockListener(QuickShop plugin) { + this.plugin = plugin; + } + + /** + * Listens for chest placement, so a doublechest shop can't be created. + */ + @EventHandler(ignoreCancelled = true) + public void onPlace(BlockPlaceEvent e) { + if (e.isCancelled()) + return; + BlockState bs = e.getBlock().getState(); + if (bs instanceof DoubleChest == false) + return; + Block b = e.getBlock(); + Player p = e.getPlayer(); + Block chest = Util.getSecondHalf(b); + if (chest != null && plugin.getShopManager().getShop(chest.getLocation()) != null && !p.hasPermission("quickshop.create.double")) { + e.setCancelled(true); + p.sendMessage(MsgUtil.getMessage("no-double-chests")); + } + } + + /** + * Removes chests when they're destroyed. + */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBreak(BlockBreakEvent e) { + Block b = e.getBlock(); + Player p = e.getPlayer(); + // If the shop was a chest + if (b.getState() instanceof InventoryHolder) { + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop == null) + return; + // If they're either survival or the owner, they can break it + if (p.getGameMode() == GameMode.CREATIVE && !p.getUniqueId().equals(shop.getOwner())) { + e.setCancelled(true); + p.sendMessage(MsgUtil.getMessage("no-creative-break")); + return; + } + // Cancel their current menu... Doesnt cancel other's menu's. + Info action = plugin.getShopManager().getActions().get(p.getName()); + if (action != null) { + action.setAction(ShopAction.CANCELLED); + } + shop.delete(); + p.sendMessage(MsgUtil.getMessage("success-removed-shop")); + } else if (b.getType() == Material.WALL_SIGN) { + Shop shop = getShopNextTo(b.getLocation()); + if (shop == null) + return; + // If they're in creative and not the owner, don't let them + // (accidents happen) + if (p.getGameMode() == GameMode.CREATIVE && !p.getUniqueId().equals(shop.getOwner())) { + e.setCancelled(true); + p.sendMessage(MsgUtil.getMessage("no-creative-break")); + return; + } + if (e.isCancelled()) + return; + e.setCancelled(true); // Cancel the event so that the sign does not + // drop.. TODO: Find a better way. + b.setType(Material.AIR); + } + } + + /** + * Handles shops breaking through explosions + */ + @EventHandler(priority = EventPriority.HIGHEST) + public void onExplode(EntityExplodeEvent e) { + if (e.isCancelled()) + return; + for (int i = 0; i < e.blockList().size(); i++) { + Block b = e.blockList().get(i); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null) { + shop.delete(); + } + } + } + + /** + * Gets the shop a sign is attached to + * + * @param loc + * The location of the sign + * @return The shop + */ + private Shop getShopNextTo(Location loc) { + Block b = Util.getAttached(loc.getBlock()); + // Util.getAttached(b) + if (b == null) + return null; + return plugin.getShopManager().getShop(b.getLocation()); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Listeners/ChatListener.java b/src/main/java/org/maxgamer/quickshop/Listeners/ChatListener.java new file mode 100644 index 0000000..7cd94dd --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Listeners/ChatListener.java @@ -0,0 +1,28 @@ +package org.maxgamer.quickshop.Listeners; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.maxgamer.quickshop.QuickShop; + +/** + * + * @author Netherfoam + * + */ +public class ChatListener implements Listener { + QuickShop plugin; + + public ChatListener(QuickShop plugin) { + this.plugin = plugin; + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onChat(final AsyncPlayerChatEvent e) { + if (!plugin.getShopManager().getActions().containsKey(e.getPlayer().getUniqueId())) + return; + plugin.getShopManager().handleChat(e.getPlayer(), e.getMessage()); + e.setCancelled(true); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Listeners/ChunkListener.java b/src/main/java/org/maxgamer/quickshop/Listeners/ChunkListener.java new file mode 100644 index 0000000..d41609f --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Listeners/ChunkListener.java @@ -0,0 +1,45 @@ +package org.maxgamer.quickshop.Listeners; + +import java.util.HashMap; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Shop.Shop; + +public class ChunkListener implements Listener { + private QuickShop plugin; + + public ChunkListener(QuickShop plugin) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onChunkLoad(ChunkLoadEvent e) { + Chunk c = e.getChunk(); + if (plugin.getShopManager().getShops() == null) + return; + HashMap inChunk = plugin.getShopManager().getShops(c); + if (inChunk == null) + return; + for (Shop shop : inChunk.values()) { + shop.onLoad(); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onChunkUnload(ChunkUnloadEvent e) { + Chunk c = e.getChunk(); + HashMap inChunk = plugin.getShopManager().getShops(c); + if (inChunk == null) + return; + for (Shop shop : inChunk.values()) { + shop.onUnload(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Listeners/HeroChatListener.java b/src/main/java/org/maxgamer/quickshop/Listeners/HeroChatListener.java new file mode 100644 index 0000000..4d33414 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Listeners/HeroChatListener.java @@ -0,0 +1,30 @@ +package org.maxgamer.quickshop.Listeners; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.maxgamer.quickshop.QuickShop; + +import com.dthielke.herochat.ChannelChatEvent; +import com.dthielke.herochat.Chatter.Result; + +/** + * + * @author Netherfoam + * + */ +public class HeroChatListener implements Listener { + QuickShop plugin; + + public HeroChatListener(QuickShop plugin) { + this.plugin = plugin; + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onHeroChat(ChannelChatEvent e) { + if (!plugin.getShopManager().getActions().containsKey(e.getSender().getName())) + return; + plugin.getShopManager().handleChat(e.getSender().getPlayer(), e.getMessage()); + e.setResult(Result.FAIL); + } +} diff --git a/src/main/java/org/maxgamer/quickshop/Listeners/LockListener.java b/src/main/java/org/maxgamer/quickshop/Listeners/LockListener.java new file mode 100644 index 0000000..8aa170f --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Listeners/LockListener.java @@ -0,0 +1,144 @@ +package org.maxgamer.quickshop.Listeners; + +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Shop.Shop; +import org.maxgamer.quickshop.Util.MsgUtil; +import org.maxgamer.quickshop.Util.Util; + +public class LockListener implements Listener { + private QuickShop plugin; + + public LockListener(QuickShop plugin) { + this.plugin = plugin; + } + + @EventHandler(ignoreCancelled = true) + public void onClick(PlayerInteractEvent e) { + Block b = e.getClickedBlock(); + Player p = e.getPlayer(); + if (e.getAction() != Action.RIGHT_CLICK_BLOCK) + return; // Didn't right click it, we dont care. + if (!Util.canBeShop(b)) + return; // Interacted with air + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + // Make sure they're not using the non-shop half of a double chest. + if (shop == null) { + b = Util.getSecondHalf(b); + if (b == null) + return; + shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop == null) + return; + } + if (!shop.getOwner().equals(p.getUniqueId())) { + if (p.hasPermission("quickshop.other.open")) { + p.sendMessage(MsgUtil.getMessage("bypassing-lock")); + return; + } + p.sendMessage(MsgUtil.getMessage("that-is-locked")); + e.setCancelled(true); + return; + } + } + + /** + * Handles hopper placement + */ + @EventHandler(priority = EventPriority.LOW) + public void onPlace(BlockPlaceEvent e) { + Block b = e.getBlock(); + try { + if (b.getType() != Material.HOPPER) + return; + } catch (NoSuchFieldError er) { + return; // Your server doesn't have hoppers + } + Block c = e.getBlockAgainst(); + if (Util.canBeShop(c) == false) + return; + Player p = e.getPlayer(); + Shop shop = plugin.getShopManager().getShop(c.getLocation()); + if (shop == null) { + c = Util.getSecondHalf(c); + if (c == null) + return; // You didn't place a hopper on a shop. Meh. + else + shop = plugin.getShopManager().getShop(c.getLocation()); + if (shop == null) + return; + } + if (p.getUniqueId().equals(shop.getOwner()) == false) { + if (p.hasPermission("quickshop.other.open")) { + p.sendMessage(MsgUtil.getMessage("bypassing-lock")); + return; + } + p.sendMessage(MsgUtil.getMessage("that-is-locked")); + e.setCancelled(true); + return; + } + } + + /** + * Removes chests when they're destroyed. + */ + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void onBreak(BlockBreakEvent e) { + Block b = e.getBlock(); + Player p = e.getPlayer(); + // If the chest was a chest + if (Util.canBeShop(b)) { + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop == null) + return; // Wasn't a shop + // If they owned it or have bypass perms, they can destroy it + if (!shop.getOwner().equals(p.getUniqueId()) && !p.hasPermission("quickshop.other.destroy")) { + e.setCancelled(true); + p.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + } else if (b.getType() == Material.WALL_SIGN) { + b = Util.getAttached(b); + if (b == null) + return; + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop == null) + return; + // If they're the shop owner or have bypass perms, they can destroy + // it. + if (!shop.getOwner().equals(p.getUniqueId()) && !p.hasPermission("quickshop.other.destroy")) { + e.setCancelled(true); + p.sendMessage(MsgUtil.getMessage("no-permission")); + return; + } + } + } + + /** + * Handles shops breaking through explosions + */ + @EventHandler(priority = EventPriority.LOW) + public void onExplode(EntityExplodeEvent e) { + if (e.isCancelled()) + return; + for (int i = 0; i < e.blockList().size(); i++) { + Block b = e.blockList().get(i); + Shop shop = plugin.getShopManager().getShop(b.getLocation()); + if (shop != null) { + // ToDo: Shouldn't I be decrementing 1 here? Concurrency and + // all.. + e.blockList().remove(b); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Listeners/PlayerListener.java b/src/main/java/org/maxgamer/quickshop/Listeners/PlayerListener.java new file mode 100644 index 0000000..a43219d --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Listeners/PlayerListener.java @@ -0,0 +1,189 @@ +package org.maxgamer.quickshop.Listeners; + +import java.util.HashMap; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerPickupItemEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.BlockIterator; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Shop.Info; +import org.maxgamer.quickshop.Shop.Shop; +import org.maxgamer.quickshop.Shop.ShopAction; +import org.maxgamer.quickshop.Util.MsgUtil; +import org.maxgamer.quickshop.Util.Util; + +public class PlayerListener implements Listener { + private QuickShop plugin; + + public PlayerListener(QuickShop plugin) { + this.plugin = plugin; + } + + /* + * Could be useful one day private LinkedList getParents(Class + * clazz){ LinkedList classes = new LinkedList(); + * + * while(clazz != null){ classes.add("Extends " + ChatColor.GREEN + + * clazz.getCanonicalName()); for(Class iface : clazz.getInterfaces()){ + * classes.add("Implements " + ChatColor.RED + iface.getCanonicalName()); + * classes.addAll(getParents(iface)); } + * + * clazz = clazz.getSuperclass(); } return classes; } + */ + /** + * Handles players left clicking a chest. Left click a NORMAL chest with + * item : Send creation menu Left click a SHOP chest : Send purchase menu + */ + @SuppressWarnings("deprecation") + @EventHandler(priority = EventPriority.MONITOR) + public void onClick(PlayerInteractEvent e) { + if (e.getAction() != Action.LEFT_CLICK_BLOCK) + return; + Block b = e.getClickedBlock(); + if (!Util.canBeShop(b) && b.getType() != Material.WALL_SIGN) + return; + Player p = e.getPlayer(); + if (plugin.sneak && !p.isSneaking()) { + // Sneak only + return; + } + Location loc = b.getLocation(); + ItemStack item = e.getItem(); + // Get the shop + Shop shop = plugin.getShopManager().getShop(loc); + // If that wasn't a shop, search nearby shops + if (shop == null && b.getType() == Material.WALL_SIGN) { + Block attached = Util.getAttached(b); + if (attached != null) { + shop = plugin.getShopManager().getShop(attached.getLocation()); + } + } + // Purchase handling + if (shop != null && p.hasPermission("quickshop.use") && (plugin.sneakTrade == false || p.isSneaking())) { + shop.onClick(); + // Text menu + MsgUtil.sendShopInfo(p, shop); + if (shop.isSelling()) { + p.sendMessage(MsgUtil.getMessage("how-many-buy")); + } else { + int items = Util.countItems(p.getInventory(), shop.getItem()); + p.sendMessage(MsgUtil.getMessage("how-many-sell", "" + items)); + } + // Add the new action + HashMap actions = plugin.getShopManager().getActions(); + Info info = new Info(shop.getLocation(), ShopAction.BUY, null, null, shop); + actions.put(p.getUniqueId(), info); + return; + } + // Handles creating shops + else if (!e.isCancelled() && shop == null && item != null && item.getType() != Material.AIR && p.hasPermission("quickshop.create.sell") && Util.canBeShop(b) && p.getGameMode() != GameMode.CREATIVE && (plugin.sneakCreate == false || p.isSneaking())) { + if (!plugin.getShopManager().canBuildShop(p, b, e.getBlockFace())) { + // As of the new checking system, most plugins will tell the + // player why they can't create a shop there. + // So telling them a message would cause spam etc. + return; + } + if (Util.getSecondHalf(b) != null && !p.hasPermission("quickshop.create.double")) { + p.sendMessage(MsgUtil.getMessage("no-double-chests")); + return; + } + if (Util.isBlacklisted(item.getType()) && !p.hasPermission("quickshop.bypass." + item.getTypeId())) { + p.sendMessage(MsgUtil.getMessage("blacklisted-item")); + return; + } + // Finds out where the sign should be placed for the shop + Block last = null; + Location from = p.getLocation().clone(); + from.setY(b.getY()); + from.setPitch(0); + BlockIterator bIt = new BlockIterator(from, 0, 7); + while (bIt.hasNext()) { + Block n = bIt.next(); + if (n.equals(b)) + break; + last = n; + } + // Send creation menu. + Info info = new Info(b.getLocation(), ShopAction.CREATE, e.getItem(), last); + plugin.getShopManager().getActions().put(p.getUniqueId(), info); + p.sendMessage(MsgUtil.getMessage("how-much-to-trade-for", Util.getName(info.getItem()))); + } + } + + @EventHandler(priority = EventPriority.HIGH) + /** + * Waits for a player to move too far from a shop, then cancels the menu. + */ + public void onMove(PlayerMoveEvent e) { + if (e.isCancelled()) + return; + Info info = plugin.getShopManager().getActions().get(e.getPlayer().getUniqueId()); + if (info != null) { + Player p = e.getPlayer(); + Location loc1 = info.getLocation(); + Location loc2 = p.getLocation(); + if (loc1.getWorld() != loc2.getWorld() || loc1.distanceSquared(loc2) > 25) { + if (info.getAction() == ShopAction.CREATE) { + p.sendMessage(MsgUtil.getMessage("shop-creation-cancelled")); + } else if (info.getAction() == ShopAction.BUY) { + p.sendMessage(MsgUtil.getMessage("shop-purchase-cancelled")); + } + plugin.getShopManager().getActions().remove(p.getUniqueId()); + return; + } + } + } + + @EventHandler + public void onTeleport(PlayerTeleportEvent e) { + PlayerMoveEvent me = new PlayerMoveEvent(e.getPlayer(), e.getFrom(), e.getTo()); + onMove(me); + } + + @EventHandler + public void onJoin(final PlayerJoinEvent e) { + // Notify the player any messages they were sent + Bukkit.getScheduler().runTaskLater(QuickShop.instance, new Runnable() { + @Override + public void run() { + MsgUtil.flush(e.getPlayer()); + } + }, 60); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent e) { + // Remove them from the menu + plugin.getShopManager().getActions().remove(e.getPlayer().getUniqueId()); + } + + @EventHandler + public void onPlayerPickup(PlayerPickupItemEvent e) { + ItemStack stack = e.getItem().getItemStack(); + try { + if (stack.getItemMeta().getDisplayName().startsWith(ChatColor.RED + "QuickShop ")) { + e.setCancelled(true); + // You shouldn't be able to pick up that... + } + } catch (NullPointerException ex) { + } // if meta/displayname/stack is null. We don't really care in that + // case. + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Listeners/WorldListener.java b/src/main/java/org/maxgamer/quickshop/Listeners/WorldListener.java new file mode 100644 index 0000000..afc2f06 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Listeners/WorldListener.java @@ -0,0 +1,79 @@ +package org.maxgamer.quickshop.Listeners; + +import java.util.HashMap; +import java.util.Map.Entry; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Shop.Shop; +import org.maxgamer.quickshop.Shop.ShopChunk; + +public class WorldListener implements Listener { + QuickShop plugin; + + public WorldListener(QuickShop plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onWorldLoad(WorldLoadEvent e) { + /* ************************************* + * This listener fixes any broken world references. Such as hashmap + * lookups will fail, because the World reference is different, but the + * world value is the same. ************************************ + */ + World world = e.getWorld(); + // New world data + HashMap> inWorld = new HashMap>(1); + // Old world data + HashMap> oldInWorld = plugin.getShopManager().getShops(world.getName()); + // Nothing in the old world, therefore we don't care. No locations to + // update. + if (oldInWorld == null) + return; + for (Entry> oldInChunk : oldInWorld.entrySet()) { + HashMap inChunk = new HashMap(1); + // Put the new chunk were the old chunk was + inWorld.put(oldInChunk.getKey(), inChunk); + for (Entry entry : oldInChunk.getValue().entrySet()) { + Shop shop = entry.getValue(); + shop.getLocation().setWorld(world); + inChunk.put(shop.getLocation(), shop); + } + } + // Done - Now we can store the new world dataz! + plugin.getShopManager().getShops().put(world.getName(), inWorld); + // This is a workaround, because I don't get parsed chunk events when a + // world first loads.... + // So manually tell all of these shops they're loaded. + for (Chunk chunk : world.getLoadedChunks()) { + HashMap inChunk = plugin.getShopManager().getShops(chunk); + if (inChunk == null) + continue; + for (Shop shop : inChunk.values()) { + shop.onLoad(); + } + } + } + + @EventHandler + public void onWorldUnload(WorldUnloadEvent e) { + // This is a workaround, because I don't get parsed chunk events when a + // world unloads, I think... + // So manually tell all of these shops they're unloaded. + for (Chunk chunk : e.getWorld().getLoadedChunks()) { + HashMap inChunk = plugin.getShopManager().getShops(chunk); + if (inChunk == null) + continue; + for (Shop shop : inChunk.values()) { + shop.onUnload(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/QuickShop.java b/src/main/java/org/maxgamer/quickshop/QuickShop.java new file mode 100644 index 0000000..0e0d9d9 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/QuickShop.java @@ -0,0 +1,400 @@ +package org.maxgamer.quickshop; + +import java.io.File; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.maxgamer.quickshop.Command.QS; +import org.maxgamer.quickshop.Database.Database; +import org.maxgamer.quickshop.Database.Database.ConnectionException; +import org.maxgamer.quickshop.Database.DatabaseCore; +import org.maxgamer.quickshop.Database.DatabaseHelper; +import org.maxgamer.quickshop.Database.MySQLCore; +import org.maxgamer.quickshop.Database.SQLiteCore; +import org.maxgamer.quickshop.Economy.Economy; +import org.maxgamer.quickshop.Economy.EconomyCore; +import org.maxgamer.quickshop.Economy.Economy_Vault; +import org.maxgamer.quickshop.Listeners.BlockListener; +import org.maxgamer.quickshop.Listeners.ChatListener; +import org.maxgamer.quickshop.Listeners.ChunkListener; +import org.maxgamer.quickshop.Listeners.HeroChatListener; +import org.maxgamer.quickshop.Listeners.LockListener; +import org.maxgamer.quickshop.Listeners.PlayerListener; +import org.maxgamer.quickshop.Listeners.WorldListener; +import org.maxgamer.quickshop.Shop.ContainerShop; +import org.maxgamer.quickshop.Shop.Shop; +import org.maxgamer.quickshop.Shop.ShopManager; +import org.maxgamer.quickshop.Shop.ShopType; +import org.maxgamer.quickshop.Util.Converter; +import org.maxgamer.quickshop.Util.MsgUtil; +import org.maxgamer.quickshop.Util.Util; +import org.maxgamer.quickshop.Watcher.ItemWatcher; +import org.maxgamer.quickshop.Watcher.LogWatcher; + + +public class QuickShop extends JavaPlugin { + /** The active instance of QuickShop */ + public static QuickShop instance; + /** The economy we hook into for transactions */ + private Economy economy; + /** The Shop Manager used to store shops */ + private ShopManager shopManager; + /** + * A set of players who have been warned + * ("Your shop isn't automatically locked") + */ + public HashSet warnings = new HashSet(); + /** The database for storing all our data for persistence */ + private Database database; + // Listeners - We decide which one to use at runtime + private ChatListener chatListener; + private HeroChatListener heroChatListener; + // Listeners (These don't) + private BlockListener blockListener = new BlockListener(this); + private PlayerListener playerListener = new PlayerListener(this); + private ChunkListener chunkListener = new ChunkListener(this); + private WorldListener worldListener = new WorldListener(this); + private BukkitTask itemWatcherTask; + private LogWatcher logWatcher; + /** Whether players are required to sneak to create/buy from a shop */ + public boolean sneak; + /** Whether players are required to sneak to create a shop */ + public boolean sneakCreate; + /** Whether players are required to sneak to trade with a shop */ + public boolean sneakTrade; + /** Whether we should use display items or not */ + public boolean display = true; + /** + * Whether we players are charged a fee to change the price on their shop + * (To help deter endless undercutting + */ + public boolean priceChangeRequiresFee = false; + /** Whether or not to limit players shop amounts */ + public boolean limit = false; + private HashMap limits = new HashMap(); + /** Use SpoutPlugin to get item / block names */ + public boolean useSpout = false; + // private Metrics metrics; + /** Whether debug info should be shown in the console */ + public static boolean debug = false; + + /** The plugin metrics from Hidendra */ + // public Metrics getMetrics(){ return metrics; } + public int getShopLimit(Player p) { + int max = getConfig().getInt("limits.default"); + for (Entry entry : limits.entrySet()) { + if (entry.getValue() > max && p.hasPermission(entry.getKey())) + max = entry.getValue(); + } + return max; + } + + public void onEnable() { + instance = this; + saveDefaultConfig(); // Creates the config folder and copies config.yml + // (If one doesn't exist) as required. + reloadConfig(); // Reloads messages.yml too, aswell as config.yml and + // others. + getConfig().options().copyDefaults(true); // Load defaults. + if (getConfig().contains("debug")) + debug = true; + if (loadEcon() == false) + return; + // Create the shop manager. + this.shopManager = new ShopManager(this); + if (this.display) { + // Display item handler thread + getLogger().info("Starting item scheduler"); + ItemWatcher itemWatcher = new ItemWatcher(this); + itemWatcherTask = Bukkit.getScheduler().runTaskTimer(this, itemWatcher, 600, 600); + } + if (this.getConfig().getBoolean("log-actions")) { + // Logger Handler + this.logWatcher = new LogWatcher(this, new File(this.getDataFolder(), "qs.log")); + logWatcher.task = Bukkit.getScheduler().runTaskTimerAsynchronously(this, this.logWatcher, 150, 150); + } + if (getConfig().getBoolean("shop.lock")) { + LockListener ll = new LockListener(this); + getServer().getPluginManager().registerEvents(ll, this); + } + ConfigurationSection limitCfg = this.getConfig().getConfigurationSection("limits"); + if (limitCfg != null) { + getLogger().info("Limit cfg found..."); + this.limit = limitCfg.getBoolean("use", false); + getLogger().info("Limits.use: " + limit); + limitCfg = limitCfg.getConfigurationSection("ranks"); + for (String key : limitCfg.getKeys(true)) { + limits.put(key, limitCfg.getInt(key)); + } + getLogger().info(limits.toString()); + } + try { + ConfigurationSection dbCfg = getConfig().getConfigurationSection("database"); + DatabaseCore dbCore; + if (dbCfg.getBoolean("mysql")) { + // MySQL database - Required database be created first. + String user = dbCfg.getString("user"); + String pass = dbCfg.getString("password"); + String host = dbCfg.getString("host"); + String port = dbCfg.getString("port"); + String database = dbCfg.getString("database"); + dbCore = new MySQLCore(host, user, pass, database, port); + } else { + // SQLite database - Doing this handles file creation + dbCore = new SQLiteCore(new File(this.getDataFolder(), "shops.db")); + } + this.database = new Database(dbCore); + // Make the database up to date + DatabaseHelper.setup(getDB()); + } catch (ConnectionException e) { + e.printStackTrace(); + getLogger().severe("Error connecting to database. Aborting plugin load."); + getServer().getPluginManager().disablePlugin(this); + return; + } catch (SQLException e) { + e.printStackTrace(); + getLogger().severe("Error setting up database. Aborting plugin load."); + getServer().getPluginManager().disablePlugin(this); + return; + } + /* Load shops from database to memory */ + int count = 0; // Shops count + Connection con; + try { + getLogger().info("Loading shops from database..."); + int res = Converter.convert(); + if (res < 0) { + System.out.println("Could not convert shops. Exitting."); + return; + } + if (res > 0) { + System.out.println("Conversion success. Continuing..."); + } + con = database.getConnection(); + PreparedStatement ps = con.prepareStatement("SELECT * FROM shops"); + ResultSet rs = ps.executeQuery(); + int errors = 0; + while (rs.next()) { + int x = 0; + int y = 0; + int z = 0; + String worldName = null; + try { + x = rs.getInt("x"); + y = rs.getInt("y"); + z = rs.getInt("z"); + worldName = rs.getString("world"); + World world = Bukkit.getWorld(worldName); + ItemStack item = Util.deserialize(rs.getString("itemConfig")); + String owner = rs.getString("owner"); + double price = rs.getDouble("price"); + Location loc = new Location(world, x, y, z); + /* Skip invalid shops, if we know of any */ + if (world != null && (loc.getBlock().getState() instanceof InventoryHolder) == false) { + getLogger().info("Shop is not an InventoryHolder in " + rs.getString("world") + " at: " + x + ", " + y + ", " + z + ". Deleting."); + PreparedStatement delps = getDB().getConnection().prepareStatement("DELETE FROM shops WHERE x = ? AND y = ? and z = ? and world = ?"); + delps.setInt(1, x); + delps.setInt(2, y); + delps.setInt(3, z); + delps.setString(4, worldName); + delps.execute(); + continue; + } + int type = rs.getInt("type"); + Shop shop = new ContainerShop(loc, price, item, UUID.fromString(owner)); + shop.setUnlimited(rs.getBoolean("unlimited")); + shop.setShopType(ShopType.fromID(type)); + shopManager.loadShop(rs.getString("world"), shop); + if (loc.getWorld() != null && loc.getChunk().isLoaded()) { + shop.onLoad(); + } + count++; + } catch (Exception e) { + errors++; + e.printStackTrace(); + getLogger().severe("Error loading a shop! Coords: " + worldName + " (" + x + ", " + y + ", " + z + ")..."); + if (errors < 3) { + getLogger().info("Deleting the shop..."); + PreparedStatement delps = getDB().getConnection().prepareStatement("DELETE FROM shops WHERE x = ? AND y = ? and z = ? and world = ?"); + delps.setInt(1, x); + delps.setInt(2, y); + delps.setInt(3, z); + delps.setString(4, worldName); + delps.execute(); + } else { + getLogger().severe("Multiple errors in shops - Something seems to be wrong with your shops database! Please check it out immediately!"); + e.printStackTrace(); + } + } + } + } catch (SQLException e) { + e.printStackTrace(); + getLogger().severe("Could not load shops."); + } + getLogger().info("Loaded " + count + " shops."); + MsgUtil.loadTransactionMessages(); + MsgUtil.clean(); + // Register events + getLogger().info("Registering Listeners"); + Bukkit.getServer().getPluginManager().registerEvents(blockListener, this); + Bukkit.getServer().getPluginManager().registerEvents(playerListener, this); + if (this.display) { + Bukkit.getServer().getPluginManager().registerEvents(chunkListener, this); + } + Bukkit.getServer().getPluginManager().registerEvents(worldListener, this); + if (Bukkit.getPluginManager().getPlugin("Herochat") != null) { + this.getLogger().info("Found Herochat... Hooking!"); + this.heroChatListener = new HeroChatListener(this); + Bukkit.getServer().getPluginManager().registerEvents(heroChatListener, this); + } else { + this.chatListener = new ChatListener(this); + Bukkit.getServer().getPluginManager().registerEvents(chatListener, this); + } + // Command handlers + QS commandExecutor = new QS(this); + getCommand("qs").setExecutor(commandExecutor); + if (getConfig().getInt("shop.find-distance") > 100) { + getLogger().severe("Shop.find-distance is too high! Pick a number under 100!"); + } + /** + * If the server has Spout we can get the names of custom items. Latest + * SpoutPlugin http://get.spout.org/1412/SpoutPlugin.jar + * http://build.spout.org/view/Legacy/job/SpoutPlugin/1412/ + */ +// if (Bukkit.getPluginManager().getPlugin("Spout") != null) { +// this.getLogger().info("Found Spout..."); +// this.useSpout = true; +// } else { +// this.useSpout = false; +// } + getLogger().info("QuickShop loaded!"); + } + + /** Reloads QuickShops config */ + @Override + public void reloadConfig() { + super.reloadConfig(); + // Load quick variables + this.display = this.getConfig().getBoolean("shop.display-items"); + this.sneak = this.getConfig().getBoolean("shop.sneak-only"); + this.sneakCreate = this.getConfig().getBoolean("shop.sneak-to-create"); + this.sneakTrade = this.getConfig().getBoolean("shop.sneak-to-trade"); + this.priceChangeRequiresFee = this.getConfig().getBoolean("shop.price-change-requires-fee"); + MsgUtil.loadCfgMessages(); + } + + /** + * Tries to load the economy and its core. If this fails, it will try to use + * vault. If that fails, it will return false. + * + * @return true if successful, false if the core is invalid or is not found, + * and vault cannot be used. + */ + public boolean loadEcon() { + EconomyCore core = new Economy_Vault(); + if (core == null || !core.isValid()) { + // getLogger().severe("Economy is not valid!"); + getLogger().severe("QuickShop could not hook an economy!"); + getLogger().severe("QuickShop CANNOT start!"); + this.getPluginLoader().disablePlugin(this); + // if(econ.equals("Vault")) + // getLogger().severe("(Does Vault have an Economy to hook into?!)"); + return false; + } else { + this.economy = new Economy(core); + return true; + } + } + + public void onDisable() { + if (itemWatcherTask != null) { + itemWatcherTask.cancel(); + } + if (logWatcher != null) { + logWatcher.task.cancel(); + logWatcher.close(); // Closes the file + } + /* Remove all display items, and any dupes we can find */ + shopManager.clear(); + /* Empty the buffer */ + database.close(); + try { + this.database.getConnection().close(); + } catch (SQLException e) { + e.printStackTrace(); + } + this.warnings.clear(); + this.reloadConfig(); + } + + /** + * Returns the economy for moving currency around + * + * @return The economy for moving currency around + */ + public EconomyCore getEcon() { + return economy; + } + + /** + * Logs the given string to qs.log, if QuickShop is configured to do so. + * + * @param s + * The string to log. It will be prefixed with the date and time. + */ + public void log(String s) { + if (this.logWatcher == null) + return; + Date date = Calendar.getInstance().getTime(); + Timestamp time = new Timestamp(date.getTime()); + this.logWatcher.add("[" + time.toString() + "] " + s); + } + + /** + * @return Returns the database handler for queries etc. + */ + public Database getDB() { + return this.database; + } + + /** + * Prints debug information if QuickShop is configured to do so. + * + * @param s + * The string to print. + */ + public void debug(String s) { + if (!debug) + return; + this.getLogger().info(ChatColor.YELLOW + "[Debug] " + s); + } + + /** + * Returns the ShopManager. This is used for fetching, adding and removing + * shops. + * + * @return The ShopManager. + */ + public ShopManager getShopManager() { + return this.shopManager; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/.gitignore b/src/main/java/org/maxgamer/quickshop/Shop/.gitignore new file mode 100644 index 0000000..2a28477 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/.gitignore @@ -0,0 +1 @@ +/Shop.java diff --git a/src/main/java/org/maxgamer/quickshop/Shop/ContainerShop.java b/src/main/java/org/maxgamer/quickshop/Shop/ContainerShop.java new file mode 100644 index 0000000..0a6baa3 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/ContainerShop.java @@ -0,0 +1,659 @@ +package org.maxgamer.quickshop.Shop; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.Sign; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Util.MsgUtil; +import org.maxgamer.quickshop.Util.Util; + +public class ContainerShop implements Shop { + private Location loc; + private double price; + private UUID owner; + private ItemStack item; + private DisplayItem displayItem; + private boolean unlimited; + private ShopType shopType; + private QuickShop plugin; + + /** + * Returns a clone of this shop. References to the same display item, + * itemstack, location and owner as this shop does. Do not modify them or + * you will modify this shop. + * + * **NOT A DEEP CLONE** + */ + public ContainerShop clone() { + return new ContainerShop(this); + } + + private ContainerShop(ContainerShop s) { + this.displayItem = s.displayItem; + this.shopType = s.shopType; + this.item = s.item; + this.loc = s.loc; + this.plugin = s.plugin; + this.unlimited = s.unlimited; + this.owner = s.owner; + this.price = s.price; + } + + /** + * Adds a new shop. + * + * @param loc + * The location of the chest block + * @param price + * The cost per item + * @param item + * The itemstack with the properties we want. This is .cloned, no + * need to worry about references + * @param owner + * The player who owns this shop. + */ + public ContainerShop(Location loc, double price, ItemStack item, UUID owner) { + this.loc = loc; + this.price = price; + this.owner = owner; + this.item = item.clone(); + this.plugin = (QuickShop) Bukkit.getPluginManager().getPlugin("QuickShop"); + this.item.setAmount(1); + if (plugin.display) { + this.displayItem = new DisplayItem(this, this.item); + } + this.shopType = ShopType.SELLING; + } + + /** + * Returns the number of items this shop has in stock. + * + * @return The number of items available for purchase. + */ + public int getRemainingStock() { + if (this.unlimited) + return 10000; + return Util.countItems(this.getInventory(), this.getItem()); + } + + /** + * Returns the number of free spots in the chest for the particular item. + * + * @param stackSize + * @return + */ + public int getRemainingSpace() { + if (this.unlimited) + return 10000; + return Util.countSpace(this.getInventory(), item); + } + + /** + * Returns true if the ItemStack matches what this shop is selling/buying + * + * @param item + * The ItemStack + * @return True if the ItemStack is the same (Excludes amounts) + */ + public boolean matches(ItemStack item) { + return Util.matches(this.item, item); + } + + /** + * Returns the shop that shares it's inventory with this one. + * + * @return the shop that shares it's inventory with this one. Will return + * null if this shop is not attached to another. + */ + public ContainerShop getAttachedShop() { + Block c = Util.getSecondHalf(this.getLocation().getBlock()); + if (c == null) + return null; + Shop shop = plugin.getShopManager().getShop(c.getLocation()); + return shop == null ? null : (ContainerShop) shop; + } + + /** + * Returns true if this shop is a double chest, and the other half is + * selling/buying the same as this is buying/selling. + * + * @return true if this shop is a double chest, and the other half is + * selling/buying the same as this is buying/selling. + */ + public boolean isDoubleShop() { + ContainerShop nextTo = this.getAttachedShop(); + if (nextTo == null) { + return false; + } + if (nextTo.matches(this.getItem())) { + // They're both trading the same item + if (this.getShopType() == nextTo.getShopType()) { + // They're both buying or both selling => Not a double shop, + // just two shops. + return false; + } else { + // One is buying, one is selling. + return true; + } + } else { + return false; + } + } + + /** + * @return The location of the shops chest + */ + public Location getLocation() { + return this.loc; + } + + /** + * @return The price per item this shop is selling + */ + public double getPrice() { + return this.price; + } + + /** + * Sets the price of the shop. Does not update it in the database. Use + * shop.update() for that. + * + * @param price + * The new price of the shop. + */ + public void setPrice(double price) { + this.price = price; + } + + /** + * @return The ItemStack type of this shop + */ + public Material getMaterial() { + return this.item.getType(); + } + + /** + * Upates the shop into the database. + */ + public void update() { + int x = this.getLocation().getBlockX(); + int y = this.getLocation().getBlockY(); + int z = this.getLocation().getBlockZ(); + String world = this.getLocation().getWorld().getName(); + int unlimited = this.isUnlimited() ? 1 : 0; + String q = "UPDATE shops SET owner = ?, itemConfig = ?, unlimited = ?, type = ?, price = ? WHERE x = ? AND y = ? and z = ? and world = ?"; + try { + plugin.getDB().execute(q, this.getOwner().toString(), Util.serialize(this.getItem()), unlimited, shopType.toID(), this.getPrice(), x, y, z, world); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Could not update shop in database! Changes will revert after a reboot!"); + } + } + + /** + * @return The durability of the item + */ + public short getDurability() { + return this.item.getDurability(); + } + + /** + * @return The chest this shop is based on. + */ + public Inventory getInventory() { + InventoryHolder container = (InventoryHolder) this.loc.getBlock().getState(); + return container.getInventory(); + } + + /** + * @return The name of the player who owns the shop. + */ + public UUID getOwner() { + return this.owner; + } + + /** + * @return The enchantments the shop has on its items. + */ + public Map getEnchants() { + return this.item.getItemMeta().getEnchants(); + } + + /** + * @return Returns a dummy itemstack of the item this shop is selling. + */ + public ItemStack getItem() { + return item; + } + + /** + * Removes an item from the shop. + * + * @param item + * The itemstack. The amount does not matter, just everything + * else + * @param amount + * The amount to remove from the shop. + */ + public void remove(ItemStack item, int amount) { + if (this.unlimited) + return; + Inventory inv = this.getInventory(); + int remains = amount; + while (remains > 0) { + int stackSize = Math.min(remains, item.getMaxStackSize()); + item.setAmount(stackSize); + inv.removeItem(item); + remains = remains - stackSize; + } + } + + /** + * Add an item to shops chest. + * + * @param item + * The itemstack. The amount does not matter, just everything + * else + * @param amount + * The amount to add to the shop. + */ + public void add(ItemStack item, int amount) { + if (this.unlimited) + return; + Inventory inv = this.getInventory(); + int remains = amount; + while (remains > 0) { + int stackSize = Math.min(remains, item.getMaxStackSize()); + item.setAmount(stackSize); + inv.addItem(item); + remains = remains - stackSize; + } + } + + /** + * Sells amount of item to Player p. Does NOT check our inventory, or + * balances + * + * @param p + * The player to sell to + * @param amount + * The amount to sell + */ + public void sell(Player p, int amount) { + if (amount < 0) + this.buy(p, -amount); + // Items to drop on floor + ArrayList floor = new ArrayList(5); + Inventory pInv = p.getInventory(); + if (this.isUnlimited()) { + ItemStack item = this.item.clone(); + while (amount > 0) { + int stackSize = Math.min(amount, this.item.getMaxStackSize()); + item.setAmount(stackSize); + pInv.addItem(item); + amount -= stackSize; + } + } else { + ItemStack[] chestContents = this.getInventory().getContents(); + for (int i = 0; amount > 0 && i < chestContents.length; i++) { + // Can't clone it here, it could be null + ItemStack item = chestContents[i]; + if (item != null && this.matches(item)) { + // Copy it, we don't want to interfere + item = item.clone(); + // Amount = total, item.getAmount() = how many items in the + // stack + int stackSize = Math.min(amount, item.getAmount()); + // If Amount is item.getAmount(), then this sets the amount + // to 0 + // Else it sets it to the remainder + chestContents[i].setAmount(chestContents[i].getAmount() - stackSize); + // We can modify this, it is a copy. + item.setAmount(stackSize); + // Add the items to the players inventory + floor.addAll(pInv.addItem(item).values()); + amount -= stackSize; + } + } + // We now have to update the chests inventory manually. + this.getInventory().setContents(chestContents); + } + for (int i = 0; i < floor.size(); i++) { + p.getWorld().dropItem(p.getLocation(), floor.get(i)); + } + } + + /** + * Buys amount of item from Player p. Does NOT check our inventory, or + * balances + * + * @param p + * The player to buy from + * @param item + * The itemStack to buy + * @param amount + * The amount to buy + */ + public void buy(Player p, int amount) { + if (amount < 0) + this.sell(p, -amount); + if (this.isUnlimited()) { + ItemStack[] contents = p.getInventory().getContents(); + for (int i = 0; amount > 0 && i < contents.length; i++) { + ItemStack stack = contents[i]; + if (stack == null) + continue; // No item + if (matches(stack)) { + int stackSize = Math.min(amount, stack.getAmount()); + stack.setAmount(stack.getAmount() - stackSize); + amount -= stackSize; + } + } + // Send the players new inventory to them + p.getInventory().setContents(contents); + // This should not happen. + if (amount > 0) { + plugin.getLogger().log(Level.WARNING, "Could not take all items from a players inventory on purchase! " + p.getName() + ", missing: " + amount + ", item: " + this.getDataName() + "!"); + } + } else { + ItemStack[] playerContents = p.getInventory().getContents(); + Inventory chestInv = this.getInventory(); + for (int i = 0; amount > 0 && i < playerContents.length; i++) { + ItemStack item = playerContents[i]; + if (item != null && this.matches(item)) { + // Copy it, we don't want to interfere + item = item.clone(); + // Amount = total, item.getAmount() = how many items in the + // stack + int stackSize = Math.min(amount, item.getAmount()); + // If Amount is item.getAmount(), then this sets the amount + // to 0 + // Else it sets it to the remainder + playerContents[i].setAmount(playerContents[i].getAmount() - stackSize); + // We can modify this, it is a copy. + item.setAmount(stackSize); + // Add the items to the players inventory + chestInv.addItem(item); + amount -= stackSize; + } + } + // Now update the players inventory. + p.getInventory().setContents(playerContents); + } + } + + /** + * Changes the owner of this shop to the given player. + * + * @param owner + * The name of the owner. You must do shop.update() after to save + * it after a reboot. + */ + public void setOwner(UUID owner) { + this.owner = owner; + } + + /** + * Returns the display item associated with this shop. + * + * @return The display item associated with this shop. + */ + public DisplayItem getDisplayItem() { + return this.displayItem; + } + + public void setUnlimited(boolean unlimited) { + this.unlimited = unlimited; + } + + public boolean isUnlimited() { + return this.unlimited; + } + + public ShopType getShopType() { + return this.shopType; + } + + public boolean isBuying() { + return this.shopType == ShopType.BUYING; + } + + public boolean isSelling() { + return this.shopType == ShopType.SELLING; + } + + /** + * Changes a shop type to Buying or Selling. Also updates the signs nearby. + * + * @param shopType + * The new type (ShopType.BUYING or ShopType.SELLING) + */ + public void setShopType(ShopType shopType) { + this.shopType = shopType; + this.setSignText(); + } + + /** + * Updates signs attached to the shop + */ + public void setSignText() { + if (Util.isLoaded(this.getLocation()) == false) + return; + String[] lines = new String[4]; + lines[0] = ChatColor.RED + "[QuickShop]"; + if (this.isBuying()) { + lines[1] = MsgUtil.getMessage("signs.buying", "" + this.getRemainingSpace()); + } + if (this.isSelling()) { + lines[1] = MsgUtil.getMessage("signs.selling", "" + this.getRemainingStock()); + } + lines[2] = Util.getNameForSign(this.item); + lines[3] = MsgUtil.getMessage("signs.price", "" + this.getPrice()); + this.setSignText(lines); + } + + /** + * Changes all lines of text on a sign near the shop + * + * @param lines + * The array of lines to change. Index is line number. + */ + public void setSignText(String[] lines) { + if (Util.isLoaded(this.getLocation()) == false) + return; + for (Sign sign : this.getSigns()) { + for (int i = 0; i < lines.length; i++) { + sign.setLine(i, lines[i]); + } + sign.update(); + } + } + + /** + * Returns a list of signs that are attached to this shop (QuickShop and + * blank signs only) + * + * @return a list of signs that are attached to this shop (QuickShop and + * blank signs only) + */ + public List getSigns() { + ArrayList signs = new ArrayList(1); + if (this.getLocation().getWorld() == null) + return signs; + Block[] blocks = new Block[4]; + blocks[0] = loc.getBlock().getRelative(1, 0, 0); + blocks[1] = loc.getBlock().getRelative(-1, 0, 0); + blocks[2] = loc.getBlock().getRelative(0, 0, 1); + blocks[3] = loc.getBlock().getRelative(0, 0, -1); + for (Block b : blocks) { + if (b.getType() != Material.WALL_SIGN) + continue; + if (!isAttached(b)) + continue; + Sign sign = (Sign) b.getState(); + if (sign.getLine(0).contains("[QuickShop")) { + signs.add(sign); + } else { + boolean text = false; + for (String s : sign.getLines()) { + if (!s.isEmpty()) { + text = true; + break; + } + } + if (!text) { + signs.add(sign); + } + } + } + return signs; + } + + public boolean isAttached(Block b) { + if (b.getType() != Material.WALL_SIGN) + new IllegalArgumentException(b + " Is not a sign!").printStackTrace(); + return this.getLocation().getBlock().equals(Util.getAttached(b)); + } + + /** + * Convenience method. Equivilant to + * org.maxgamer.quickshop.Util.getName(shop.getItem()). + * + * @return The name of this shops item + */ + public String getDataName() { + return Util.getName(this.getItem()); + } + + /** + * Deletes the shop from the list of shops and queues it for database + * deletion *DOES* delete it from memory + */ + public void delete() { + delete(true); + } + + /** + * Deletes the shop from the list of shops and queues it for database + * deletion + * + * @param fromMemory + * True if you are *NOT* iterating over this currently, *false if + * you are iterating* + */ + public void delete(boolean fromMemory) { + // Delete the display item + if (this.getDisplayItem() != null) { + this.getDisplayItem().remove(); + } + // Delete the signs around it + for (Sign s : this.getSigns()) { + s.getBlock().setType(Material.AIR); + } + // Delete it from the database + int x = this.getLocation().getBlockX(); + int y = this.getLocation().getBlockY(); + int z = this.getLocation().getBlockZ(); + String world = this.getLocation().getWorld().getName(); + plugin.getDB().execute("DELETE FROM shops WHERE x = '" + x + "' AND y = '" + y + "' AND z = '" + z + "' AND world = '" + world + "'"); + // Refund if necessary + if (plugin.getConfig().getBoolean("shop.refund")) { + plugin.getEcon().deposit(this.getOwner(), plugin.getConfig().getDouble("shop.cost")); + } + if (fromMemory) { + // Delete it from memory + plugin.getShopManager().removeShop(this); + } + } + + public boolean isValid() { + checkDisplay(); + return Util.canBeShop(this.getLocation().getBlock()); + } + + private void checkDisplay() { + if (plugin.display == false) + return; + if (getLocation().getWorld() == null) + return; // not loaded + boolean trans = Util.isTransparent(getLocation().clone().add(0.5, 1.2, 0.5).getBlock().getType()); + if (trans && this.getDisplayItem() == null) { + this.displayItem = new DisplayItem(this, this.getItem()); + this.getDisplayItem().spawn(); + } + if (this.getDisplayItem() != null) { + if (!trans) { // We have a display item in a block... delete it + this.getDisplayItem().remove(); + this.displayItem = null; + return; + } + DisplayItem disItem = this.getDisplayItem(); + Location dispLoc = disItem.getDisplayLocation(); + if (dispLoc.getBlock() != null && dispLoc.getBlock().getType() == Material.WATER) { // Flowing + // water. + // Stationery + // water + // does + // not + // move + // items. + disItem.remove(); + return; + } + if (disItem.getItem() == null) { + disItem.removeDupe(); + disItem.spawn(); + return; + } + Item item = disItem.getItem(); + if (item.getTicksLived() > 5000 || !item.isValid() || item.isDead()) { + disItem.respawn(); + disItem.removeDupe(); + } else if (item.getLocation().distanceSquared(dispLoc) > 1) { + item.teleport(dispLoc, TeleportCause.PLUGIN); + } + } + } + + public void onUnload() { + if (this.getDisplayItem() != null) { + this.getDisplayItem().remove(); + this.displayItem = null; + } + } + + public void onLoad() { + checkDisplay(); + } + + public void onClick() { + this.setSignText(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Shop " + (loc.getWorld() == null ? "unloaded world" : loc.getWorld().getName()) + "(" + loc.getBlockX() + ", " + loc.getBlockY() + ", " + loc.getBlockZ() + ")"); + sb.append(" Owner: " + getOwner().toString()); + if (isUnlimited()) + sb.append(" Unlimited: true"); + sb.append(" Price: " + getPrice()); + sb.append("Item: " + getItem().toString()); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/DisplayItem.java b/src/main/java/org/maxgamer/quickshop/Shop/DisplayItem.java new file mode 100644 index 0000000..50f39e1 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/DisplayItem.java @@ -0,0 +1,120 @@ +package org.maxgamer.quickshop.Shop; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Item; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Util.NMS; + +/** + * @author Netherfoam A display item, that spawns a block above the chest and + * cannot be interacted with. + */ +public class DisplayItem { + private Shop shop; + private ItemStack iStack; + private Item item; + + // private Location displayLoc; + /** + * Creates a new display item. + * + * @param shop + * The shop (See Shop) + * @param iStack + * The item stack to clone properties of the display item from. + */ + public DisplayItem(Shop shop, ItemStack iStack) { + this.shop = shop; + this.iStack = iStack.clone(); + // this.displayLoc = shop.getLocation().clone().add(0.5, 1.2, 0.5); + } + + /** + * Spawns the dummy item on top of the shop. + */ + public void spawn() { + if (shop.getLocation().getWorld() == null) + return; + Location dispLoc = this.getDisplayLocation(); + this.item = shop.getLocation().getWorld().dropItem(dispLoc, this.iStack); + this.item.setVelocity(new Vector(0, 0.1, 0)); + if (QuickShop.debug) { + System.out.println("Spawned item. Safeguarding."); + } + try { + NMS.safeGuard(this.item); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("QuickShop version mismatch! This version of QuickShop is incompatible with this version of bukkit! Try update?"); + } + } + + /** + * Spawns the new display item. Does not remove duplicate items. + */ + public void respawn() { + remove(); + spawn(); + } + + /** + * Removes all items floating ontop of the chest that aren't the display + * item. + */ + public boolean removeDupe() { + if (shop.getLocation().getWorld() == null) + return false; + // QuickShop qs = (QuickShop) + // Bukkit.getPluginManager().getPlugin("QuickShop"); + Location displayLoc = shop.getLocation().getBlock().getRelative(0, 1, 0).getLocation(); + boolean removed = false; + Chunk c = displayLoc.getChunk(); + for (Entity e : c.getEntities()) { + if (!(e instanceof Item)) + continue; + if (this.item != null && e.getEntityId() == this.item.getEntityId()) + continue; + Location eLoc = e.getLocation().getBlock().getLocation(); + if (eLoc.equals(displayLoc) || eLoc.equals(shop.getLocation())) { + ItemStack near = ((Item) e).getItemStack(); + // if its the same its a dupe + if (this.shop.matches(near)) { + e.remove(); + removed = true; + if (QuickShop.debug) { + System.out.println("Removed rogue item: " + near.getType()); + } + } + } + } + return removed; + } + + /** + * Removes the display item. + */ + public void remove() { + if (this.item == null) + return; + this.item.remove(); + } + + /** + * @return Returns the exact location of the display item. (1 above shop + * block, in the center) + */ + public Location getDisplayLocation() { + return this.shop.getLocation().clone().add(0.5, 1.2, 0.5); + } + + /** + * Returns the reference to this shops item. Do not modify. + */ + public Item getItem() { + return this.item; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/Info.java b/src/main/java/org/maxgamer/quickshop/Shop/Info.java new file mode 100644 index 0000000..76ffe9a --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/Info.java @@ -0,0 +1,98 @@ +package org.maxgamer.quickshop.Shop; + +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.inventory.ItemStack; + +public class Info { + private Location loc; + private ShopAction action; + private ItemStack item; + private Block last; + private Shop shop; + + /** + * Stores info for the players last shop interact. + * + * @param loc + * The location they clicked (Block.getLocation()) + * @param action + * The action (ShopAction.*) + * @param material + * The material they were holding + * @param data + * The data value of the material + */ + public Info(Location loc, ShopAction action, ItemStack item, Block last) { + this.loc = loc; + this.action = action; + this.last = last; + if (item != null) + this.item = item.clone(); + } + + /** + * Stores info for the players last shop interact. + * + * @param loc + * The location they clicked (Block.getLocation()) + * @param action + * The action (ShopAction.*) + * @param material + * The material they were holding + * @param data + * The data value of the material + * @param shop + * The shop they interacted with, or null if none + */ + public Info(Location loc, ShopAction action, ItemStack item, Block last, Shop shop) { + this.loc = loc; + this.action = action; + this.last = last; + if (item != null) + this.item = item.clone(); + if (shop != null) { + this.shop = shop.clone(); + } + } + + public boolean hasChanged(Shop shop) { + if (this.shop.isUnlimited() != shop.isUnlimited()) + return true; + if (this.shop.getShopType() != shop.getShopType()) + return true; + if (!this.shop.getOwner().equals(shop.getOwner())) + return true; + if (this.shop.getPrice() != shop.getPrice()) + return true; + if (!this.shop.getLocation().equals(shop.getLocation())) + return true; + if (!this.shop.matches(shop.getItem())) + return true; + return false; + } + + public ShopAction getAction() { + return this.action; + } + + public Location getLocation() { + return this.loc; + } + + /* + * public Material getMaterial(){ return this.item.getType(); } public byte + * getData(){ return this.getData(); } + */ + public ItemStack getItem() { + return this.item; + } + + public void setAction(ShopAction action) { + this.action = action; + } + + public Block getSignBlock() { + return this.last; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/ShopAction.java b/src/main/java/org/maxgamer/quickshop/Shop/ShopAction.java new file mode 100644 index 0000000..c4eeae4 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/ShopAction.java @@ -0,0 +1,5 @@ +package org.maxgamer.quickshop.Shop; + +public enum ShopAction { + BUY(), CREATE(), CANCELLED(); +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/ShopChunk.java b/src/main/java/org/maxgamer/quickshop/Shop/ShopChunk.java new file mode 100644 index 0000000..7c02b77 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/ShopChunk.java @@ -0,0 +1,44 @@ +package org.maxgamer.quickshop.Shop; + +public class ShopChunk { + private String world; + private int x; + private int z; + private int hash = 0; + + public ShopChunk(String world, int x, int z) { + this.world = world; + this.x = x; + this.z = z; + this.hash = this.x * this.z; // We don't need to use the world's hash, + // as these are seperated by world in + // memory + } + + public int getX() { + return this.x; + } + + public int getZ() { + return this.z; + } + + public String getWorld() { + return this.world; + } + + @Override + public boolean equals(Object obj) { + if (obj.getClass() != this.getClass()) { + return false; + } else { + ShopChunk shopChunk = (ShopChunk) obj; + return (this.getWorld().equals(shopChunk.getWorld()) && this.getX() == shopChunk.getX() && this.getZ() == shopChunk.getZ()); + } + } + + @Override + public int hashCode() { + return hash; + } +} diff --git a/src/main/java/org/maxgamer/quickshop/Shop/ShopCreateEvent.java b/src/main/java/org/maxgamer/quickshop/Shop/ShopCreateEvent.java new file mode 100644 index 0000000..7fab40c --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/ShopCreateEvent.java @@ -0,0 +1,55 @@ +package org.maxgamer.quickshop.Shop; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +public class ShopCreateEvent extends Event implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + private Shop shop; + private boolean cancelled; + private Player p; + + public ShopCreateEvent(Shop shop, Player p) { + this.shop = shop; + this.p = p; + } + + /** + * The shop to be created + * + * @return The shop to be created + */ + public Shop getShop() { + return this.shop; + } + + /** + * The player who is creating this shop + * + * @return The player who is creating this shop + */ + public Player getPlayer() { + return p; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/ShopManager.java b/src/main/java/org/maxgamer/quickshop/Shop/ShopManager.java new file mode 100644 index 0000000..71ee996 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/ShopManager.java @@ -0,0 +1,627 @@ +package org.maxgamer.quickshop.Shop; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Player; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.material.Sign; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Database.Database; +import org.maxgamer.quickshop.Util.MsgUtil; +import org.maxgamer.quickshop.Util.Util; + +public class ShopManager { + private QuickShop plugin; + private HashMap actions = new HashMap(); + private HashMap>> shops = new HashMap>>(); + + public ShopManager(QuickShop plugin) { + this.plugin = plugin; + } + + public Database getDatabase() { + return plugin.getDB(); + } + + /** + * @return Returns the HashMap. Info contains what + * their last question etc was. + */ + public HashMap getActions() { + return this.actions; + } + + public void createShop(Shop shop) { + Location loc = shop.getLocation(); + ItemStack item = shop.getItem(); + try { + // Write it to the database + String q = "INSERT INTO shops (owner, price, itemConfig, x, y, z, world, unlimited, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + plugin.getDB().execute(q, shop.getOwner().toString(), shop.getPrice(), Util.serialize(item), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), loc.getWorld().getName(), (shop.isUnlimited() ? 1 : 0), shop.getShopType().toID()); + // Add it to the world + addShop(loc.getWorld().getName(), shop); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Could not create shop! Changes will revert after a reboot!"); + } + } + + /** + * Loads the given shop into storage. This method is used for loading data + * from the database. Do not use this method to create a shop. + * + * @param world + * The world the shop is in + * @param shop + * The shop to load + */ + public void loadShop(String world, Shop shop) { + this.addShop(world, shop); + } + + /** + * Returns a hashmap of World -> Chunk -> Shop + * + * @return a hashmap of World -> Chunk -> Shop + */ + public HashMap>> getShops() { + return this.shops; + } + + /** + * Returns a hashmap of Chunk -> Shop + * + * @param world + * The name of the world (case sensitive) to get the list of + * shops from + * @return a hashmap of Chunk -> Shop + */ + public HashMap> getShops(String world) { + return this.shops.get(world); + } + + /** + * Returns a hashmap of Shops + * + * @param c + * The chunk to search. Referencing doesn't matter, only + * coordinates and world are used. + * @return + */ + public HashMap getShops(Chunk c) { + // long start = System.nanoTime(); + HashMap shops = getShops(c.getWorld().getName(), c.getX(), c.getZ()); + // long end = System.nanoTime(); + // System.out.println("Chunk lookup in " + ((end - start)/1000000.0) + + // "ms."); + return shops; + } + + public HashMap getShops(String world, int chunkX, int chunkZ) { + HashMap> inWorld = this.getShops(world); + if (inWorld == null) { + return null; + } + ShopChunk shopChunk = new ShopChunk(world, chunkX, chunkZ); + return inWorld.get(shopChunk); + } + + /** + * Gets a shop in a specific location + * + * @param loc + * The location to get the shop from + * @return The shop at that location + */ + public Shop getShop(Location loc) { + HashMap inChunk = getShops(loc.getChunk()); + if (inChunk == null) { + return null; + } + // We can do this because WorldListener updates the world reference so + // the world in loc is the same as world in inChunk.get(loc) + return inChunk.get(loc); + } + + /** + * Adds a shop to the world. Does NOT require the chunk or world to be + * loaded + * + * @param world + * The name of the world + * @param shop + * The shop to add + */ + private void addShop(String world, Shop shop) { + HashMap> inWorld = this.getShops().get(world); + // There's no world storage yet. We need to create that hashmap. + if (inWorld == null) { + inWorld = new HashMap>(3); + // Put it in the data universe + this.getShops().put(world, inWorld); + } + // Calculate the chunks coordinates. These are 1,2,3 for each chunk, NOT + // location rounded to the nearest 16. + int x = (int) Math.floor((shop.getLocation().getBlockX()) / 16.0); + int z = (int) Math.floor((shop.getLocation().getBlockZ()) / 16.0); + // Get the chunk set from the world info + ShopChunk shopChunk = new ShopChunk(world, x, z); + HashMap inChunk = inWorld.get(shopChunk); + // That chunk data hasn't been created yet - Create it! + if (inChunk == null) { + inChunk = new HashMap(1); + // Put it in the world + inWorld.put(shopChunk, inChunk); + } + // Put the shop in its location in the chunk list. + inChunk.put(shop.getLocation(), shop); + } + + /** + * Removes a shop from the world. Does NOT remove it from the database. * + * REQUIRES * the world to be loaded + * + * @param shop + * The shop to remove + */ + public void removeShop(Shop shop) { + Location loc = shop.getLocation(); + String world = loc.getWorld().getName(); + HashMap> inWorld = this.getShops().get(world); + int x = (int) Math.floor((shop.getLocation().getBlockX()) / 16.0); + int z = (int) Math.floor((shop.getLocation().getBlockZ()) / 16.0); + ShopChunk shopChunk = new ShopChunk(world, x, z); + HashMap inChunk = inWorld.get(shopChunk); + inChunk.remove(loc); + } + + /** + * Removes all shops from memory and the world. Does not delete them from + * the database. Call this on plugin disable ONLY. + */ + public void clear() { + if (plugin.display) { + for (World world : Bukkit.getWorlds()) { + for (Chunk chunk : world.getLoadedChunks()) { + HashMap inChunk = this.getShops(chunk); + if (inChunk == null) + continue; + for (Shop shop : inChunk.values()) { + shop.onUnload(); + } + } + } + } + this.actions.clear(); + this.shops.clear(); + } + + /** + * Checks other plugins to make sure they can use the chest they're making a + * shop. + * + * @param p + * The player to check + * @param b + * The block to check + * @return True if they're allowed to place a shop there. + */ + public boolean canBuildShop(Player p, Block b, BlockFace bf) { + if (plugin.limit) { + int owned = 0; + Iterator it = getShopIterator(); + while (it.hasNext()) { + if (it.next().getOwner().equals(p.getUniqueId())) { + owned++; + } + } + int max = plugin.getShopLimit(p); + if (owned + 1 > max) { + p.sendMessage(ChatColor.RED + "You have already created a maximum of " + owned + "/" + max + " shops!"); + return false; + } + } + PlayerInteractEvent pie = new PlayerInteractEvent(p, Action.RIGHT_CLICK_BLOCK, new ItemStack(Material.AIR), b, bf); // PIE + // = + // PlayerInteractEvent + // - + // What + // else? + Bukkit.getPluginManager().callEvent(pie); + pie.getPlayer().closeInventory(); // If the player has chat open, this + // will close their chat. + if (pie.isCancelled()) { + return false; + } + ShopPreCreateEvent spce = new ShopPreCreateEvent(p, b.getLocation()); + Bukkit.getPluginManager().callEvent(spce); + if (spce.isCancelled()) { + return false; + } + return true; + } + + public void handleChat(final Player p, String msg) { + final String message = ChatColor.stripColor(msg); + // Use from the main thread, because Bukkit hates life + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { + @SuppressWarnings("deprecation") + @Override + public void run() { + HashMap actions = getActions(); + // They wanted to do something. + Info info = actions.remove(p.getUniqueId()); + if (info == null) + return; // multithreaded means this can happen + if (info.getLocation().getWorld() != p.getLocation().getWorld()) { + p.sendMessage(MsgUtil.getMessage("shop-creation-cancelled")); + return; + } + if (info.getLocation().distanceSquared(p.getLocation()) > 25) { + p.sendMessage(MsgUtil.getMessage("shop-creation-cancelled")); + return; + } + /* Creation handling */ + if (info.getAction() == ShopAction.CREATE) { + try { + // Checking the shop can be created + if (plugin.getShopManager().getShop(info.getLocation()) != null) { + p.sendMessage(MsgUtil.getMessage("shop-already-owned")); + return; + } + if (Util.getSecondHalf(info.getLocation().getBlock()) != null && !p.hasPermission("quickshop.create.double")) { + p.sendMessage(MsgUtil.getMessage("no-double-chests")); + return; + } + if (Util.canBeShop(info.getLocation().getBlock()) == false) { + p.sendMessage(MsgUtil.getMessage("chest-was-removed")); + return; + } + // Price per item + double price; + if (plugin.getConfig().getBoolean("whole-number-prices-only")) { + price = Integer.parseInt(message); + } else { + price = Double.parseDouble(message); + } + if (price < 0.01) { + p.sendMessage(MsgUtil.getMessage("price-too-cheap")); + return; + } + double tax = plugin.getConfig().getDouble("shop.cost"); + // Tax refers to the cost to create a shop. Not actual + // tax, that would be silly + if (tax != 0 && plugin.getEcon().getBalance(p.getName()) < tax) { + p.sendMessage(MsgUtil.getMessage("you-cant-afford-a-new-shop", format(tax))); + return; + } + // Create the sample shop. + Shop shop = new ContainerShop(info.getLocation(), price, info.getItem(), p.getUniqueId()); + shop.onLoad(); + ShopCreateEvent e = new ShopCreateEvent(shop, p); + Bukkit.getPluginManager().callEvent(e); + if (e.isCancelled()) { + shop.onUnload(); + return; + } + // This must be called after the event has been called. + // Else, if the event is cancelled, they won't get their + // money back. + if (tax != 0) { + if (!plugin.getEcon().withdraw(p.getName(), tax)) { + p.sendMessage(MsgUtil.getMessage("you-cant-afford-a-new-shop", format(tax))); + shop.onUnload(); + return; + } + plugin.getEcon().deposit(plugin.getConfig().getString("tax-account"), tax); + } + /* The shop has hereforth been successfully created */ + createShop(shop); + Location loc = shop.getLocation(); + plugin.log(p.getName() + " created a " + shop.getDataName() + " shop at (" + loc.getWorld().getName() + " - " + loc.getX() + "," + loc.getY() + "," + loc.getZ() + ")"); + if (!plugin.getConfig().getBoolean("shop.lock")) { + // Warn them if they haven't been warned since + // reboot + if (!plugin.warnings.contains(p.getName())) { + p.sendMessage(MsgUtil.getMessage("shops-arent-locked")); + plugin.warnings.add(p.getName()); + } + } + // Figures out which way we should put the sign on and + // sets its text. + if (info.getSignBlock() != null && info.getSignBlock().getType() == Material.AIR && plugin.getConfig().getBoolean("shop.auto-sign")) { + BlockState bs = info.getSignBlock().getState(); + BlockFace bf = info.getLocation().getBlock().getFace(info.getSignBlock()); + bs.setType(Material.WALL_SIGN); + Sign sign = (Sign) bs.getData(); + sign.setFacingDirection(bf); + bs.update(true); + shop.setSignText(); + /* + * Block b = shop.getLocation().getBlock(); + * ItemFrame iFrame = (ItemFrame) + * b.getWorld().spawnEntity(b.getLocation(), + * EntityType.ITEM_FRAME); + * + * BlockFace[] faces = new + * BlockFace[]{BlockFace.NORTH, BlockFace.EAST, + * BlockFace.SOUTH, BlockFace.WEST}; for(BlockFace + * face : faces){ if(face == bf) continue; //This is + * the sign's location iFrame.setFacingDirection(bf, + * true); //iFrame.setItem(shop.getItem()); + * ItemStack iStack = shop.getItem().clone(); + * iStack.setAmount(0); iFrame.setItem(iStack); /* + * Field handleField = + * iFrame.getClass().getField("entity"); + * handleField.setAccessible(true); Object handle = + * handleField.get(iFrame); + * + * ItemStack bukkitStack = shop.getItem(); + * + * Field itemStackHandle = + * + * Method setItemStack = + * handle.getClass().getMethod("a", Object.class); + * setItemStack. + */ + // } + } + if (shop instanceof ContainerShop) { + ContainerShop cs = (ContainerShop) shop; + if (cs.isDoubleShop()) { + Shop nextTo = cs.getAttachedShop(); + if (nextTo.getPrice() > shop.getPrice()) { + // The one next to it must always be a + // buying shop. + p.sendMessage(MsgUtil.getMessage("buying-more-than-selling")); + } + } + } + } + /* They didn't enter a number. */ + catch (NumberFormatException ex) { + p.sendMessage(MsgUtil.getMessage("shop-creation-cancelled")); + return; + } + } + /* Purchase Handling */ + else if (info.getAction() == ShopAction.BUY) { + int amount = 0; + try { + amount = Integer.parseInt(message); + } catch (NumberFormatException e) { + p.sendMessage(MsgUtil.getMessage("shop-purchase-cancelled")); + return; + } + // Get the shop they interacted with + Shop shop = plugin.getShopManager().getShop(info.getLocation()); + // It's not valid anymore + if (shop == null || Util.canBeShop(info.getLocation().getBlock()) == false) { + p.sendMessage(MsgUtil.getMessage("chest-was-removed")); + return; + } + if (info.hasChanged(shop)) { + p.sendMessage(MsgUtil.getMessage("shop-has-changed")); + return; + } + if (shop.isSelling()) { + int stock = shop.getRemainingStock(); + if (stock < amount) { + p.sendMessage(MsgUtil.getMessage("shop-stock-too-low", "" + shop.getRemainingStock(), shop.getDataName())); + return; + } + if (amount == 0) { + // Dumb. + MsgUtil.sendPurchaseSuccess(p, shop, amount); + return; + } else if (amount < 0) { + // & Dumber + p.sendMessage(MsgUtil.getMessage("negative-amount")); + return; + } + int pSpace = Util.countSpace(p.getInventory(), shop.getItem()); + if (amount > pSpace) { + p.sendMessage(MsgUtil.getMessage("not-enough-space", "" + pSpace)); + return; + } + ShopPurchaseEvent e = new ShopPurchaseEvent(shop, p, amount); + Bukkit.getPluginManager().callEvent(e); + if (e.isCancelled()) + return; // Cancelled + // Money handling + if (!p.getUniqueId().equals(shop.getOwner())) { + // Check their balance. Works with *most* economy + // plugins* + if (plugin.getEcon().getBalance(p.getName()) < amount * shop.getPrice()) { + p.sendMessage(MsgUtil.getMessage("you-cant-afford-to-buy", format(amount * shop.getPrice()), format(plugin.getEcon().getBalance(p.getName())))); + return; + } + // Don't tax them if they're purchasing from + // themselves. + // Do charge an amount of tax though. + double tax = plugin.getConfig().getDouble("tax"); + double total = amount * shop.getPrice(); + if (!plugin.getEcon().withdraw(p.getUniqueId(), total)) { + p.sendMessage(MsgUtil.getMessage("you-cant-afford-to-buy", format(amount * shop.getPrice()), format(plugin.getEcon().getBalance(p.getName())))); + return; + } + if (!shop.isUnlimited() || plugin.getConfig().getBoolean("shop.pay-unlimited-shop-owners")) { + plugin.getEcon().deposit(shop.getOwner(), total * (1 - tax)); + if (tax != 0) { + plugin.getEcon().deposit(plugin.getConfig().getString("tax-account"), total * tax); + } + } + // Notify the shop owner + if (plugin.getConfig().getBoolean("show-tax")) { + String msg = MsgUtil.getMessage("player-bought-from-your-store-tax", p.getName(), "" + amount, shop.getDataName(), Util.format((tax * total))); + if (stock == amount) + msg += "\n" + MsgUtil.getMessage("shop-out-of-stock", "" + shop.getLocation().getBlockX(), "" + shop.getLocation().getBlockY(), "" + shop.getLocation().getBlockZ(), shop.getDataName()); + MsgUtil.send(shop.getOwner(), msg); + } else { + String msg = MsgUtil.getMessage("player-bought-from-your-store", p.getName(), "" + amount, shop.getDataName()); + if (stock == amount) + msg += "\n" + MsgUtil.getMessage("shop-out-of-stock", "" + shop.getLocation().getBlockX(), "" + shop.getLocation().getBlockY(), "" + shop.getLocation().getBlockZ(), shop.getDataName()); + MsgUtil.send(shop.getOwner(), msg); + } + } + // Transfers the item from A to B + shop.sell(p, amount); + MsgUtil.sendPurchaseSuccess(p, shop, amount); + plugin.log(p.getName() + " bought " + amount + " for " + (shop.getPrice() * amount) + " from " + shop.toString()); + } else if (shop.isBuying()) { + int space = shop.getRemainingSpace(); + if (space < amount) { + p.sendMessage(MsgUtil.getMessage("shop-has-no-space", "" + space, shop.getDataName())); + return; + } + int count = Util.countItems(p.getInventory(), shop.getItem()); + // Not enough items + if (amount > count) { + p.sendMessage(MsgUtil.getMessage("you-dont-have-that-many-items", "" + count, shop.getDataName())); + return; + } + if (amount == 0) { + // Dumb. + MsgUtil.sendPurchaseSuccess(p, shop, amount); + return; + } else if (amount < 0) { + // & Dumber + p.sendMessage(MsgUtil.getMessage("negative-amount")); + return; + } + // Money handling + if (!p.getUniqueId().equals(shop.getOwner())) { + // Don't tax them if they're purchasing from + // themselves. + // Do charge an amount of tax though. + double tax = plugin.getConfig().getDouble("tax"); + double total = amount * shop.getPrice(); + if (!shop.isUnlimited() || plugin.getConfig().getBoolean("shop.pay-unlimited-shop-owners")) { + // Tries to check their balance nicely to see if + // they can afford it. + if (plugin.getEcon().getBalance(shop.getOwner()) < amount * shop.getPrice()) { + p.sendMessage(MsgUtil.getMessage("the-owner-cant-afford-to-buy-from-you", format(amount * shop.getPrice()), format(plugin.getEcon().getBalance(shop.getOwner())))); + return; + } + // Check for plugins faking econ.has(amount) + if (!plugin.getEcon().withdraw(shop.getOwner(), total)) { + p.sendMessage(MsgUtil.getMessage("the-owner-cant-afford-to-buy-from-you", format(amount * shop.getPrice()), format(plugin.getEcon().getBalance(shop.getOwner())))); + return; + } + if (tax != 0) { + plugin.getEcon().deposit(plugin.getConfig().getString("tax-account"), total * tax); + } + } + // Give them the money after we know we succeeded + plugin.getEcon().deposit(p.getName(), total * (1 - tax)); + // Notify the owner of the purchase. + String msg = MsgUtil.getMessage("player-sold-to-your-store", p.getName(), "" + amount, shop.getDataName()); + if (space == amount) + msg += "\n" + MsgUtil.getMessage("shop-out-of-space", "" + shop.getLocation().getBlockX(), "" + shop.getLocation().getBlockY(), "" + shop.getLocation().getBlockZ()); + MsgUtil.send(shop.getOwner(), msg); + } + shop.buy(p, amount); + MsgUtil.sendSellSuccess(p, shop, amount); + plugin.log(p.getName() + " sold " + amount + " for " + (shop.getPrice() * amount) + " to " + shop.toString()); + } + shop.setSignText(); // Update the signs count + } + /* If it was already cancelled (from destroyed) */ + else { + return; // It was cancelled, go away. + } + } + }); + } + + /** + * Returns a new shop iterator object, allowing iteration over shops easily, + * instead of sorting through a 3D hashmap. + * + * @return a new shop iterator object. + */ + public Iterator getShopIterator() { + return new ShopIterator(); + } + + public String format(double d) { + return plugin.getEcon().format(d); + } + + public class ShopIterator implements Iterator { + private Iterator shops; + private Iterator> chunks; + private Iterator>> worlds; + private Shop current; + + public ShopIterator() { + worlds = getShops().values().iterator(); + } + + /** + * Returns true if there is still more shops to iterate over. + */ + @Override + public boolean hasNext() { + if (shops == null || !shops.hasNext()) { + if (chunks == null || !chunks.hasNext()) { + if (!worlds.hasNext()) { + return false; + } else { + chunks = worlds.next().values().iterator(); + return hasNext(); + } + } else { + shops = chunks.next().values().iterator(); + return hasNext(); + } + } + return true; + } + + /** + * Fetches the next shop. Throws NoSuchElementException if there are no + * more shops. + */ + @Override + public Shop next() { + if (shops == null || !shops.hasNext()) { + if (chunks == null || !chunks.hasNext()) { + if (!worlds.hasNext()) { + throw new NoSuchElementException("No more shops to iterate over!"); + } + chunks = worlds.next().values().iterator(); + } + shops = chunks.next().values().iterator(); + } + if (!shops.hasNext()) + return this.next(); // Skip to the next one (Empty iterator?) + current = shops.next(); + return current; + } + + /** + * Removes the current shop. This method will delete the shop from + * memory and the database. + */ + @Override + public void remove() { + current.delete(false); + shops.remove(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/ShopPreCreateEvent.java b/src/main/java/org/maxgamer/quickshop/Shop/ShopPreCreateEvent.java new file mode 100644 index 0000000..38455e5 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/ShopPreCreateEvent.java @@ -0,0 +1,61 @@ +package org.maxgamer.quickshop.Shop; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This event is called before the shop creation request is sent. E.g. A player + * clicks a chest, this event is thrown, if successful, the player is asked how + * much they wish to trade for. + */ +public class ShopPreCreateEvent extends Event implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + private boolean cancelled; + private Player p; + private Location loc; + + public ShopPreCreateEvent(Player p, Location loc) { + this.loc = loc; + this.p = p; + } + + /** + * The location of the shop that will be created. + * + * @return The location of the shop that will be created. + */ + public Location getLocation() { + return loc; + } + + /** + * The player who is creating this shop + * + * @return The player who is creating this shop + */ + public Player getPlayer() { + return p; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/ShopPurchaseEvent.java b/src/main/java/org/maxgamer/quickshop/Shop/ShopPurchaseEvent.java new file mode 100644 index 0000000..ec03f7a --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/ShopPurchaseEvent.java @@ -0,0 +1,76 @@ +package org.maxgamer.quickshop.Shop; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +public class ShopPurchaseEvent extends Event implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + private Shop shop; + private Player p; + private int amount; + private boolean cancelled; + + /** + * Builds a new shop purchase event + * + * @param shop + * The shop bought from + * @param p + * The player buying + * @param amount + * The amount they're buying + */ + public ShopPurchaseEvent(Shop shop, Player p, int amount) { + this.shop = shop; + this.p = p; + this.amount = amount; + } + + /** + * The shop used in this event + * + * @return The shop used in this event + */ + public Shop getShop() { + return this.shop; + } + + /** + * The player trading with the shop + * + * @return The player trading with the shop + */ + public Player getPlayer() { + return this.p; + } + + /** + * The amount the purchase was for + * + * @return The amount the purchase was for + */ + public int getAmount() { + return this.amount; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Shop/ShopType.java b/src/main/java/org/maxgamer/quickshop/Shop/ShopType.java new file mode 100644 index 0000000..ef466be --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Shop/ShopType.java @@ -0,0 +1,26 @@ +package org.maxgamer.quickshop.Shop; + +public enum ShopType { + SELLING(0), BUYING(1); + private int id; + private ShopType(int id){ + this.id = id; + } + + public static ShopType fromID(int id) { + for(ShopType type:ShopType.values()){ + if(type.id==id){ + return type; + } + } + return null; + } + + public static int toID(ShopType shopType) { + return shopType.id; + } + + public int toID() { + return id; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Util/Converter.java b/src/main/java/org/maxgamer/quickshop/Util/Converter.java new file mode 100644 index 0000000..2046288 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Util/Converter.java @@ -0,0 +1,12 @@ +package org.maxgamer.quickshop.Util; + +public class Converter { + /** + * Attempts to convert the quickshop database, if necessary. + * + * @return -1 for failure, 0 for no changes, 1 for success converting. + */ + public static int convert() { + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Util/MsgUtil.java b/src/main/java/org/maxgamer/quickshop/Util/MsgUtil.java new file mode 100644 index 0000000..a69d116 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Util/MsgUtil.java @@ -0,0 +1,283 @@ +package org.maxgamer.quickshop.Util; + +import java.io.File; +import java.io.InputStream; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Shop.Shop; + +@SuppressWarnings("deprecation") +public class MsgUtil { + private static QuickShop plugin; + private static YamlConfiguration messages; + private static HashMap> player_messages = new HashMap>(); + static { + plugin = QuickShop.instance; + } + + /** + * Loads all the messages from messages.yml + */ + public static void loadCfgMessages() { + // Load messages.yml + File messageFile = new File(plugin.getDataFolder(), "messages.yml"); + if (!messageFile.exists()) { + plugin.getLogger().info("Creating messages.yml"); + plugin.saveResource("messages.yml", true); + } + // Store it + messages = YamlConfiguration.loadConfiguration(messageFile); + messages.options().copyDefaults(true); + // Load default messages + InputStream defMessageStream = plugin.getResource("messages.yml"); + YamlConfiguration defMessages = YamlConfiguration.loadConfiguration(defMessageStream); + messages.setDefaults(defMessages); + // Parse colour codes + Util.parseColours(messages); + } + + /** + * loads all player purchase messages from the database. + */ + public static void loadTransactionMessages() { //TODO Converted to UUID + player_messages.clear(); // Delete old messages + try { + ResultSet rs = plugin.getDB().getConnection().prepareStatement("SELECT * FROM messages").executeQuery(); + while (rs.next()) { + UUID owner = UUID.fromString(rs.getString("owner")); + String message = rs.getString("message"); + LinkedList msgs = player_messages.get(owner); + if (msgs == null) { + msgs = new LinkedList(); + player_messages.put(owner, msgs); + } + msgs.add(message); + } + } catch (SQLException e) { + e.printStackTrace(); + System.out.println("Could not load transaction messages from database. Skipping."); + } + } + + /** + * @param player + * The name of the player to message + * @param message + * The message to send them Sends the given player a message if + * they're online. Else, if they're not online, queues it for + * them in the database. + */ + public static void send(UUID player, String message) { //TODO Converted to UUID + OfflinePlayer p = Bukkit.getOfflinePlayer(player); + if (p == null || !p.isOnline()) { + LinkedList msgs = player_messages.get(player); + if (msgs == null) { + msgs = new LinkedList(); + player_messages.put(player, msgs); + } + msgs.add(message); + String q = "INSERT INTO messages (owner, message, time) VALUES (?, ?, ?)"; + plugin.getDB().execute(q, player.toString(), message, System.currentTimeMillis()); + } else { + p.getPlayer().sendMessage(message); + } + } + + /** + * Deletes any messages that are older than a week in the database, to save + * on space. + */ + public static void clean() { + System.out.println("Cleaning purchase messages from database that are over a week old..."); + // 604800,000 msec = 1 week. + long weekAgo = System.currentTimeMillis() - 604800000; + plugin.getDB().execute("DELETE FROM messages WHERE time < ?", weekAgo); + } + + /** + * Empties the queue of messages a player has and sends them to the player. + * + * @param p + * The player to message + * @return true if success, false if the player is offline or null + */ + public static boolean flush(OfflinePlayer p) { //TODO Changed to UUID + if (p != null && p.isOnline()) { + UUID pName = p.getUniqueId(); + LinkedList msgs = player_messages.get(pName); + if (msgs != null) { + for (String msg : msgs) { + p.getPlayer().sendMessage(msg); + } + plugin.getDB().execute("DELETE FROM messages WHERE owner = ?", pName.toString()); + msgs.clear(); + } + return true; + } + return false; + } + + public static void sendShopInfo(Player p, Shop shop) { + sendShopInfo(p, shop, shop.getRemainingStock()); + } + + public static void sendShopInfo(Player p, Shop shop, int stock) { + // Potentially faster with an array? + ItemStack items = shop.getItem(); + p.sendMessage(""); + p.sendMessage(""); + p.sendMessage(ChatColor.DARK_PURPLE + "+---------------------------------------------------+"); + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.shop-information")); + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.owner", Bukkit.getOfflinePlayer(shop.getOwner()).getName())); + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.item", shop.getDataName())); + if (Util.isTool(items.getType())) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.damage-percent-remaining", Util.getToolPercentage(items))); + } + if (shop.isSelling()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.stock", "" + stock)); + } else { + int space = shop.getRemainingSpace(); + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.space", "" + space)); + } + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.price-per", shop.getDataName(), Util.format(shop.getPrice()))); + if (shop.isBuying()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.this-shop-is-buying")); + } else { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.this-shop-is-selling")); + } + Map enchs = items.getItemMeta().getEnchants(); + if (enchs != null && !enchs.isEmpty()) { + p.sendMessage(ChatColor.DARK_PURPLE + "+--------------------" + MsgUtil.getMessage("menu.enchants") + "-----------------------+"); + for (Entry entries : enchs.entrySet()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + ChatColor.YELLOW + entries.getKey().getName() + " " + entries.getValue()); + } + } + try { + Class.forName("org.bukkit.inventory.meta.EnchantmentStorageMeta"); + if (items.getItemMeta() instanceof EnchantmentStorageMeta) { + EnchantmentStorageMeta stor = (EnchantmentStorageMeta) items.getItemMeta(); + stor.getStoredEnchants(); + enchs = stor.getStoredEnchants(); + if (enchs != null && !enchs.isEmpty()) { + p.sendMessage(ChatColor.DARK_PURPLE + "+-----------------" + MsgUtil.getMessage("menu.stored-enchants") + "--------------------+"); + for (Entry entries : enchs.entrySet()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + ChatColor.YELLOW + entries.getKey().getName() + " " + entries.getValue()); + } + } + } + } catch (ClassNotFoundException e) { + // They don't have an up to date enough build of CB to do this. + // TODO: Remove this when it becomes redundant + } + p.sendMessage(ChatColor.DARK_PURPLE + "+---------------------------------------------------+"); + } + + public static void sendPurchaseSuccess(Player p, Shop shop, int amount) { + p.sendMessage(ChatColor.DARK_PURPLE + "+---------------------------------------------------+"); + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.successful-purchase")); + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.item-name-and-price", "" + amount, shop.getDataName(), Util.format((amount * shop.getPrice())))); + Map enchs = shop.getItem().getItemMeta().getEnchants(); + if (enchs != null && !enchs.isEmpty()) { + p.sendMessage(ChatColor.DARK_PURPLE + "+--------------------" + MsgUtil.getMessage("menu.enchants") + "-----------------------+"); + for (Entry entries : enchs.entrySet()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + ChatColor.YELLOW + entries.getKey().getName() + " " + entries.getValue()); + } + } + enchs = shop.getItem().getItemMeta().getEnchants(); + if (enchs != null && !enchs.isEmpty()) { + p.sendMessage(ChatColor.DARK_PURPLE + "+-----------------" + MsgUtil.getMessage("menu.stored-enchants") + "--------------------+"); + for (Entry entries : enchs.entrySet()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + ChatColor.YELLOW + entries.getKey().getName() + " " + entries.getValue()); + } + } + try { + Class.forName("org.bukkit.inventory.meta.EnchantmentStorageMeta"); + if (shop.getItem().getItemMeta() instanceof EnchantmentStorageMeta) { + EnchantmentStorageMeta stor = (EnchantmentStorageMeta) shop.getItem().getItemMeta(); + stor.getStoredEnchants(); + enchs = stor.getStoredEnchants(); + if (enchs != null && !enchs.isEmpty()) { + p.sendMessage(ChatColor.DARK_PURPLE + "+-----------------" + MsgUtil.getMessage("menu.stored-enchants") + "--------------------+"); + for (Entry entries : enchs.entrySet()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + ChatColor.YELLOW + entries.getKey().getName() + " " + entries.getValue()); + } + } + } + } catch (ClassNotFoundException e) { + // They don't have an up to date enough build of CB to do this. + // TODO: Remove this when it becomes redundant + } + p.sendMessage(ChatColor.DARK_PURPLE + "+---------------------------------------------------+"); + } + + public static void sendSellSuccess(Player p, Shop shop, int amount) { + p.sendMessage(ChatColor.DARK_PURPLE + "+---------------------------------------------------+"); + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.successfully-sold")); + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.item-name-and-price", "" + amount, shop.getDataName(), Util.format((amount * shop.getPrice())))); + if (plugin.getConfig().getBoolean("show-tax")) { + double tax = plugin.getConfig().getDouble("tax"); + double total = amount * shop.getPrice(); + if (tax != 0) { + if (!p.getUniqueId().equals(shop.getOwner())) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.sell-tax", "" + Util.format((tax * total)))); + } else { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + MsgUtil.getMessage("menu.sell-tax-self")); + } + } + } + Map enchs = shop.getItem().getItemMeta().getEnchants(); + if (enchs != null && !enchs.isEmpty()) { + p.sendMessage(ChatColor.DARK_PURPLE + "+--------------------" + MsgUtil.getMessage("menu.enchants") + "-----------------------+"); + for (Entry entries : enchs.entrySet()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + ChatColor.YELLOW + entries.getKey().getName() + " " + entries.getValue()); + } + } + try { + Class.forName("org.bukkit.inventory.meta.EnchantmentStorageMeta"); + if (shop.getItem().getItemMeta() instanceof EnchantmentStorageMeta) { + EnchantmentStorageMeta stor = (EnchantmentStorageMeta) shop.getItem().getItemMeta(); + stor.getStoredEnchants(); + enchs = stor.getStoredEnchants(); + if (enchs != null && !enchs.isEmpty()) { + p.sendMessage(ChatColor.DARK_PURPLE + "+--------------------" + MsgUtil.getMessage("menu.stored-enchants") + "-----------------------+"); + for (Entry entries : enchs.entrySet()) { + p.sendMessage(ChatColor.DARK_PURPLE + "| " + ChatColor.YELLOW + entries.getKey().getName() + " " + entries.getValue()); + } + } + } + } catch (ClassNotFoundException e) { + // They don't have an up to date enough build of CB to do this. + // TODO: Remove this when it becomes redundant + } + p.sendMessage(ChatColor.DARK_PURPLE + "+---------------------------------------------------+"); + } + + public static String getMessage(String loc, String... args) { + String raw = messages.getString(loc); + if (raw == null || raw.isEmpty()) { + return "Invalid message: " + loc; + } + if (args == null) { + return raw; + } + for (int i = 0; i < args.length; i++) { + raw = raw.replace("{" + i + "}", args[i]); //TODO Nullpointer? + } + return raw; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Util/NMS.java b/src/main/java/org/maxgamer/quickshop/Util/NMS.java new file mode 100644 index 0000000..57c4e2b --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Util/NMS.java @@ -0,0 +1,260 @@ +package org.maxgamer.quickshop.Util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Item; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.maxgamer.quickshop.QuickShop; + +public class NMS { + private static ArrayList dependents = new ArrayList(); + private static int nextId = 0; + private static NMSDependent nms; + + static { + NMSDependent dep; + dep = new NMSDependent("v1_6_R3") { + @Override + public void safeGuard(Item item) { + org.bukkit.inventory.ItemStack iStack = item.getItemStack(); + net.minecraft.server.v1_6_R3.ItemStack nmsI = org.bukkit.craftbukkit.v1_6_R3.inventory.CraftItemStack.asNMSCopy(iStack); + nmsI.count = 0; + iStack = org.bukkit.craftbukkit.v1_6_R3.inventory.CraftItemStack.asBukkitCopy(nmsI); + item.setItemStack(iStack); + } + + @Override + public byte[] getNBTBytes(org.bukkit.inventory.ItemStack iStack) { + net.minecraft.server.v1_6_R3.ItemStack is = org.bukkit.craftbukkit.v1_6_R3.inventory.CraftItemStack.asNMSCopy(iStack); + net.minecraft.server.v1_6_R3.NBTTagCompound itemCompound = new net.minecraft.server.v1_6_R3.NBTTagCompound(); + itemCompound = is.save(itemCompound); + return net.minecraft.server.v1_6_R3.NBTCompressedStreamTools.a(itemCompound); + } + + @Override + public org.bukkit.inventory.ItemStack getItemStack(byte[] bytes) { + net.minecraft.server.v1_6_R3.NBTTagCompound c = net.minecraft.server.v1_6_R3.NBTCompressedStreamTools.a(bytes); + net.minecraft.server.v1_6_R3.ItemStack is = net.minecraft.server.v1_6_R3.ItemStack.createStack(c); + return org.bukkit.craftbukkit.v1_6_R3.inventory.CraftItemStack.asBukkitCopy(is); + } + }; + dependents.add(dep); + dep = new NMSDependent("v1_7_R1") { + @Override + public void safeGuard(Item item) { + if(QuickShop.debug)System.out.println("safeGuard"); + org.bukkit.inventory.ItemStack iStack = item.getItemStack(); + net.minecraft.server.v1_7_R1.ItemStack nmsI = org.bukkit.craftbukkit.v1_7_R1.inventory.CraftItemStack.asNMSCopy(iStack); + nmsI.count = 0; + iStack = org.bukkit.craftbukkit.v1_7_R1.inventory.CraftItemStack.asBukkitCopy(nmsI); + item.setItemStack(iStack); + } + + @Override + public byte[] getNBTBytes(org.bukkit.inventory.ItemStack iStack) { + if(QuickShop.debug)System.out.println("getNBTBytes"); + net.minecraft.server.v1_7_R1.ItemStack is = org.bukkit.craftbukkit.v1_7_R1.inventory.CraftItemStack.asNMSCopy(iStack); + net.minecraft.server.v1_7_R1.NBTTagCompound itemCompound = new net.minecraft.server.v1_7_R1.NBTTagCompound(); + itemCompound = is.save(itemCompound); + return net.minecraft.server.v1_7_R1.NBTCompressedStreamTools.a(itemCompound); + } + + @Override + public org.bukkit.inventory.ItemStack getItemStack(byte[] bytes) { + if(QuickShop.debug)System.out.println("getItemStack"); + net.minecraft.server.v1_7_R1.NBTTagCompound c = net.minecraft.server.v1_7_R1.NBTCompressedStreamTools.a(bytes); + net.minecraft.server.v1_7_R1.ItemStack is = net.minecraft.server.v1_7_R1.ItemStack.createStack(c); + return org.bukkit.craftbukkit.v1_7_R1.inventory.CraftItemStack.asBukkitCopy(is); + } + }; + dependents.add(dep); + dep = new NMSDependent("v1_7_R3") { + @Override + public void safeGuard(Item item) { + if(QuickShop.debug)System.out.println("safeGuard"); + org.bukkit.inventory.ItemStack iStack = item.getItemStack(); + net.minecraft.server.v1_7_R3.ItemStack nmsI = org.bukkit.craftbukkit.v1_7_R3.inventory.CraftItemStack.asNMSCopy(iStack); + nmsI.count = 0; + iStack = org.bukkit.craftbukkit.v1_7_R3.inventory.CraftItemStack.asBukkitCopy(nmsI); + item.setItemStack(iStack); + } + + @Override + public byte[] getNBTBytes(org.bukkit.inventory.ItemStack iStack) { + if(QuickShop.debug)System.out.println("getNBTBytes"); + net.minecraft.server.v1_7_R3.ItemStack is = org.bukkit.craftbukkit.v1_7_R3.inventory.CraftItemStack.asNMSCopy(iStack); + net.minecraft.server.v1_7_R3.NBTTagCompound itemCompound = new net.minecraft.server.v1_7_R3.NBTTagCompound(); + itemCompound = is.save(itemCompound); + return net.minecraft.server.v1_7_R3.NBTCompressedStreamTools.a(itemCompound); + } + + @Override + public org.bukkit.inventory.ItemStack getItemStack(byte[] bytes) { + if(QuickShop.debug)System.out.println("getItemStack"); + net.minecraft.server.v1_7_R3.NBTTagCompound c = net.minecraft.server.v1_7_R3.NBTCompressedStreamTools.a(bytes, null); + net.minecraft.server.v1_7_R3.ItemStack is = net.minecraft.server.v1_7_R3.ItemStack.createStack(c); + return org.bukkit.craftbukkit.v1_7_R3.inventory.CraftItemStack.asBukkitCopy(is); + } + }; + dependents.add(dep); + dep = new NMSDependent("v1_8") { + @Override + public void safeGuard(Item item) { + if(QuickShop.debug)System.out.println("safeGuard"); + org.bukkit.inventory.ItemStack iStack = item.getItemStack(); + net.minecraft.server.v1_8_R1.ItemStack nmsI = org.bukkit.craftbukkit.v1_8_R1.inventory.CraftItemStack.asNMSCopy(iStack); + nmsI.count = 0; + iStack = org.bukkit.craftbukkit.v1_8_R1.inventory.CraftItemStack.asBukkitCopy(nmsI); + item.setItemStack(iStack); + } + + @Override + public byte[] getNBTBytes(org.bukkit.inventory.ItemStack iStack) { + try{ + if(QuickShop.debug)System.out.println("getNBTBytes"); + net.minecraft.server.v1_8_R1.ItemStack is = org.bukkit.craftbukkit.v1_8_R1.inventory.CraftItemStack.asNMSCopy(iStack); + net.minecraft.server.v1_8_R1.NBTTagCompound itemCompound = new net.minecraft.server.v1_8_R1.NBTTagCompound(); + itemCompound = is.save(itemCompound); + ByteArrayOutputStream bytearrayoutputstream = new ByteArrayOutputStream(); + DataOutputStream dataoutputstream = new DataOutputStream(new GZIPOutputStream(bytearrayoutputstream)); + try { + net.minecraft.server.v1_8_R1.NBTCompressedStreamTools.a(itemCompound, (DataOutput) dataoutputstream); + } finally { + dataoutputstream.close(); + } + return bytearrayoutputstream.toByteArray(); + }catch(Exception e){ + return new byte[0]; + } + //return net.minecraft.server.v1_8_R1.NBTCompressedStreamTools.a(itemCompound); + } + + @Override + public org.bukkit.inventory.ItemStack getItemStack(byte[] bytes) { + try{ + if(QuickShop.debug)System.out.println("getItemStack"); + DataInputStream datainputstream = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new ByteArrayInputStream(bytes)))); + net.minecraft.server.v1_8_R1.NBTTagCompound nbttagcompound; + try { + nbttagcompound = net.minecraft.server.v1_8_R1.NBTCompressedStreamTools.a((DataInput) datainputstream, null); + } finally { + datainputstream.close(); + } + //net.minecraft.server.v1_8_R1.NBTTagCompound c = net.minecraft.server.v1_8_R1.NBTCompressedStreamTools.a(bytes, null); + net.minecraft.server.v1_8_R1.ItemStack is = net.minecraft.server.v1_8_R1.ItemStack.createStack(nbttagcompound); + return org.bukkit.craftbukkit.v1_8_R1.inventory.CraftItemStack.asBukkitCopy(is); + }catch(Exception e){ + return new ItemStack(Material.AIR); + } + } + }; + dependents.add(dep); + } + + public static void safeGuard(Item item) throws ClassNotFoundException { + if(QuickShop.debug)System.out.println("Renaming"); + rename(item.getItemStack()); + if(QuickShop.debug)System.out.println("Protecting"); + protect(item); + if(QuickShop.debug)System.out.println("Seting pickup delay"); + item.setPickupDelay(2147483647); + } + + private static void rename(ItemStack iStack) { + ItemMeta meta = iStack.getItemMeta(); + meta.setDisplayName(ChatColor.RED + "QuickShop " + Util.getName(iStack) + " " + nextId++); + iStack.setItemMeta(meta); + } + + public static byte[] getNBTBytes(org.bukkit.inventory.ItemStack iStack) throws ClassNotFoundException { + validate(); + return nms.getNBTBytes(iStack); + } + + public static ItemStack getItemStack(byte[] bytes) throws ClassNotFoundException { + validate(); + return nms.getItemStack(bytes); + } + + private static void protect(Item item) { + try { + Field itemField = item.getClass().getDeclaredField("item"); + itemField.setAccessible(true); + Object nmsEntityItem = itemField.get(item); + Method getItemStack; + try { + getItemStack = nmsEntityItem.getClass().getMethod("getItemStack", new Class[0]); + } catch (NoSuchMethodException e) { + try { + getItemStack = nmsEntityItem.getClass().getMethod("d", new Class[0]); + } catch (NoSuchMethodException e2) { + return; + } + } + Object itemStack = getItemStack.invoke(nmsEntityItem, new Object[0]); + Field countField; + try { + countField = itemStack.getClass().getDeclaredField("count"); + } catch (NoSuchFieldException e) { + countField = itemStack.getClass().getDeclaredField("a"); + } + countField.setAccessible(true); + countField.set(itemStack, Integer.valueOf(0)); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + System.out.println("[QuickShop] Could not protect item from pickup properly! Dupes are now possible."); + } catch (Exception e) { + System.out.println("Other error"); + e.printStackTrace(); + } + } + + private static void validate() throws ClassNotFoundException { + if (nms != null) { + return; + } + String packageName = Bukkit.getServer().getClass().getPackage().getName(); + packageName = packageName.substring(packageName.lastIndexOf(".") + 1); + // System.out.println("Package: " + packageName); + for (NMSDependent dep : dependents) { + if ((packageName.startsWith(dep.getVersion())) || ((dep.getVersion().isEmpty()) && ((packageName.equals("bukkit")) || (packageName.equals("craftbukkit"))))) { + nms = dep; + return; + } + } + throw new ClassNotFoundException("This version of QuickShop is incompatible."); + } + + private static abstract class NMSDependent { + private String version; + + public String getVersion() { + return this.version; + } + + public NMSDependent(String version) { + this.version = version; + } + + public abstract void safeGuard(Item paramItem); + + public abstract byte[] getNBTBytes(org.bukkit.inventory.ItemStack paramItemStack); + + public abstract org.bukkit.inventory.ItemStack getItemStack(byte[] paramArrayOfByte); + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Util/Util.java b/src/main/java/org/maxgamer/quickshop/Util/Util.java new file mode 100644 index 0000000..dc4d8c8 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Util/Util.java @@ -0,0 +1,937 @@ +package org.maxgamer.quickshop.Util; + +import java.text.DecimalFormat; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.material.MaterialData; +import org.bukkit.material.Sign; +import org.bukkit.potion.Potion; +import org.bukkit.potion.PotionEffect; +import org.maxgamer.quickshop.QuickShop; + +@SuppressWarnings("deprecation") +public class Util { + private static HashSet tools = new HashSet(); + private static HashSet blacklist = new HashSet(); + private static HashSet shoppables = new HashSet(); + private static HashSet transparent = new HashSet(); + private static QuickShop plugin; + static { + plugin = QuickShop.instance; + for (String s : plugin.getConfig().getStringList("shop-blocks")) { + Material mat = Material.getMaterial(s.toUpperCase()); + if (mat == null) { + try { + mat = Material.getMaterial(Integer.parseInt(s)); + } catch (NumberFormatException e) { + } + } + if (mat == null) { + plugin.getLogger().info("Invalid shop-block: " + s); + } else { + shoppables.add(mat); + } + } + tools.add(Material.BOW); + tools.add(Material.SHEARS); + tools.add(Material.FISHING_ROD); + tools.add(Material.FLINT_AND_STEEL); + tools.add(Material.CHAINMAIL_BOOTS); + tools.add(Material.CHAINMAIL_CHESTPLATE); + tools.add(Material.CHAINMAIL_HELMET); + tools.add(Material.CHAINMAIL_LEGGINGS); + tools.add(Material.WOOD_AXE); + tools.add(Material.WOOD_HOE); + tools.add(Material.WOOD_PICKAXE); + tools.add(Material.WOOD_SPADE); + tools.add(Material.WOOD_SWORD); + tools.add(Material.LEATHER_BOOTS); + tools.add(Material.LEATHER_CHESTPLATE); + tools.add(Material.LEATHER_HELMET); + tools.add(Material.LEATHER_LEGGINGS); + tools.add(Material.DIAMOND_AXE); + tools.add(Material.DIAMOND_HOE); + tools.add(Material.DIAMOND_PICKAXE); + tools.add(Material.DIAMOND_SPADE); + tools.add(Material.DIAMOND_SWORD); + tools.add(Material.DIAMOND_BOOTS); + tools.add(Material.DIAMOND_CHESTPLATE); + tools.add(Material.DIAMOND_HELMET); + tools.add(Material.DIAMOND_LEGGINGS); + tools.add(Material.STONE_AXE); + tools.add(Material.STONE_HOE); + tools.add(Material.STONE_PICKAXE); + tools.add(Material.STONE_SPADE); + tools.add(Material.STONE_SWORD); + tools.add(Material.GOLD_AXE); + tools.add(Material.GOLD_HOE); + tools.add(Material.GOLD_PICKAXE); + tools.add(Material.GOLD_SPADE); + tools.add(Material.GOLD_SWORD); + tools.add(Material.GOLD_BOOTS); + tools.add(Material.GOLD_CHESTPLATE); + tools.add(Material.GOLD_HELMET); + tools.add(Material.GOLD_LEGGINGS); + tools.add(Material.IRON_AXE); + tools.add(Material.IRON_HOE); + tools.add(Material.IRON_PICKAXE); + tools.add(Material.IRON_SPADE); + tools.add(Material.IRON_SWORD); + tools.add(Material.IRON_BOOTS); + tools.add(Material.IRON_CHESTPLATE); + tools.add(Material.IRON_HELMET); + tools.add(Material.IRON_LEGGINGS); + List configBlacklist = plugin.getConfig().getStringList("blacklist"); + for (String s : configBlacklist) { + Material mat = Material.getMaterial(s.toUpperCase()); + if (mat == null) { + mat = Material.getMaterial(Integer.parseInt(s)); + if (mat == null) { + plugin.getLogger().info(s + " is not a valid material. Check your spelling or ID"); + continue; + } + } + blacklist.add(mat); + } + transparent.clear(); + // ToDo: add extras to config file + addTransparentBlock(Material.AIR); + /* Misc */ + addTransparentBlock(Material.CAKE_BLOCK); + /* Redstone Material */ + addTransparentBlock(Material.REDSTONE_WIRE); + /* Redstone Torches */ + addTransparentBlock(Material.REDSTONE_TORCH_OFF); + addTransparentBlock(Material.REDSTONE_TORCH_ON); + /* Diodes (Repeaters) */ + addTransparentBlock(Material.DIODE_BLOCK_OFF); + addTransparentBlock(Material.DIODE_BLOCK_ON); + /* Power Sources */ + addTransparentBlock(Material.DETECTOR_RAIL); + addTransparentBlock(Material.LEVER); + addTransparentBlock(Material.STONE_BUTTON); + addTransparentBlock(Material.WOOD_BUTTON); + addTransparentBlock(Material.STONE_PLATE); + addTransparentBlock(Material.WOOD_PLATE); + /* Nature Material */ + addTransparentBlock(Material.RED_MUSHROOM); + addTransparentBlock(Material.BROWN_MUSHROOM); + addTransparentBlock(Material.RED_ROSE); + addTransparentBlock(Material.YELLOW_FLOWER); + addTransparentBlock(Material.FLOWER_POT); + /* Greens */ + addTransparentBlock(Material.LONG_GRASS); + addTransparentBlock(Material.VINE); + addTransparentBlock(Material.WATER_LILY); + /* Seedy things */ + addTransparentBlock(Material.MELON_STEM); + addTransparentBlock(Material.PUMPKIN_STEM); + addTransparentBlock(Material.CROPS); + addTransparentBlock(Material.NETHER_WARTS); + /* Semi-nature */ + addTransparentBlock(Material.SNOW); + addTransparentBlock(Material.FIRE); + addTransparentBlock(Material.WEB); + addTransparentBlock(Material.TRIPWIRE); + addTransparentBlock(Material.TRIPWIRE_HOOK); + /* Stairs */ + addTransparentBlock(Material.COBBLESTONE_STAIRS); + addTransparentBlock(Material.BRICK_STAIRS); + addTransparentBlock(Material.SANDSTONE_STAIRS); + addTransparentBlock(Material.NETHER_BRICK_STAIRS); + addTransparentBlock(Material.SMOOTH_STAIRS); + /* Wood Stairs */ + addTransparentBlock(Material.BIRCH_WOOD_STAIRS); + addTransparentBlock(Material.WOOD_STAIRS); + addTransparentBlock(Material.JUNGLE_WOOD_STAIRS); + addTransparentBlock(Material.SPRUCE_WOOD_STAIRS); + /* Lava & Water */ + addTransparentBlock(Material.LAVA); + addTransparentBlock(Material.STATIONARY_LAVA); + addTransparentBlock(Material.WATER); + addTransparentBlock(Material.STATIONARY_WATER); + /* Saplings and bushes */ + addTransparentBlock(Material.SAPLING); + addTransparentBlock(Material.DEAD_BUSH); + /* Construction Material */ + /* Fences */ + addTransparentBlock(Material.FENCE); + addTransparentBlock(Material.FENCE_GATE); + addTransparentBlock(Material.IRON_FENCE); + addTransparentBlock(Material.NETHER_FENCE); + /* Ladders, Signs */ + addTransparentBlock(Material.LADDER); + addTransparentBlock(Material.SIGN_POST); + addTransparentBlock(Material.WALL_SIGN); + /* Bed */ + addTransparentBlock(Material.BED_BLOCK); + /* Pistons */ + addTransparentBlock(Material.PISTON_EXTENSION); + addTransparentBlock(Material.PISTON_MOVING_PIECE); + addTransparentBlock(Material.RAILS); + /* Torch & Trapdoor */ + addTransparentBlock(Material.TORCH); + addTransparentBlock(Material.TRAP_DOOR); + /* New */ + addTransparentBlock(Material.BREWING_STAND); + addTransparentBlock(Material.WOODEN_DOOR); + addTransparentBlock(Material.WOOD_STEP); + } + + public static boolean isTransparent(Material m) { + boolean trans = transparent.contains(m); + return trans; + } + + public static void addTransparentBlock(Material m) { + if (transparent.add(m) == false) { + System.out.println("Already added as transparent: " + m.toString()); + } + if (!m.isBlock()) + System.out.println(m + " is not a block!"); + } + + public static void parseColours(YamlConfiguration config) { + Set keys = config.getKeys(true); + for (String key : keys) { + String filtered = config.getString(key); + if (filtered.startsWith("MemorySection")) { + continue; + } + filtered = ChatColor.translateAlternateColorCodes('&', filtered); + config.set(key, filtered); + } + } + + /** + * Returns true if the given block could be used to make a shop out of. + * + * @param b + * The block to check. Possibly a chest, dispenser, etc. + * @return True if it can be made into a shop, otherwise false. + */ + public static boolean canBeShop(Block b) { + BlockState bs = b.getState(); + if (bs instanceof InventoryHolder == false) + return false; + return shoppables.contains(bs.getType()); + } + + /** + * Gets the percentage (Without trailing %) damage on a tool. + * + * @param item + * The ItemStack of tools to check + * @return The percentage 'health' the tool has. (Opposite of total damage) + */ + public static String getToolPercentage(ItemStack item) { + double dura = item.getDurability(); + double max = item.getType().getMaxDurability(); + DecimalFormat formatter = new DecimalFormat("0"); + return formatter.format((1 - dura / max) * 100.0); + } + + /** + * Returns the chest attached to the given chest. The given block must be a + * chest. + * + * @param b + * The chest to check. + * @return the block which is also a chest and connected to b. + */ + public static Block getSecondHalf(Block b) { + if (b.getType().toString().contains("CHEST") == false) + return null; + Block[] blocks = new Block[4]; + blocks[0] = b.getRelative(1, 0, 0); + blocks[1] = b.getRelative(-1, 0, 0); + blocks[2] = b.getRelative(0, 0, 1); + blocks[3] = b.getRelative(0, 0, -1); + for (Block c : blocks) { + if (c.getType() == b.getType()) { + return c; + } + } + return null; + } + + /** + * Converts a string into an item from the database. + * + * @param itemString + * The database string. Is the result of makeString(ItemStack + * item). + * @return A new itemstack, with the properties given in the string + */ + public static ItemStack makeItem(String itemString) { + String[] itemInfo = itemString.split(":"); + ItemStack item = new ItemStack(Material.getMaterial(itemInfo[0])); + MaterialData data = new MaterialData(Integer.parseInt(itemInfo[1])); + item.setData(data); + item.setDurability(Short.parseShort(itemInfo[2])); + item.setAmount(Integer.parseInt(itemInfo[3])); + for (int i = 4; i < itemInfo.length; i = i + 2) { + int level = Integer.parseInt(itemInfo[i + 1]); + Enchantment ench = Enchantment.getByName(itemInfo[i]); + if (ench == null) + continue; // Invalid + if (ench.canEnchantItem(item)) { + if (level <= 0) + continue; + level = Math.min(ench.getMaxLevel(), level); + item.addEnchantment(ench, level); + } + } + return item; + } + + public static String serialize(ItemStack iStack) { + YamlConfiguration cfg = new YamlConfiguration(); + cfg.set("item", iStack); + return cfg.saveToString(); + } + + public static ItemStack deserialize(String config) throws InvalidConfigurationException { + YamlConfiguration cfg = new YamlConfiguration(); + cfg.loadFromString(config); + ItemStack stack = cfg.getItemStack("item"); + return stack; + } + + /** + * Fetches an ItemStack's name - For example, converting INK_SAC:11 to + * Dandellion Yellow, or WOOL:14 to Red Wool + * + * @param i + * The itemstack to fetch the name of + * @return The human readable item name. + */ + public static String getName(ItemStack i) { + String vanillaName = getDataName(i.getType(), i.getDurability()); + return prettifyText(vanillaName); + } + + /** + * Converts a name like IRON_INGOT into Iron Ingot to improve readability + * + * @param ugly + * The string such as IRON_INGOT + * @return A nicer version, such as Iron Ingot + * + */ + public static String prettifyText(String ugly) { + String[] nameParts = ugly.split("_"); + if (nameParts.length==1) { + return firstUppercase(ugly); + } + + StringBuilder sb=new StringBuilder(); + for (String part : nameParts) { + sb.append(firstUppercase(part)+" "); + } + + return sb.toString(); + } + + // Let's make very long names shorter for our sign + public static String getNameForSign(ItemStack itemStack) { + String name = getDataName(itemStack.getType(), itemStack.getDurability()); + + String[] nameParts = name.split("_"); + if (nameParts.length==1) { + return firstUppercase(nameParts[0]); + } + + for (int i=0; i16) { + nameParts[i] = nameParts[i].substring(0, 1)+"."; + } else { + nameParts[i] = firstUppercase(nameParts[i]); + } + } + + nameParts[nameParts.length-1] = firstUppercase(nameParts[nameParts.length-1]); + + return StringUtils.join(nameParts); + } + + public static String firstUppercase(String string) { + if (string.length()>1) { + return Character.toUpperCase(string.charAt(0))+string.substring(1).toLowerCase(); + } else { + return string.toUpperCase(); + } + } + + + public static String toRomain(Integer value) { + return toRoman(value.intValue()); + } + + private static final String[] ROMAN = { "X", "IX", "V", "IV", "I" }; + private static final int[] DECIMAL = { 10, 9, 5, 4, 1 }; + + /** + * Converts the given number to roman numerals. If the number is >= 40 or <= + * 0, it will just return the number as a string. + * + * @param n + * The number to convert + * @return The roman numeral representation of this number, or the number in + * decimal form as a string if n <= 0 || n >= 40. + */ + public static String toRoman(int n) { + if (n <= 0 || n >= 40) + return "" + n; + String roman = ""; + for (int i = 0; i < ROMAN.length; i++) { + while (n >= DECIMAL[i]) { + n -= DECIMAL[i]; + roman += ROMAN[i]; + } + } + return roman; + } + + /** + * Converts a given material and data value into a format similar to + * Material..toString(). Upper case, with underscores. Includes material + * name in result. + * + * @param mat + * The base material. + * @param damage + * The durability/damage of the item. + * @return A string with the name of the item. + */ + private static String getDataName(Material mat, short damage) { + int id = mat.getId(); + switch (id) { + case 35: + switch ((int) damage) { + case 0: + return "WHITE_WOOL"; + case 1: + return "ORANGE_WOOL"; + case 2: + return "MAGENTA_WOOL"; + case 3: + return "LIGHT_BLUE_WOOL"; + case 4: + return "YELLOW_WOOL"; + case 5: + return "LIME_WOOL"; + case 6: + return "PINK_WOOL"; + case 7: + return "GRAY_WOOL"; + case 8: + return "LIGHT_GRAY_WOOL"; + case 9: + return "CYAN_WOOL"; + case 10: + return "PURPLE_WOOL"; + case 11: + return "BLUE_WOOL"; + case 12: + return "BROWN_WOOL"; + case 13: + return "GREEN_WOOL"; + case 14: + return "RED_WOOL"; + case 15: + return "BLACK_WOOL"; + } + return mat.toString(); + case 351: + switch ((int) damage) { + case 0: + return "INK_SAC"; + case 1: + return "ROSE_RED"; + case 2: + return "CACTUS_GREEN"; + case 3: + return "COCOA_BEANS"; + case 4: + return "LAPIS_LAZULI"; + case 5: + return "PURPLE_DYE"; + case 6: + return "CYAN_DYE"; + case 7: + return "LIGHT_GRAY_DYE"; + case 8: + return "GRAY_DYE"; + case 9: + return "PINK_DYE"; + case 10: + return "LIME_DYE"; + case 11: + return "DANDELION_YELLOW"; + case 12: + return "LIGHT_BLUE_DYE"; + case 13: + return "MAGENTA_DYE"; + case 14: + return "ORANGE_DYE"; + case 15: + return "BONE_MEAL"; + } + return mat.toString(); + case 98: + switch ((int) damage) { + case 0: + return "STONE_BRICKS"; + case 1: + return "MOSSY_STONE_BRICKS"; + case 2: + return "CRACKED_STONE_BRICKS"; + case 3: + return "CHISELED_STONE_BRICKS"; + } + return mat.toString(); + case 373: + // Special case,.. Why? + if (damage == 0) + return "WATER_BOTTLE"; + Potion pot; + try { + pot = Potion.fromDamage(damage); + } catch (Exception e) { + return "CUSTOM_POTION"; + } + String prefix = ""; + String suffix = ""; + if (pot.getLevel() > 0) + suffix += "_" + pot.getLevel(); + if (pot.hasExtendedDuration()) + prefix += "EXTENDED_"; + if (pot.isSplash()) + prefix += "SPLASH_"; + if (pot.getEffects().isEmpty()) { + switch ((int) pot.getNameId()) { + case 0: + return prefix + "MUNDANE_POTION" + suffix; + case 7: + return prefix + "CLEAR_POTION" + suffix; + case 11: + return prefix + "DIFFUSE_POTION" + suffix; + case 13: + return prefix + "ARTLESS_POTION" + suffix; + case 15: + return prefix + "THIN_POTION" + suffix; + case 16: + return prefix + "AWKWARD_POTION" + suffix; + case 32: + return prefix + "THICK_POTION" + suffix; + } + } else { + String effects = ""; + for (PotionEffect effect : pot.getEffects()) { + effects += effect.toString().split(":")[0]; + } + return prefix + effects + suffix; + } + return mat.toString(); + case 6: + switch ((int) damage) { + case 0: + return "OAK_SAPLING"; + case 1: + return "PINE_SAPLING"; + case 2: + return "BIRCH_SAPLING"; + case 3: + return "JUNGLE_TREE_SAPLING"; + } + return mat.toString(); + case 5: + switch ((int) damage) { + case 0: + return "OAK_PLANKS"; + case 1: + return "PINE_PLANKS"; + case 2: + return "BIRCH_PLANKS"; + case 3: + return "JUNGLE_PLANKS"; + } + return mat.toString(); + case 17: + switch (damage) { + case 0: + return "OAK_LOG"; + case 1: + return "PINE_LOG"; + case 2: + return "BIRCH_LOG"; + case 3: + return "JUNGLE_LOG"; + } + return mat.toString(); + case 18: + damage = (short) (damage % 4); + switch (damage) { + case 0: + return "OAK_LEAVES"; + case 1: + return "PINE_LEAVES"; + case 2: + return "BIRCH_LEAVES"; + case 3: + return "JUNGLE_LEAVES"; + } + case 263: + switch (damage) { + case 0: + return "COAL"; + case 1: + return "CHARCOAL"; + } + return mat.toString(); + case 24: + switch ((int) damage) { + case 0: + return "SANDSTONE"; + case 1: + return "CHISELED_SANDSTONE"; + case 2: + return "SMOOTH_SANDSTONE"; + } + return mat.toString(); + case 31: + switch ((int) damage) { + case 0: + return "DEAD_SHRUB"; + case 1: + return "TALL_GRASS"; + case 2: + return "FERN"; + } + return mat.toString(); + case 44: + switch ((int) damage) { + case 0: + return "STONE_SLAB"; + case 1: + return "SANDSTONE_SLAB"; + case 2: + return "WOODEN_SLAB"; + case 3: + return "COBBLESTONE_SLAB"; + case 4: + return "BRICK_SLAB"; + case 5: + return "STONE_BRICK_SLAB"; + } + return mat.toString(); + case 383: + switch ((int) damage) { + case 50: + return "CREEPER_EGG"; + case 51: + return "SKELETON_EGG"; + case 52: + return "SPIDER_EGG"; + case 53: + return "GIANT_EGG"; + case 54: + return "ZOMBIE_EGG"; + case 55: + return "SLIME_EGG"; + case 56: + return "GHAST_EGG"; + case 57: + return "ZOMBIE_PIGMAN_EGG"; + case 58: + return "ENDERMAN_EGG"; + case 59: + return "CAVE_SPIDER_EGG"; + case 60: + return "SILVERFISH_EGG"; + case 61: + return "BLAZE_EGG"; + case 62: + return "MAGMA_CUBE_EGG"; + case 63: + return "ENDER_DRAGON_EGG"; + case 90: + return "PIG_EGG"; + case 91: + return "SHEEP_EGG"; + case 92: + return "COW_EGG"; + case 93: + return "CHICKEN_EGG"; + case 94: + return "SQUID_EGG"; + case 95: + return "WOLF_EGG"; + case 96: + return "MOOSHROOM_EGG"; + case 97: + return "SNOW_GOLEM_EGG"; + case 98: + return "OCELOT_EGG"; + case 99: + return "IRON_GOLEM_EGG"; + case 120: + return "VILLAGER_EGG"; + case 200: + return "ENDER_CRYSTAL_EGG"; + case 14: + return "PRIMED_TNT_EGG"; + case 66: + return "WITCH_EGG"; + case 65: + return "BAT_EGG"; + } + return mat.toString(); + case 397: + switch ((int) damage) { + case 0: + return "SKELETON_SKULL"; + case 1: + return "WITHER_SKULL"; + case 2: + return "ZOMBIE_HEAD"; + case 3: + return "PLAYER_HEAD"; + case 4: + return "CREEPER_HEAD"; + } + break; + case 76: + return "REDSTONE_TORCH"; + case 115: + return "NETHER_WART"; + case 30: + return "COBWEB"; + case 102: + return "GLASS_PANE"; + case 101: + return "IRON_BARS"; + case 58: + return "CRAFTING_TABLE"; + case 123: + return "REDSTONE_LAMP"; + case 392: + return "POTATO"; + case 289: + return "GUNPOWDER"; + case 391: + return "CARROT"; + case 322: + switch ((int) damage) { + case 0: + return "GOLDEN_APPLE"; + case 1: + return "ENCHANTED_GOLDEN_APPLE"; + } + break; + case 390: + return "FLOWER_POT"; + case 145: + switch ((int) damage) { + case 0: + return "ANVIL"; + case 1: + return "SLIGHTLY_DAMAGED_ANVIL"; + case 2: + return "VERY_DAMAGED:ANVIL"; + } + break; + case 384: + return "BOTTLE_O'_ENCHANTING"; + case 402: + return "FIREWORK_STAR"; + case 385: + return "FIREWORK_CHARGE"; + } + if (damage == 0 || isTool(mat)) + return mat.toString(); + return mat.toString() + ":" + damage; + } + + /** + * @param mat + * The material to check + * @return Returns true if the item is a tool (Has durability) or false if + * it doesn't. + */ + public static boolean isTool(Material mat) { + return tools.contains(mat); + } + + /** + * Compares two items to each other. Returns true if they match. + * + * @param stack1 + * The first item stack + * @param stack2 + * The second item stack + * @return true if the itemstacks match. (Material, durability, enchants) + */ + public static boolean matches(ItemStack stack1, ItemStack stack2) { + if (stack1 == stack2) + return true; // Referring to the same thing, or both are null. + if (stack1 == null || stack2 == null) + return false; // One of them is null (Can't be both, see above) + if (stack1.getType() != stack2.getType()) + return false; // Not the same material + if (stack1.getDurability() != stack2.getDurability()) + return false; // Not the same durability + if (!stack1.getEnchantments().equals(stack2.getEnchantments())) + return false; // They have the same enchants + try { + Class.forName("org.bukkit.inventory.meta.EnchantmentStorageMeta"); + boolean book1 = stack1.getItemMeta() instanceof EnchantmentStorageMeta; + boolean book2 = stack2.getItemMeta() instanceof EnchantmentStorageMeta; + if (book1 != book2) + return false;// One has enchantment meta, the other does not. + if (book1 == true) { // They are the same here (both true or both + // false). So if one is true, the other is + // true. + Map ench1 = ((EnchantmentStorageMeta) stack1.getItemMeta()).getStoredEnchants(); + Map ench2 = ((EnchantmentStorageMeta) stack2.getItemMeta()).getStoredEnchants(); + if (!ench1.equals(ench2)) + return false; // Enchants aren't the same. + } + } catch (ClassNotFoundException e) { + // Nothing. They dont have a build high enough to support this. + } + return true; + } + + /** + * Formats the given number according to how vault would like it. E.g. $50 + * or 5 dollars. + * + * @return The formatted string. + */ + public static String format(double n) { + try { + return plugin.getEcon().format(n); + } catch (NumberFormatException e) { + return "$" + n; + } + } + + /** + * @param m + * The material to check if it is blacklisted + * @return true if the material is black listed. False if not. + */ + public static boolean isBlacklisted(Material m) { + return blacklist.contains(m); + } + + /** + * Fetches the block which the given sign is attached to + * + * @param sign + * The sign which is attached + * @return The block the sign is attached to + */ + public static Block getAttached(Block b) { + try { + Sign sign = (Sign) b.getState().getData(); // Throws a NPE + // sometimes?? + BlockFace attached = sign.getAttachedFace(); + if (attached == null) + return null; + return b.getRelative(attached); + } catch (NullPointerException e) { + return null; // /Not sure what causes this. + } + } + + /** + * Counts the number of items in the given inventory where + * Util.matches(inventory item, item) is true. + * + * @param inv + * The inventory to search + * @param item + * The ItemStack to search for + * @return The number of items that match in this inventory. + */ + public static int countItems(Inventory inv, ItemStack item) { + int items = 0; + for (ItemStack iStack : inv.getContents()) { + if (iStack == null) + continue; + if (Util.matches(item, iStack)) { + items += iStack.getAmount(); + } + } + return items; + } + + /** + * Returns the number of items that can be given to the inventory safely. + * + * @param inv + * The inventory to count + * @param item + * The item prototype. Material, durabiltiy and enchants must + * match for 'stackability' to occur. + * @return The number of items that can be given to the inventory safely. + */ + public static int countSpace(Inventory inv, ItemStack item) { + int space = 0; + for (ItemStack iStack : inv.getContents()) { + if (iStack == null || iStack.getType() == Material.AIR) { + space += item.getMaxStackSize(); + } else if (matches(item, iStack)) { + space += item.getMaxStackSize() - iStack.getAmount(); + } + } + return space; + } + + /** + * Returns true if the given location is loaded or not. + * + * @param loc + * The location + * @return true if the given location is loaded or not. + */ + public static boolean isLoaded(Location loc) { + // System.out.println("Checking isLoaded(Location loc)"); + if (loc.getWorld() == null) { + // System.out.println("Is not loaded. (No world)"); + return false; + } + // Calculate the chunks coordinates. These are 1,2,3 for each chunk, NOT + // location rounded to the nearest 16. + int x = (int) Math.floor((loc.getBlockX()) / 16.0); + int z = (int) Math.floor((loc.getBlockZ()) / 16.0); + if (loc.getWorld().isChunkLoaded(x, z)) { + // System.out.println("Chunk is loaded " + x + ", " + z); + return true; + } else { + // System.out.println("Chunk is NOT loaded " + x + ", " + z); + return false; + } + } +} diff --git a/src/main/java/org/maxgamer/quickshop/Watcher/ItemWatcher.java b/src/main/java/org/maxgamer/quickshop/Watcher/ItemWatcher.java new file mode 100644 index 0000000..5e45f06 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Watcher/ItemWatcher.java @@ -0,0 +1,52 @@ +package org.maxgamer.quickshop.Watcher; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.Shop.Shop; +import org.maxgamer.quickshop.Shop.ShopChunk; + +/** + * @author Netherfoam Maintains the display items, restoring them when needed. + * Also deletes invalid items. + */ +public class ItemWatcher implements Runnable { + private QuickShop plugin; + + public ItemWatcher(QuickShop plugin) { + this.plugin = plugin; + } + + public void run() { + List toRemove = new ArrayList(1); + for (Entry>> inWorld : plugin.getShopManager().getShops().entrySet()) { + // This world + World world = Bukkit.getWorld(inWorld.getKey()); + if (world == null) + continue; // world not loaded. + for (Entry> inChunk : inWorld.getValue().entrySet()) { + if (!world.isChunkLoaded(inChunk.getKey().getX(), inChunk.getKey().getZ())) { + // If the chunk is not loaded, next chunk! + continue; + } + for (Shop shop : inChunk.getValue().values()) { + // Validate the shop. + if (!shop.isValid()) { + toRemove.add(shop); + continue; + } + } + } + } + // Now we can remove it. + for (Shop shop : toRemove) { + shop.delete(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/Watcher/LogWatcher.java b/src/main/java/org/maxgamer/quickshop/Watcher/LogWatcher.java new file mode 100644 index 0000000..d32855c --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/Watcher/LogWatcher.java @@ -0,0 +1,53 @@ +package org.maxgamer.quickshop.Watcher; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; + +import org.bukkit.scheduler.BukkitTask; +import org.maxgamer.quickshop.QuickShop; + +public class LogWatcher implements Runnable { + private PrintStream ps; + private ArrayList logs = new ArrayList(5); + public BukkitTask task; + + public LogWatcher(QuickShop plugin, File log) { + try { + if (!log.exists()) { + log.createNewFile(); + } + FileOutputStream fos = new FileOutputStream(log, true); + this.ps = new PrintStream(fos); + } catch (FileNotFoundException e) { + e.printStackTrace(); + plugin.getLogger().severe("Log file not found!"); + } catch (IOException e) { + e.printStackTrace(); + plugin.getLogger().severe("Could not create log file!"); + } + } + + @Override + public void run() { + synchronized (logs) { + for (String s : logs) { + ps.println(s); + } + logs.clear(); + } + } + + public void add(String s) { + synchronized (logs) { + logs.add(s); + } + } + + public void close() { + this.ps.close(); + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..3157b8d --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,78 @@ +#Tax amount (decimal) - Eg, P1 buys $50 worth of stuff from P2. Therefore, P1 loses $50, P2 gains $(1-0.05)*50, and tax-account gains $(0.05)*50. +tax: 0.05 +#The fake player who money from taxing people goes to +tax-account: tax +#Whether or not to show taxes paid when selling to a shop +show-tax: false + +#Should we log transactions/creations to Bukkit\Plugins\QuickShop\qs.log? +log-actions: true + +#For item-item based economies that don't use virtual coins. +whole-number-prices-only: false + +database: + mysql: false + host: localhost + port: 3306 + database: quickshop + user: root + password: passwd + +#Limits the number of shops a person can create and own at a single time. +limits: + #Disable these if you're not using them! If this is false, the rest of this section is ignored + use: false + #The default number of shops players can make + default: 10 + #Players with these permissions can create these amounts of shops. + ranks: + #Anyone with 'quickshop.vip' permissions, can create 20 shops instead of 10. + quickshop.vip: 20 #Players with quickshop.vip can make 20 shops. + +#A list of block (materials) which can be used to create shops. +#By default, chests are added to this list. +#This will only work for blocks which implement InventoryHolder +#in other words, no enderchest shops, no shops on dirt blocks, etc. +#May cause unexpected behaviour with some blocks... Eg don't make a +#shop on a hopper (It sucks the display item in) and furnace shops +#allow players to put whatever item they want in all 3 slots and +#dispenser shops aren't protected from redstone... Etc. +shop-blocks: + - CHEST + - TRAPPED_CHEST + +shop: + #Cost to make a stall + cost: 10 + #Should we refund when their shops are deleted/removed/broken? + refund: false + + #Is there a fee for changing prices on a shop (Help deter endless undercutting) + price-change-requires-fee: true + #If price changes require a fee, how much is the fee + fee-for-price-change: 50 + + #Should we try and lock their shops from other players, so people can't steal from them? + lock: true + #Should we require players be sneaking to create and use shops? + sneak-to-create: false + sneak-to-trade: false + + #Should we automatically create the sign for the chest? + auto-sign: true + #If a player owns an unlimited shop, should they receive the cash from it or not? + #If you buy from YOUR unlimited shop, you will NEVER be charged $$, regardless of this setting + pay-unlimited-shop-owners: false + #Should we place display items on the chests? + display-items: true + #Should we place item frames on chests? + frame-items: true + #When someone uses /qs find , how far should we search in blocks? + #This command lets users shop quickly without wasting time searching + #Settings > 100 WILL cause lag. Don't do it, or don't cry when your server lags. + find-distance: 45 + +#List of items to disallow selling of. Anyone with quickshop.bypass. can bypass it +blacklist: + - 7 #Bedrock \ No newline at end of file diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..e902555 --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,104 @@ +# Colors: +# &0-9, &a-f +# {0}, {1}, {2}, etc are variables. You can swap them around, but adding a new variable won't work. Removing them will work + +not-looking-at-shop: "&cNo QuickShop found. You must be looking at one." +no-permission: "&cYou do not have permission to do that." +no-creative-break: "&cYou cannot break other players shops in creative mode. Use survival instead." +no-double-chests: "&cYou don't have permission to create a double-chest shop." +shop-already-owned: "&cThat is already a shop." +chest-was-removed: "&cThat chest was removed." +price-too-cheap: "&cPrice must be greater than &e$0.01" +no-price-change: "&cThat wouldn't result in a price change!" +you-cant-afford-a-new-shop: "&cIt costs {0} to create a new shop." +player-bought-from-your-store-tax: "&c{0} purchased {1} {2} from your store, and you paid {3} in taxes." +you-cant-afford-to-change-price: "&cIt costs {0} to change the price in your shop." +success-created-shop: "&aCreated shop." +success-removed-shop: "&aShop removed." +shops-arent-locked: "&cRemember, shops are NOT protected from theft! If you want to stop thieves, lock it with LWC, Lockette, etc!" +shop-creation-cancelled: "&cCancelled Shop Creation." +shop-purchase-cancelled: "&cCancelled Shop Purchase." +shop-stock-too-low: "&cThe shop only has {0} {1} left" +you-cant-afford-to-buy: "&cThat costs {0}, but you only have {1}" +negative-amount: "&cDerp, can't trade negative amounts" +player-bought-from-your-store: "&c{0} purchased {1} {2} from your store." +shop-out-of-stock: "&5Your shop at {0}, {1}, {2}, has run out of {3}" +shop-has-no-space: "&cThe shop only has room for {0} more {1}." +you-dont-have-that-many-items: "&cYou only have {0} {1}." +the-owner-cant-afford-to-buy-from-you: "&cThat costs {0} but the owner only has {1}" +player-sold-to-your-store: "&a{0} sold {1} {2} to your store." +shop-out-of-space: "&5Your shop at {0}, {1}, {2}, is now full." +fee-charged-for-price-change: "&aYou pay &c{0}&a to change the price." +price-is-now: "&aThe shops new price is &e{0}" +thats-not-a-number: "&cInvalid number" +no-price-given: "&cPlease give a valid price." +average-price-nearby: "&aAverage Price Nearby: &e{0}" +shop-has-changed: "&cThe shop you tried to use has changed since you clicked it!" +nearby-shop-this-way: "&aShop is {0} blocks away from you." +no-nearby-shop: "&cNo shops matching {0} nearby." +buying-more-than-selling: "&cWARNING: You are buying items for more than you are selling them!" +not-enough-space: "&cYou only have room for {0} more of that!" +refill-success: "&aRefill success" +empty-success: "&aEmpty success" + +menu: + successful-purchase: "&aSuccessfully Purchased:" + successfully-sold: "&aSuccessfully Sold:" + item-name-and-price: "&e{0} {1} &afor &e{2}" + sell-tax: "&aYou paid &e{0} &ain taxes." + sell-tax-self: "&aYou own this shop so you don't pay taxes." + enchants: "&5Enchants" + stored-enchants: "&5Stored Enchants" + + shop-information: "&aShop Information:" + owner: "&aOwner: {0}" + item: "&aItem: &e{0}" + space: "&aSpace: &e{0}" + stock: "&aStock &e{0}" + price-per: "&aPrice per &e{0} &a- &e{1}" + total-value-of-chest: "&aTotal value of Chest: &e{0}" + damage-percent-remaining: "&e{0}% &aRemaining." + this-shop-is-buying: "&aThis shop is &dBUYING&a items." + this-shop-is-selling: "&aThis shop is &bSELLING&a items." + + +bypassing-lock: "&cBypassing a QuickShop lock!" +that-is-locked: "&cThat shop is locked." + +how-many-buy: "&aEnter how many you wish to &bBUY&a in chat." +how-many-sell: "&aEnter how many you wish to &dSELL&a in chat. You have &e{0}&a available" + +not-allowed-to-create: "&cYou may not create a shop here." +blacklisted-item: "&cThat item is blacklisted. You may not sell it" +how-much-to-trade-for: "&aEnter how much you wish to trade one &e{0}&a for in chat." + +command: + #success-created-unlimited: "&aUnlimited QuickShop created." + toggle-unlimited: "&aShop is now {0}" + no-owner-given: "&cNo owner given. Use &a/qs setowner &c" + new-owner: "&aNew owner: &e{0}" + now-buying: "&aNow &dBUYING&a &e{0}" + now-selling: "&aNow &bSELLING &e{0}" + cleaning: "&aCleaning up shops with 0 stock..." + reloading: "&aReloading..." + cleaned: "&aCleaned &e{0}&a shops" + no-type-given: "&cUsage: /qs find " + description: + title: "&aQuickShop Help" + unlimited: "&eMakes a shop unlimited" + setowner: "&eChanges who owns a shop" + buy: "&eChanges a shop to &dBUY&e mode" + sell: "&eChanges a shop to &bSELL&e mode" + clean: "&eRemoves all (loaded) shops with 0 stock" + price: "&eChanges the buy/selling price of one of your shops" + find: "&eLocates the nearest shop of a specific type." + reload: "&eReloads QuickShop from config.yml" + refill: "&eAdds a given number of items to a shop" + empty: "&eRemoves all stock from a shop" + +signs: + #Line 1 is used as an identifier at the moment, so I kind of need it to work, thats why I won't let you change it + #Line 3 is the item name... There really isnt anything to change. + selling: "Selling {0}" + buying: "Buying {0}" + price: "For {0} each" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..35b2b5e --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,54 @@ +name: ${name} +main: ${package}.${name} +version: ${version} +author: Netherfoam, Timtower, KaiNoMood + +description: Economy Shops plugin +softdepend: [Herochat, Vault] + +commands: + qs: + description: QuickShop command + usage: /qs + aliases: [shop] + +permissions: + quickshop.create.sell: + description: Allows a player to sell from a shop + default: op + quickshop.create.buy: + description: Allows a player to buy from a shop + default: op + quickshop.create.double: + description: Allows a player to create a double shop + default: op + quickshop.use: + description: Allows a player to buy/sell using other players shops + default: true + quickshop.unlimited: + description: Allows a Staff Member to use /qs unlimited and make a shop infinite + quickshop.bypass.: + description: Allows a player to sell , even if its blacklisted + quickshop.other.destroy: + description: Allows a Staff Member to destroy other players shops if they are locked in the config + quickshop.other.open: + description: Allows a Staff Member to open someone elses shop if they are locked in the config + quickshop.other.price: + description: Allows a Staff Member to change the price of someone elses shop + quickshop.setowner: + description: Allows a Staff Member to change the owner of any shop + quickshop.find: + description: Allows a player to locate the nearest shop of a specific item type. Works in a 3 chunk radius. + default: true + quickshop.refill: + description: Allows a Staff Member to refill the shop theyre looking at with the given number of items. + default: op + quickshop.empty: + description: Allows a Staff Member to empty the shop theyre looking at of all items. + default: op + quickshop.debug: + description: Enables debug info to console + default: op + quickshop.export: + description: Allows exporting database to mysql or sqlite + default: op \ No newline at end of file