diff --git a/SaneEconomyCore/src/main/java/org/appledash/saneeconomy/economy/transaction/TransactionResult.java b/SaneEconomyCore/src/main/java/org/appledash/saneeconomy/economy/transaction/TransactionResult.java index 977a229..50c881c 100644 --- a/SaneEconomyCore/src/main/java/org/appledash/saneeconomy/economy/transaction/TransactionResult.java +++ b/SaneEconomyCore/src/main/java/org/appledash/saneeconomy/economy/transaction/TransactionResult.java @@ -39,8 +39,17 @@ public class TransactionResult { } public enum Status { - SUCCESS, + SUCCESS("Success."), + ERR_NOT_ENOUGH_FUNDS("Not enough money is available for you to complete that transaction."); - ERR_NOT_ENOUGH_FUNDS + private final String message; + + Status(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } } } diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/SaneEconomySignShop.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/SaneEconomySignShop.java index 2bff285..04d0d4c 100644 --- a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/SaneEconomySignShop.java +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/SaneEconomySignShop.java @@ -7,6 +7,7 @@ import org.appledash.saneeconomysignshop.listeners.SignChangeListener; import org.appledash.saneeconomysignshop.signshop.SignShopManager; import org.appledash.saneeconomysignshop.signshop.storage.SignShopStorageFlatfile; import org.appledash.saneeconomysignshop.util.ItemDatabase; +import org.appledash.saneeconomysignshop.util.LimitManager; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; @@ -19,6 +20,7 @@ import java.io.File; public class SaneEconomySignShop extends JavaPlugin { private ISaneEconomy saneEconomy; private final SignShopManager signShopManager = new SignShopManager(new SignShopStorageFlatfile(new File(getDataFolder(), "shops.db"))); + private final LimitManager limitManager = new LimitManager(); @Override public void onEnable() { @@ -50,4 +52,8 @@ public class SaneEconomySignShop extends JavaPlugin { public ISaneEconomy getSaneEconomy() { return saneEconomy; } + + public LimitManager getLimitManager() { + return limitManager; + } } diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/listeners/InteractListener.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/listeners/InteractListener.java index e29df2c..d08fde4 100644 --- a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/listeners/InteractListener.java +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/listeners/InteractListener.java @@ -1,12 +1,11 @@ package org.appledash.saneeconomysignshop.listeners; import org.appledash.saneeconomy.economy.EconomyManager; -import org.appledash.saneeconomy.economy.economable.Economable; import org.appledash.saneeconomy.economy.transaction.Transaction; -import org.appledash.saneeconomy.economy.transaction.TransactionReason; import org.appledash.saneeconomy.economy.transaction.TransactionResult; import org.appledash.saneeconomy.utils.MessageUtils; import org.appledash.saneeconomysignshop.SaneEconomySignShop; +import org.appledash.saneeconomysignshop.signshop.ShopTransaction; import org.appledash.saneeconomysignshop.signshop.SignShop; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -84,26 +83,30 @@ public class InteractListener implements Listener { private void doBuy(SignShop shop, Player player) { EconomyManager ecoMan = plugin.getSaneEconomy().getEconomyManager(); int quantity = player.isSneaking() ? 1 : shop.getQuantity(); - double price = shop.getBuyPrice(quantity); - if (!ecoMan.hasBalance(Economable.wrap(player), price)) { - MessageUtils.sendMessage(player, "You do not have enough money to buy {1} {2}.", quantity, shop.getItem()); + ShopTransaction shopTransaction = shop.makeTransaction(player, ShopTransaction.TransactionDirection.BUY, quantity); + + if (!plugin.getLimitManager().shouldAllowTransaction(shopTransaction)) { + MessageUtils.sendMessage(player, "You have reached your buying limit for the time being. Try back in an hour or so."); return; } - TransactionResult result = ecoMan.transact(new Transaction(Economable.wrap(player), Economable.PLUGIN, price, TransactionReason.PLUGIN_TAKE)); + plugin.getLimitManager().setRemainingLimit(player, ShopTransaction.TransactionDirection.BUY, shop.getItem(), plugin.getLimitManager().getRemainingLimit(player, ShopTransaction.TransactionDirection.BUY, shop.getItem()) - quantity); + + Transaction ecoTransaction = shopTransaction.makeEconomyTransaction(); + TransactionResult result = ecoMan.transact(ecoTransaction); if (result.getStatus() != TransactionResult.Status.SUCCESS) { MessageUtils.sendMessage(player, "An error occurred attempting to perform that transaction: {1}", result.getStatus()); return; } - ItemStack stack = shop.getItem().clone(); + ItemStack stack = shop.getItemStack().clone(); stack.setAmount(quantity); - player.getInventory().addItem(stack); - MessageUtils.sendMessage(player, "You have bought {1} {2} for {3}.", quantity, shop.getItem(), ecoMan.getCurrency().formatAmount(price)); - LOGGER.info(String.format("%s just bought %s for %s.", player.getName(), shop.getItem(), ecoMan.getCurrency().formatAmount(price))); + + MessageUtils.sendMessage(player, "You have bought {1} {2} for {3}.", quantity, shop.getItemStack().getType().name(), ecoMan.getCurrency().formatAmount(shopTransaction.getPrice())); + LOGGER.info(String.format("%s just bought %s for %s.", player.getName(), shop.getItemStack(), ecoMan.getCurrency().formatAmount(shopTransaction.getPrice()))); } private void doSell(SignShop shop, Player player) { // TODO: Selling enchanted items @@ -111,17 +114,29 @@ public class InteractListener implements Listener { int quantity = player.isSneaking() ? 1 : shop.getQuantity(); double price = shop.getSellPrice(quantity); - if (!player.getInventory().containsAtLeast(new ItemStack(shop.getItem()), quantity)) { - MessageUtils.sendMessage(player, "You do not have {1} {2}!", quantity, shop.getItem()); + if (!player.getInventory().containsAtLeast(new ItemStack(shop.getItemStack()), quantity)) { + MessageUtils.sendMessage(player, "You do not have {1} {2}!", quantity, shop.getItemStack().getType().name()); return; } - ItemStack stack = shop.getItem().clone(); - stack.setAmount(quantity); + ShopTransaction shopTransaction = shop.makeTransaction(player, ShopTransaction.TransactionDirection.SELL, quantity); + if (!plugin.getLimitManager().shouldAllowTransaction(shopTransaction)) { + MessageUtils.sendMessage(player, "You have reached your selling limit for the time being. Try back in an hour or so."); + return; + } + + plugin.getLimitManager().setRemainingLimit(player, ShopTransaction.TransactionDirection.SELL, shop.getItem(), plugin.getLimitManager().getRemainingLimit(player, ShopTransaction.TransactionDirection.BUY, shop.getItem()) - quantity); + + + ItemStack stack = shop.getItemStack().clone(); + stack.setAmount(quantity); player.getInventory().removeItem(stack); // FIXME: This does not remove items with damage values that were detected by contains() - ecoMan.transact(new Transaction(Economable.PLUGIN, Economable.wrap(player), price, TransactionReason.PLUGIN_GIVE)); - MessageUtils.sendMessage(player, "You have sold {1} {2} for {3}.", quantity, shop.getItem(), ecoMan.getCurrency().formatAmount(price)); - LOGGER.info(String.format("%s just sold %s for %s.", player.getName(), shop.getItem(), ecoMan.getCurrency().formatAmount(price))); + + ecoMan.transact(shopTransaction.makeEconomyTransaction()); + + MessageUtils.sendMessage(player, "You have sold {1} {2} for {3}.", quantity, shop.getItemStack().getType().name(), ecoMan.getCurrency().formatAmount(price)); + LOGGER.info(String.format("%s just sold %s for %s.", player.getName(), shop.getItemStack(), ecoMan.getCurrency().formatAmount(price))); } + } diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/listeners/SignChangeListener.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/listeners/SignChangeListener.java index ea1028f..13eab1a 100644 --- a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/listeners/SignChangeListener.java +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/listeners/SignChangeListener.java @@ -50,7 +50,7 @@ public class SignChangeListener implements Listener { plugin.getSignShopManager().addSignShop(signShop); evt.setLine(0, ChatColor.translateAlternateColorCodes('&', plugin.getConfig().getString("admin-shop-title"))); MessageUtils.sendMessage(evt.getPlayer(), "Sign shop created!"); - MessageUtils.sendMessage(evt.getPlayer(), "Item: {1} x {2}", signShop.getQuantity(), signShop.getItem()); + MessageUtils.sendMessage(evt.getPlayer(), "Item: {1} x {2}", signShop.getQuantity(), signShop.getItemStack()); if (signShop.canBuy()) { // The player be buying from the shop, not the other way around. MessageUtils.sendMessage(evt.getPlayer(), "Will sell to players for {!}.", diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/signshop/ShopTransaction.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/signshop/ShopTransaction.java new file mode 100644 index 0000000..22b8339 --- /dev/null +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/signshop/ShopTransaction.java @@ -0,0 +1,60 @@ +package org.appledash.saneeconomysignshop.signshop; + +import org.appledash.saneeconomy.economy.economable.Economable; +import org.appledash.saneeconomy.economy.transaction.Transaction; +import org.appledash.saneeconomy.economy.transaction.TransactionReason; +import org.appledash.saneeconomysignshop.util.ItemInfo; +import org.bukkit.entity.Player; + +/** + * Created by appledash on 1/1/17. + * Blackjack is still best pony. + */ +public class ShopTransaction { + // Direction is always what the player is doing. BUY = player is buying from shop. + private final TransactionDirection direction; + private final Player player; + private final ItemInfo item; + private final int quantity; + private final double price; + + public ShopTransaction(TransactionDirection direction, Player player, ItemInfo item, int quantity, double price) { + this.direction = direction; + this.player = player; + this.item = item; + this.quantity = quantity; + this.price = price; + } + + public TransactionDirection getDirection() { + return direction; + } + + public Player getPlayer() { + return player; + } + + public ItemInfo getItem() { + return item; + } + + public int getQuantity() { + return quantity; + } + + public double getPrice() { + return price; + } + + public Transaction makeEconomyTransaction() { + if (direction == TransactionDirection.BUY) { + return new Transaction(Economable.wrap(player), Economable.PLUGIN, price, TransactionReason.PLUGIN_TAKE); + } else { + return new Transaction(Economable.PLUGIN, Economable.wrap(player), price, TransactionReason.PLUGIN_GIVE); + } + } + + public enum TransactionDirection { + BUY, SELL + } +} diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/signshop/SignShop.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/signshop/SignShop.java index cde9354..a740a53 100644 --- a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/signshop/SignShop.java +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/signshop/SignShop.java @@ -1,8 +1,10 @@ package org.appledash.saneeconomysignshop.signshop; +import org.appledash.saneeconomysignshop.signshop.ShopTransaction.TransactionDirection; import org.appledash.saneeconomysignshop.util.ItemInfo; import org.appledash.saneeconomysignshop.util.SerializableLocation; import org.bukkit.Location; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import java.io.Serializable; @@ -49,10 +51,18 @@ public class SignShop implements Serializable { * Get the type of item this SignShop is selling * @return Material representing item/block type */ - public ItemStack getItem() { + public ItemStack getItemStack() { return item.toItemStack(); } + /** + * Get the ItemInfo for the item this SignShop is selling + * @return ItemInfo representing the type and quantity of item + */ + public ItemInfo getItem() { + return item; + } + /** * Get the price that the player can buy this item from the server for * @return Buy price for this.getQuantity() items @@ -120,4 +130,8 @@ public class SignShop implements Serializable { public int getQuantity() { return quantity; } + + public ShopTransaction makeTransaction(Player player, TransactionDirection direction, int quantity) { + return new ShopTransaction(direction, player, item, quantity, (direction == TransactionDirection.BUY) ? getBuyPrice(quantity) : getSellPrice(quantity)); + } } diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/DefaultHashMap.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/DefaultHashMap.java new file mode 100644 index 0000000..1ca08d8 --- /dev/null +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/DefaultHashMap.java @@ -0,0 +1,32 @@ +package org.appledash.saneeconomysignshop.util; + +import java.util.HashMap; +import java.util.function.Supplier; + +/** + * Created by appledash on 1/1/17. + * Blackjack is still best pony. + */ +public class DefaultHashMap extends HashMap { + private final Supplier defaultSupplier; + + public DefaultHashMap(Supplier defaultSupplier) { + if (defaultSupplier == null) { + throw new NullPointerException("defaultSupplier is null"); + } + + this.defaultSupplier = defaultSupplier; + } + + @Override + public V get(Object k) { + V v = super.get(k); + + if (v == null) { + v = defaultSupplier.get(); + this.put((K) k, v); + } + + return v; + } +} diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/ItemInfo.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/ItemInfo.java index aba7437..59b00b4 100644 --- a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/ItemInfo.java +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/ItemInfo.java @@ -4,27 +4,48 @@ import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import java.io.Serializable; +import java.util.Objects; /** * Created by appledash on 11/3/16. * Blackjack is still best pony. */ public class ItemInfo implements Serializable { - private Material id; - private short damage; - private int amount; + private final Material material; + private final short damage; + private final int amount; public ItemInfo(ItemStack stack) { this(stack.getType(), stack.getDurability(), stack.getAmount()); } - public ItemInfo(Material id, short damage, int amount) { - this.id = id; + public ItemInfo(Material material, short damage, int amount) { + this.material = material; this.damage = damage; this.amount = amount; } public ItemStack toItemStack() { - return new ItemStack(id, amount, damage); + return new ItemStack(material, amount, damage); + } + + public Material getMaterial() { + return material; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ItemInfo)) { + return false; + } + + ItemInfo other = ((ItemInfo) o); + + return (other.material == this.material) && (other.damage == this.damage); + } + + @Override + public int hashCode() { + return Objects.hash(material, damage); } } diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/ItemLimits.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/ItemLimits.java new file mode 100644 index 0000000..da6eed4 --- /dev/null +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/ItemLimits.java @@ -0,0 +1,26 @@ +package org.appledash.saneeconomysignshop.util; + +/** + * Created by appledash on 1/1/17. + * Blackjack is still best pony. + */ +public class ItemLimits { + // The default limit for items that have no limit. + public static final ItemLimits DEFAULT = new ItemLimits(10, 1); + + private final int limit; + private final int hourlyGain; + + public ItemLimits(int limit, int hourlyGain) { + this.limit = limit; + this.hourlyGain = hourlyGain; + } + + public int getHourlyGain() { + return hourlyGain; + } + + public int getLimit() { + return limit; + } +} diff --git a/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/LimitManager.java b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/LimitManager.java new file mode 100644 index 0000000..c8c517d --- /dev/null +++ b/SaneEconomySignShop/src/main/java/org/appledash/saneeconomysignshop/util/LimitManager.java @@ -0,0 +1,61 @@ +package org.appledash.saneeconomysignshop.util; + +import org.appledash.saneeconomysignshop.signshop.ShopTransaction; +import org.appledash.saneeconomysignshop.signshop.ShopTransaction.TransactionDirection; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Created by appledash on 1/1/17. + * Blackjack is still best pony. + */ +public class LimitManager { + private final Map> itemLimits = new DefaultHashMap<>(() -> new DefaultHashMap<>(() -> ItemLimits.DEFAULT)); + // This is a slightly complex data structure. It works like this: + // It's a map of (limit types to (maps of players to (maps of materials to the remaning limit))). + // All the TransactionDirections defaults to an empty map, which defaults to an empty map, which defaults to 0. + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private final Map>> playerLimits = new DefaultHashMap<>(() -> new DefaultHashMap<>(() -> new DefaultHashMap<>(() -> 0))); + + public int getRemainingLimit(Player player, TransactionDirection type, ItemInfo stack) { + return playerLimits.get(type).get(player.getUniqueId()).get(stack); + } + + public void setRemainingLimit(Player player, TransactionDirection type, ItemInfo stack, int limit) { + if (playerLimits.get(type).get(player.getUniqueId()).get(stack) == -1) { + return; + } + + limit = Math.min(limit, itemLimits.get(type).get(stack).getLimit()); + limit = Math.max(0, limit); + + playerLimits.get(type).get(player.getUniqueId()).put(stack, limit); + } + + public boolean shouldAllowTransaction(ShopTransaction transaction) { + return getRemainingLimit(transaction.getPlayer(), transaction.getDirection(), transaction.getItem()) >= transaction.getQuantity(); + } + + public void incrementLimitsHourly() { + // For every limit type + // For every player + // For every limit + // Increment limit by the limit for the specific direction and item. + + playerLimits.forEach((transactionDirection, playerToLimit) -> { + playerToLimit.forEach((playerUuid, itemToLimit) -> { + Map newLimits = new HashMap<>(); + + itemToLimit.forEach((itemInfo, currentLimit) -> { + newLimits.put(itemInfo, currentLimit + (itemLimits.get(transactionDirection).get(itemInfo).getHourlyGain())); + }); + + itemToLimit.putAll(newLimits); + }); + }); + } + +}