diff --git a/src/main/java/de/epiceric/shopchest/ShopChest.java b/src/main/java/de/epiceric/shopchest/ShopChest.java index 43b52bd..818e5f5 100644 --- a/src/main/java/de/epiceric/shopchest/ShopChest.java +++ b/src/main/java/de/epiceric/shopchest/ShopChest.java @@ -4,8 +4,8 @@ import com.palmergames.bukkit.towny.Towny; import com.sk89q.worldguard.bukkit.WorldGuardPlugin; import com.wasteofplastic.askyblock.ASkyBlock; import de.epiceric.shopchest.config.Config; +import de.epiceric.shopchest.config.HologramFormat; import de.epiceric.shopchest.config.Regex; -import de.epiceric.shopchest.event.ShopReloadEvent; import de.epiceric.shopchest.external.PlotSquaredShopFlag; import de.epiceric.shopchest.language.LanguageUtils; import de.epiceric.shopchest.language.LocalizedMessage; @@ -43,7 +43,8 @@ public class ShopChest extends JavaPlugin { private static ShopChest instance; - private Config config = null; + private Config config; + private HologramFormat hologramFormat; private ShopCommand shopCommand; private Economy econ = null; private Database database; @@ -153,8 +154,16 @@ public class ShopChest extends JavaPlugin { debug("Loading utils and extras..."); LanguageUtils.load(); + saveResource("item_names.txt", true); + File hologramFormatFile = new File(getDataFolder(), "hologram-format.yml"); + if (!hologramFormatFile.exists()) { + saveResource("hologram-format.yml", false); + } + + hologramFormat = new HologramFormat(this); + loadMetrics(); checkForUpdates(); @@ -406,9 +415,10 @@ public class ShopChest extends JavaPlugin { } } - /** - * @return The {@link ShopCommand} - */ + public HologramFormat getHologramFormat() { + return hologramFormat; + } + public ShopCommand getShopCommand() { return shopCommand; } diff --git a/src/main/java/de/epiceric/shopchest/config/Config.java b/src/main/java/de/epiceric/shopchest/config/Config.java index 82e9c32..d7d3a31 100644 --- a/src/main/java/de/epiceric/shopchest/config/Config.java +++ b/src/main/java/de/epiceric/shopchest/config/Config.java @@ -141,9 +141,6 @@ public class Config { /** Whether admin shops should be excluded of the shop limits **/ public boolean exclude_admin_shops; - /** Whether the buy- and sell price should be arranged below each other **/ - public boolean two_line_prices; - /** Whether the extension of a potion or tipped arrow (if available) should be appended to the item name. **/ public boolean append_potion_level_to_item_name; @@ -177,11 +174,8 @@ public class Config { **/ public boolean invert_mouse_buttons; - /** Amount a hologram with two price-lines should be lifted **/ - public double two_line_hologram_lift; - - /** Amount a hologram with one price-line should be lifted **/ - public double one_line_hologram_lift; + /** Whether the hologram's location should be fixed at the bottom **/ + public boolean hologram_fixed_bottom; /** Amount every hologram should be lifted **/ public double hologram_lift; @@ -362,7 +356,6 @@ public class Config { blacklist = (plugin.getConfig().getStringList("blacklist") == null) ? new ArrayList() : plugin.getConfig().getStringList("blacklist"); buy_greater_or_equal_sell = plugin.getConfig().getBoolean("buy-greater-or-equal-sell"); hopper_protection = plugin.getConfig().getBoolean("hopper-protection"); - two_line_prices = plugin.getConfig().getBoolean("two-line-prices"); enable_quality_mode = plugin.getConfig().getBoolean("enable-quality-mode"); enable_hologram_interaction = plugin.getConfig().getBoolean("enable-hologram-interaction"); enable_debug_log = plugin.getConfig().getBoolean("enable-debug-log"); @@ -384,8 +377,7 @@ public class Config { show_shop_items = plugin.getConfig().getBoolean("show-shop-items"); remove_shop_on_error = plugin.getConfig().getBoolean("remove-shop-on-error"); invert_mouse_buttons = plugin.getConfig().getBoolean("invert-mouse-buttons"); - two_line_hologram_lift = plugin.getConfig().getDouble("two-line-hologram-lift"); - one_line_hologram_lift = plugin.getConfig().getDouble("one-line-hologram-lift"); + hologram_fixed_bottom = plugin.getConfig().getBoolean("hologram-fixed-bottom"); hologram_lift = plugin.getConfig().getDouble("hologram-lift"); maximal_distance = plugin.getConfig().getDouble("maximal-distance"); maximal_item_distance = plugin.getConfig().getDouble("maximal-item-distance"); diff --git a/src/main/java/de/epiceric/shopchest/config/HologramFormat.java b/src/main/java/de/epiceric/shopchest/config/HologramFormat.java new file mode 100644 index 0000000..4f9b8eb --- /dev/null +++ b/src/main/java/de/epiceric/shopchest/config/HologramFormat.java @@ -0,0 +1,124 @@ +package de.epiceric.shopchest.config; + +import de.epiceric.shopchest.ShopChest; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import java.io.File; +import java.util.List; +import java.util.Map; + +public class HologramFormat { + + public enum Requirement { + VENDOR, AMOUNT, ITEM_TYPE, ITEM_NAME, HAS_ENCHANTMENT, BUY_PRICE, + SELL_PRICE, HAS_POTION_EFFECT, IS_MUSIC_DISC, IS_POTION_EXTENDED, IS_BOOK, ADMIN_SHOP, + NORMAL_SHOP, IN_STOCK, MAX_STACK + } + + private ShopChest plugin; + private YamlConfiguration config; + + public HologramFormat(ShopChest plugin) { + File configFile = new File(plugin.getDataFolder(), "hologram-format.yml"); + this.config = YamlConfiguration.loadConfiguration(configFile); + this.plugin = plugin; + } + + /** + * Get the format for the given line of the hologram + * @param line Line of the hologram + * @param values Values of the requirements that might be needed by the format (contains {@code null} if not comparable) + * @return The format of the first working option, or an empty String if no option is working + * because of not fulfilled requirements + */ + public String getFormat(int line, Map values) { + ConfigurationSection options = config.getConfigurationSection("lines." + line + ".options"); + + optionLoop: + for (String key : options.getKeys(false)) { + ConfigurationSection option = options.getConfigurationSection(key); + List requirements = option.getStringList("requirements"); + + String format = option.getString("format"); + + for (String sReq : requirements) { + for (Requirement req : values.keySet()) { + if (sReq.contains(req.toString())) { + if (!sReq.replace(req.toString(), "").trim().isEmpty()) { + if (!eval(sReq, values)) { + continue optionLoop; + } + } + } + } + } + + return format; + } + + return ""; + } + + /** Returns whether the hologram text has to change dynamically without reloading */ + public boolean isDynamic() { + int count = getLineCount(); + for (int i = 0; i < count; i++) { + ConfigurationSection options = config.getConfigurationSection("lines." + i + ".options"); + + for (String key : options.getKeys(false)) { + String format = options.getConfigurationSection(key).getString("format"); + if (format.contains(Regex.STOCK.getName())) { + return true; + } + } + } + + return false; + } + + /** Returns the amount of lines in a hologram */ + public int getLineCount() { + return config.getConfigurationSection("lines").getKeys(false).size(); + } + + /** Returns the configuration of the "hologram-format.yml" file */ + public YamlConfiguration getConfig() { + return config; + } + + /** + * Parse and evaluate a condition + * @param condition Condition to evaluate + * @param values Values of the requirements + * @return Result of the condition + */ + public boolean eval(String condition, Map values) { + try { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("JavaScript"); + + String c = condition; + + for (HologramFormat.Requirement req : HologramFormat.Requirement.values()) { + if (c.contains(req.toString()) && values.containsKey(req)) { + String replace = String.valueOf(values.get(req)); + if (values.get(req) instanceof String) { + replace = String.format("\"%s\"", replace); + } + c = c.replace(req.toString(), replace); + } + } + + return (boolean) engine.eval(c); + } catch (ScriptException e) { + plugin.debug("Failed to eval condition: " + condition); + plugin.debug(e); + } + + return false; + } +} diff --git a/src/main/java/de/epiceric/shopchest/config/Regex.java b/src/main/java/de/epiceric/shopchest/config/Regex.java index 6af7539..b5d7132 100644 --- a/src/main/java/de/epiceric/shopchest/config/Regex.java +++ b/src/main/java/de/epiceric/shopchest/config/Regex.java @@ -21,7 +21,9 @@ public enum Regex { VALUE("%VALUE%"), EXTENDED("%EXTENDED%"), REVENUE("%REVENUE%"), - GENERATION("%GENERATION%"); + GENERATION("%GENERATION%"), + STOCK("%STOCK%"), + MAX_STACK("%MAX-STACK%"); private String name; diff --git a/src/main/java/de/epiceric/shopchest/language/LanguageUtils.java b/src/main/java/de/epiceric/shopchest/language/LanguageUtils.java index 2abf402..58c1327 100644 --- a/src/main/java/de/epiceric/shopchest/language/LanguageUtils.java +++ b/src/main/java/de/epiceric/shopchest/language/LanguageUtils.java @@ -19,6 +19,7 @@ import org.bukkit.potion.Potion; import org.bukkit.potion.PotionType; import java.util.ArrayList; +import java.util.Map; public class LanguageUtils { @@ -1058,10 +1059,6 @@ public class LanguageUtils { messages.add(new LocalizedMessage(LocalizedMessage.Message.UPDATE_NO_UPDATE, langConfig.getString("message.update.no-update", "&6&lNo new update available."))); messages.add(new LocalizedMessage(LocalizedMessage.Message.UPDATE_CHECKING, langConfig.getString("message.update.checking", "&6&lChecking for updates..."))); messages.add(new LocalizedMessage(LocalizedMessage.Message.UPDATE_ERROR, langConfig.getString("message.update.error", "&c&lError while checking for updates."))); - messages.add(new LocalizedMessage(LocalizedMessage.Message.HOLOGRAM_FORMAT, langConfig.getString("message.hologram.format", "%AMOUNT% * %ITEMNAME%"), Regex.AMOUNT, Regex.ITEM_NAME)); - messages.add(new LocalizedMessage(LocalizedMessage.Message.HOLOGRAM_BUY_SELL, langConfig.getString("message.hologram.buy-and-sell", "Buy %BUY-PRICE% | %SELL-PRICE% Sell"), Regex.BUY_PRICE, Regex.SELL_PRICE)); - messages.add(new LocalizedMessage(LocalizedMessage.Message.HOLOGRAM_BUY, langConfig.getString("message.hologram.only-buy", "Buy %BUY-PRICE%"), Regex.BUY_PRICE)); - messages.add(new LocalizedMessage(LocalizedMessage.Message.HOLOGRAM_SELL, langConfig.getString("message.hologram.only-sell", "Sell %SELL-PRICE%"), Regex.SELL_PRICE)); messages.add(new LocalizedMessage(LocalizedMessage.Message.NO_PERMISSION_CREATE, langConfig.getString("message.noPermission.create", "&cYou don't have permission to create a shop."))); messages.add(new LocalizedMessage(LocalizedMessage.Message.NO_PERMISSION_CREATE_ADMIN, langConfig.getString("message.noPermission.create-admin", "&cYou don't have permission to create an admin shop."))); messages.add(new LocalizedMessage(LocalizedMessage.Message.NO_PERMISSION_CREATE_PROTECTED, langConfig.getString("message.noPermission.create-protected", "&cYou don't have permission to create a shop on a protected chest."))); @@ -1208,11 +1205,35 @@ public class LanguageUtils { return enchantmentString + " " + levelString; } + /** + * @param enchantmentMap Map of enchantments of an item + * @return Comma separated list of localized enchantments + */ + public static String getEnchantmentString(Map enchantmentMap) { + Enchantment[] enchantments = enchantmentMap.keySet().toArray(new Enchantment[enchantmentMap.size()]); + StringBuilder enchantmentList = new StringBuilder(); + + for (int i = 0; i < enchantments.length; i++) { + Enchantment enchantment = enchantments[i]; + + if (i == enchantments.length - 1) { + enchantmentList.append(LanguageUtils.getEnchantmentName(enchantment, enchantmentMap.get(enchantment))); + } else { + enchantmentList.append(LanguageUtils.getEnchantmentName(enchantment, enchantmentMap.get(enchantment))); + enchantmentList.append(", "); + } + } + + return enchantmentList.toString(); + } + /** * @param itemStack Potion Item whose base effect name should be looked up * @return Localized Name of the Base Potion Effect */ public static String getPotionEffectName(ItemStack itemStack) { + if (!(itemStack.getItemMeta() instanceof PotionMeta)) return ""; + PotionMeta potionMeta = (PotionMeta) itemStack.getItemMeta(); PotionType potionType; boolean upgraded; diff --git a/src/main/java/de/epiceric/shopchest/language/LocalizedMessage.java b/src/main/java/de/epiceric/shopchest/language/LocalizedMessage.java index b32503d..611b309 100644 --- a/src/main/java/de/epiceric/shopchest/language/LocalizedMessage.java +++ b/src/main/java/de/epiceric/shopchest/language/LocalizedMessage.java @@ -104,10 +104,6 @@ public class LocalizedMessage { UPDATE_NO_UPDATE, UPDATE_CHECKING, UPDATE_ERROR, - HOLOGRAM_FORMAT, - HOLOGRAM_BUY_SELL, - HOLOGRAM_BUY, - HOLOGRAM_SELL, NO_PERMISSION_CREATE, NO_PERMISSION_CREATE_ADMIN, NO_PERMISSION_CREATE_PROTECTED, diff --git a/src/main/java/de/epiceric/shopchest/listeners/ShopInteractListener.java b/src/main/java/de/epiceric/shopchest/listeners/ShopInteractListener.java index 4beca6d..21c3676 100644 --- a/src/main/java/de/epiceric/shopchest/listeners/ShopInteractListener.java +++ b/src/main/java/de/epiceric/shopchest/listeners/ShopInteractListener.java @@ -26,6 +26,7 @@ import de.epiceric.shopchest.shop.Shop; import de.epiceric.shopchest.shop.Shop.ShopType; import de.epiceric.shopchest.sql.Database; import de.epiceric.shopchest.utils.ClickType; +import de.epiceric.shopchest.utils.ItemUtils; import de.epiceric.shopchest.utils.Permissions; import de.epiceric.shopchest.utils.ShopUtils; import de.epiceric.shopchest.utils.Utils; @@ -49,14 +50,13 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.PlayerArmorStandManipulateEvent; import org.bukkit.event.player.PlayerInteractAtEntityEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.*; import org.bukkit.inventory.meta.BookMeta; -import org.bukkit.inventory.meta.EnchantmentStorageMeta; -import org.bukkit.inventory.meta.PotionMeta; -import org.bukkit.potion.Potion; +import org.bukkit.scheduler.BukkitRunnable; import pl.islandworld.api.IslandWorldApi; import us.talabrek.ultimateskyblock.api.IslandInfo; @@ -81,6 +81,29 @@ public class ShopInteractListener implements Listener { this.worldGuard = plugin.getWorldGuard(); } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent e) { + if (!plugin.getHologramFormat().isDynamic()) return; + + Inventory chestInv = e.getInventory(); + + if (!(e.getInventory().getHolder() instanceof Chest || e.getInventory().getHolder() instanceof DoubleChest)) { + return; + } + + Location loc = chestInv.getLocation(); + + final Shop shop = plugin.getShopUtils().getShop(loc); + if (shop == null) return; + + new BukkitRunnable() { + @Override + public void run() { + shop.updateHologramText(); + } + }.runTaskLater(plugin, 1L); + } + @EventHandler(ignoreCancelled = true) public void onPlayerManipulateArmorStand(PlayerArmorStandManipulateEvent e) { // When clicking an armor stand with an armor item, the armor stand will take it. @@ -715,39 +738,13 @@ public class ShopInteractListener implements Listener { LocalizedMessage.Message.SHOP_INFO_NORMAL : LocalizedMessage.Message.SHOP_INFO_ADMIN); String stock = LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_INFO_STOCK, - new LocalizedMessage.ReplacedRegex(Regex.AMOUNT, String.valueOf(amount))); + new LocalizedMessage.ReplacedRegex(Regex.STOCK, String.valueOf(amount))); - boolean potionExtended = false; - - Map enchantmentMap; - - String potionEffectName = ""; - if (Utils.getMajorVersion() >= 9) { - if (type == Material.TIPPED_ARROW || type == Material.LINGERING_POTION || type == Material.SPLASH_POTION) { - potionEffectName = LanguageUtils.getPotionEffectName(shop.getProduct()); - PotionMeta potionMeta = (PotionMeta) shop.getProduct().getItemMeta(); - potionExtended = potionMeta.getBasePotionData().isExtended(); - - if (potionEffectName.trim().isEmpty()) - potionEffectName = LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_INFO_NONE); - } - } - - if (type == Material.POTION) { - potionEffectName = LanguageUtils.getPotionEffectName(shop.getProduct()); - if (Utils.getMajorVersion() < 9) { - Potion potion = Potion.fromItemStack(shop.getProduct()); - potionExtended = potion.hasExtendedDuration(); - } else { - PotionMeta potionMeta = (PotionMeta) shop.getProduct().getItemMeta(); - potionExtended = potionMeta.getBasePotionData().isExtended(); - } - - if (potionEffectName.trim().isEmpty()) - potionEffectName = LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_INFO_NONE); - } + String potionEffectName = LanguageUtils.getPotionEffectName(shop.getProduct()); if (potionEffectName.length() > 0) { + boolean potionExtended = ItemUtils.isExtendedPotion(shop.getProduct()); + String extended = potionExtended ? LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_INFO_EXTENDED) : ""; potionEffectString = LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_INFO_POTION_EFFECT, new LocalizedMessage.ReplacedRegex(Regex.POTION_EFFECT, potionEffectName), @@ -779,25 +776,8 @@ public class ShopInteractListener implements Listener { new LocalizedMessage.ReplacedRegex(Regex.MUSIC_TITLE, musicDiscName)); } - String enchantmentList = ""; - if (shop.getProduct().getItemMeta() instanceof EnchantmentStorageMeta) { - EnchantmentStorageMeta esm = (EnchantmentStorageMeta) shop.getProduct().getItemMeta(); - enchantmentMap = esm.getStoredEnchants(); - } else { - enchantmentMap = shop.getProduct().getEnchantments(); - } - - Enchantment[] enchantments = enchantmentMap.keySet().toArray(new Enchantment[enchantmentMap.size()]); - - for (int i = 0; i < enchantments.length; i++) { - Enchantment enchantment = enchantments[i]; - - if (i == enchantments.length - 1) { - enchantmentList += LanguageUtils.getEnchantmentName(enchantment, enchantmentMap.get(enchantment)); - } else { - enchantmentList += LanguageUtils.getEnchantmentName(enchantment, enchantmentMap.get(enchantment)) + ", "; - } - } + Map enchantmentMap = ItemUtils.getEnchantments(shop.getProduct()); + String enchantmentList = LanguageUtils.getEnchantmentString(enchantmentMap); if (enchantmentList.length() > 0) { enchantmentString = LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_INFO_ENCHANTMENTS, diff --git a/src/main/java/de/epiceric/shopchest/nms/Hologram.java b/src/main/java/de/epiceric/shopchest/nms/Hologram.java index cefac40..dfe4ff9 100644 --- a/src/main/java/de/epiceric/shopchest/nms/Hologram.java +++ b/src/main/java/de/epiceric/shopchest/nms/Hologram.java @@ -2,47 +2,41 @@ package de.epiceric.shopchest.nms; import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.utils.Utils; +import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; -import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import java.util.UUID; public class Hologram { private static List holograms = new ArrayList<>(); private boolean exists = false; - private List entityList = new ArrayList<>(); - private List entityUuidList = new ArrayList<>(); - private String[] text; + private List nmsArmorStands = new ArrayList<>(); + private List armorStands = new ArrayList<>(); + private ArmorStand interactArmorStand; private Location location; private List visible = new ArrayList<>(); private ShopChest plugin; private Class entityArmorStandClass = Utils.getNMSClass("EntityArmorStand"); - private Class worldClass = Utils.getNMSClass("World"); private Class packetPlayOutSpawnEntityLivingClass = Utils.getNMSClass("PacketPlayOutSpawnEntityLiving"); private Class packetPlayOutEntityDestroyClass = Utils.getNMSClass("PacketPlayOutEntityDestroy"); - private Class entityClass = Utils.getNMSClass("Entity"); private Class entityLivingClass = Utils.getNMSClass("EntityLiving"); - private Class worldServerClass = Utils.getNMSClass("WorldServer"); - public Hologram(ShopChest plugin, String[] text, Location location) { + public Hologram(ShopChest plugin, String[] lines, Location location) { this.plugin = plugin; - this.text = text; this.location = location; Class[] requiredClasses = new Class[] { - worldClass, entityArmorStandClass, entityLivingClass, entityClass, - packetPlayOutSpawnEntityLivingClass, packetPlayOutEntityDestroyClass, - worldServerClass + entityArmorStandClass, entityLivingClass, packetPlayOutSpawnEntityLivingClass, + packetPlayOutEntityDestroyClass, }; for (Class c : requiredClasses) { @@ -52,59 +46,101 @@ public class Hologram { } } - create(); + create(lines); } - private void create() { - Location loc = location.clone(); + public void addLine(int line, String text) { + if (text == null || text.isEmpty()) return; - for (int i = 0; i <= text.length; i++) { - String text = null; + text = ChatColor.translateAlternateColorCodes('&', text); - if (i != this.text.length) { - text = this.text[i]; - if (text == null || text.isEmpty()) continue; - } else { - if (plugin.getShopChestConfig().enable_hologram_interaction) { - loc = location.clone(); - loc.add(0, 0.4, 0); - } else { - continue; - } - } + for (int i = line; i < armorStands.size(); i++) { + ArmorStand stand = armorStands.get(i); + stand.teleport(stand.getLocation().subtract(0, 0.25, 0)); + } + + if (line >= armorStands.size()) { + line = armorStands.size(); + } + + Location location = this.location.clone().subtract(0, line * 0.25, 0); + + try { + ArmorStand armorStand = (ArmorStand) location.getWorld().spawnEntity(location, EntityType.ARMOR_STAND); + armorStand.setGravity(false); + armorStand.setVisible(false); + armorStand.setCustomName(text); + armorStand.setCustomNameVisible(true); + + Object craftArmorStand = armorStand.getClass().cast(armorStand); + Object nmsArmorStand = craftArmorStand.getClass().getMethod("getHandle").invoke(craftArmorStand); + + nmsArmorStands.add(line, nmsArmorStand); + armorStands.add(line, armorStand); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + plugin.getLogger().severe("Could not create Hologram with reflection"); + plugin.debug("Could not create Hologram with reflection"); + plugin.debug(e); + } + } + + public void setLine(int line, String text) { + if (text == null ||text.isEmpty()) { + removeLine(line); + return; + } + + text = ChatColor.translateAlternateColorCodes('&', text); + + if (armorStands.size() <= line) { + addLine(line, text); + return; + } + + armorStands.get(line).setCustomName(text); + } + + public void removeLine(int line) { + for (int i = line + 1; i < armorStands.size(); i++) { + ArmorStand stand = armorStands.get(i); + stand.teleport(stand.getLocation().add(0, 0.25, 0)); + } + + if (armorStands.size() > line) { + armorStands.get(line).remove(); + armorStands.remove(line); + nmsArmorStands.remove(line); + } + } + + public String[] getLines() { + List lines = new ArrayList<>(); + for (ArmorStand armorStand : armorStands) { + lines.add(armorStand.getCustomName()); + } + + return lines.toArray(new String[lines.size()]); + } + + private void create(String[] lines) { + for (int i = 0; i < lines.length; i++) { + addLine(i, lines[i]); + } + + if (plugin.getShopChestConfig().enable_hologram_interaction) { + Location loc = location.clone().add(0, 0.4, 0); try { - Object craftWorld = loc.getWorld().getClass().cast(loc.getWorld()); - Object worldServer = craftWorld.getClass().getMethod("getHandle").invoke(craftWorld); + ArmorStand armorStand = (ArmorStand) location.getWorld().spawnEntity(loc, EntityType.ARMOR_STAND); + armorStand.setGravity(false); + armorStand.setVisible(false); - Constructor entityArmorStandConstructor = entityArmorStandClass.getConstructor(worldClass, double.class, double.class, double.class); - Object entityArmorStand = entityArmorStandConstructor.newInstance(worldServer, loc.getX(), loc.getY(),loc.getZ()); + Object craftArmorStand = armorStand.getClass().cast(armorStand); + Object nmsArmorStand = craftArmorStand.getClass().getMethod("getHandle").invoke(craftArmorStand); - if (text != null) { - entityArmorStandClass.getMethod("setCustomName", String.class).invoke(entityArmorStand, text); - entityArmorStandClass.getMethod("setCustomNameVisible", boolean.class).invoke(entityArmorStand, true); - } - - entityArmorStandClass.getMethod("setInvisible", boolean.class).invoke(entityArmorStand, true); - - if (Utils.getMajorVersion() < 10) { - entityArmorStandClass.getMethod("setGravity", boolean.class).invoke(entityArmorStand, false); - } else { - entityArmorStandClass.getMethod("setNoGravity", boolean.class).invoke(entityArmorStand, true); - } - - // Adds the entity to some lists so it can call interact events - Method addEntityMethod = worldServerClass.getDeclaredMethod((Utils.getMajorVersion() == 8 ? "a" : "b"), entityClass); - addEntityMethod.setAccessible(true); - addEntityMethod.invoke(worldServerClass.cast(worldServer), entityArmorStand); - - Object uuid = entityClass.getMethod("getUniqueID").invoke(entityArmorStand); - - entityUuidList.add((UUID) uuid); - entityList.add(entityArmorStand); - - loc.subtract(0, 0.25, 0); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { + nmsArmorStands.add(nmsArmorStand); + interactArmorStand = armorStand; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { plugin.getLogger().severe("Could not create Hologram with reflection"); plugin.debug("Could not create Hologram with reflection"); plugin.debug(e); @@ -119,7 +155,7 @@ public class Hologram { * @return Location of the hologram */ public Location getLocation() { - return location; + return location.clone(); } /** @@ -129,7 +165,7 @@ public class Hologram { new BukkitRunnable() { @Override public void run() { - for (Object o : entityList) { + for (Object o : nmsArmorStands) { try { Object entityLiving = entityLivingClass.cast(o); Object packet = packetPlayOutSpawnEntityLivingClass.getConstructor(entityLivingClass).newInstance(entityLiving); @@ -166,7 +202,7 @@ public class Hologram { } private void sendDestroyPackets(Player p) { - for (Object o : entityList) { + for (Object o : nmsArmorStands) { try { int id = (int) entityArmorStandClass.getMethod("getId").invoke(o); @@ -208,7 +244,17 @@ public class Hologram { * @return Whether the given armor stand is part of the hologram */ public boolean contains(ArmorStand armorStand) { - return entityUuidList.contains(armorStand.getUniqueId()); + return armorStands.contains(armorStand); + } + + /** Returns the ArmorStands of this hologram */ + public List getArmorStands() { + return armorStands; + } + + /** Returns the ArmorStand of this hologram that is responsible for interaction */ + public ArmorStand getInteractArmorStand() { + return interactArmorStand; } /** @@ -216,15 +262,10 @@ public class Hologram { * Hologram will be hidden from all players and will be killed */ public void remove() { - for (Object o : entityList) { - try { - o.getClass().getMethod("die").invoke(o); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - plugin.getLogger().severe("Could not remove Hologram with reflection"); - plugin.debug("Could not remove Hologram with reflection"); - plugin.debug(e); - } + for (ArmorStand armorStand : armorStands) { + armorStand.remove(); } + exists = false; holograms.remove(this); } diff --git a/src/main/java/de/epiceric/shopchest/shop/Shop.java b/src/main/java/de/epiceric/shopchest/shop/Shop.java index b5ce79e..1ebf58a 100644 --- a/src/main/java/de/epiceric/shopchest/shop/Shop.java +++ b/src/main/java/de/epiceric/shopchest/shop/Shop.java @@ -2,12 +2,15 @@ package de.epiceric.shopchest.shop; import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.config.Config; +import de.epiceric.shopchest.config.HologramFormat; import de.epiceric.shopchest.config.Regex; import de.epiceric.shopchest.exceptions.ChestNotFoundException; import de.epiceric.shopchest.exceptions.NotEnoughSpaceException; import de.epiceric.shopchest.language.LanguageUtils; -import de.epiceric.shopchest.language.LocalizedMessage; +import de.epiceric.shopchest.nms.CustomBookMeta; import de.epiceric.shopchest.nms.Hologram; +import de.epiceric.shopchest.utils.ItemUtils; +import de.epiceric.shopchest.utils.Utils; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -21,6 +24,11 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + public class Shop { private boolean created; @@ -124,7 +132,7 @@ public class Shop { Location itemLocation; ItemStack itemStack; - itemLocation = new Location(location.getWorld(), hologram.getLocation().getX(), location.getY() + 1, hologram.getLocation().getZ()); + itemLocation = new Location(location.getWorld(), hologram.getLocation().getX(), location.getY() + 0.9, hologram.getLocation().getZ()); itemStack = product.clone(); itemStack.setAmount(1); @@ -162,44 +170,99 @@ public class Shop { doubleChest = false; } - boolean twoLinePrices = config.two_line_prices; - - String[] holoText = getHologramText(twoLinePrices ? 3 : 2); - Location holoLocation = getHologramLocation(doubleChest, chests, holoText); + String[] holoText = getHologramText(); + Location holoLocation = getHologramLocation(doubleChest, chests); hologram = new Hologram(plugin, holoText, holoLocation); } - private String[] getHologramText(int length) { - String[] holoText = new String[length]; + public void updateHologramText() { + String[] lines = getHologramText(); + String[] currentLines = hologram.getLines(); - holoText[0] = LanguageUtils.getMessage(LocalizedMessage.Message.HOLOGRAM_FORMAT, - new LocalizedMessage.ReplacedRegex(Regex.AMOUNT, String.valueOf(product.getAmount())), - new LocalizedMessage.ReplacedRegex(Regex.ITEM_NAME, LanguageUtils.getItemName(product))); + int max = Math.max(lines.length, currentLines.length); - if ((buyPrice <= 0) && (sellPrice > 0)) { - holoText[1] = LanguageUtils.getMessage(LocalizedMessage.Message.HOLOGRAM_SELL, - new LocalizedMessage.ReplacedRegex(Regex.SELL_PRICE, String.valueOf(sellPrice))); - } else if ((buyPrice > 0) && (sellPrice <= 0)) { - holoText[1] = LanguageUtils.getMessage(LocalizedMessage.Message.HOLOGRAM_BUY, - new LocalizedMessage.ReplacedRegex(Regex.BUY_PRICE, String.valueOf(buyPrice))); - } else { - if (length == 2) { - holoText[1] = LanguageUtils.getMessage(LocalizedMessage.Message.HOLOGRAM_BUY_SELL, - new LocalizedMessage.ReplacedRegex(Regex.BUY_PRICE, String.valueOf(buyPrice)), - new LocalizedMessage.ReplacedRegex(Regex.SELL_PRICE, String.valueOf(sellPrice))); + for (int i = 0; i < max; i++) { + if (i < lines.length) { + hologram.setLine(i, lines[i]); } else { - holoText[1] = LanguageUtils.getMessage(LocalizedMessage.Message.HOLOGRAM_BUY, - new LocalizedMessage.ReplacedRegex(Regex.BUY_PRICE, String.valueOf(buyPrice))); - holoText[2] = LanguageUtils.getMessage(LocalizedMessage.Message.HOLOGRAM_SELL, - new LocalizedMessage.ReplacedRegex(Regex.SELL_PRICE, String.valueOf(sellPrice))); + hologram.removeLine(i); } } - - return holoText; } - private Location getHologramLocation(boolean doubleChest, Chest[] chests, String[] holoText) { + private String[] getHologramText() { + List lines = new ArrayList<>(); + + Map requirements = new HashMap<>(); + + requirements.put(HologramFormat.Requirement.VENDOR, getVendor().getName()); + requirements.put(HologramFormat.Requirement.AMOUNT, getProduct().getAmount()); + requirements.put(HologramFormat.Requirement.ITEM_TYPE, getProduct().getType() + (getProduct().getDurability() > 0 ? ":" + getProduct().getDurability() : "")); + requirements.put(HologramFormat.Requirement.ITEM_NAME, getProduct().getItemMeta().getDisplayName()); + requirements.put(HologramFormat.Requirement.HAS_ENCHANTMENT, !LanguageUtils.getEnchantmentString(ItemUtils.getEnchantments(getProduct())).isEmpty()); + requirements.put(HologramFormat.Requirement.BUY_PRICE, getBuyPrice()); + requirements.put(HologramFormat.Requirement.SELL_PRICE, getSellPrice()); + requirements.put(HologramFormat.Requirement.HAS_POTION_EFFECT, ItemUtils.getPotionEffect(getProduct()) != null); + requirements.put(HologramFormat.Requirement.IS_MUSIC_DISC, ItemUtils.isMusicDisc(getProduct())); + requirements.put(HologramFormat.Requirement.IS_POTION_EXTENDED, ItemUtils.isExtendedPotion(getProduct())); + requirements.put(HologramFormat.Requirement.IS_BOOK, ItemUtils.getBookGeneration(getProduct()) != null); + requirements.put(HologramFormat.Requirement.ADMIN_SHOP, getShopType() == ShopType.ADMIN); + requirements.put(HologramFormat.Requirement.NORMAL_SHOP, getShopType() == ShopType.NORMAL); + requirements.put(HologramFormat.Requirement.IN_STOCK, Utils.getAmount(getInventoryHolder().getInventory(), getProduct())); + requirements.put(HologramFormat.Requirement.MAX_STACK, getProduct().getMaxStackSize()); + + int lineCount = plugin.getHologramFormat().getLineCount(); + + for (int i = 0; i < lineCount; i++) { + String format = plugin.getHologramFormat().getFormat(i, requirements); + for (Regex regex : Regex.values()) { + String replace = ""; + + switch (regex) { + case VENDOR: + replace = getVendor().getName(); + break; + case AMOUNT: + replace = String.valueOf(getProduct().getAmount()); + break; + case ITEM_NAME: + replace = LanguageUtils.getItemName(getProduct()); + break; + case ENCHANTMENT: + replace = LanguageUtils.getEnchantmentString(ItemUtils.getEnchantments(getProduct())); + break; + case BUY_PRICE: + replace = plugin.getEconomy().format(getBuyPrice()); + break; + case SELL_PRICE: + replace = plugin.getEconomy().format(getSellPrice()); + break; + case POTION_EFFECT: + replace = LanguageUtils.getPotionEffectName(getProduct()); + break; + case MUSIC_TITLE: + replace = LanguageUtils.getMusicDiscName(getProduct().getType()); + break; + case GENERATION: + CustomBookMeta.Generation gen = ItemUtils.getBookGeneration(getProduct()); + if (gen != null) replace = LanguageUtils.getBookGenerationName(gen); + break; + case STOCK: + replace = String.valueOf(Utils.getAmount(getInventoryHolder().getInventory(), getProduct())); + break; + } + + format = format.replace(regex.getName(), replace); + } + + lines.add(format); + } + + return lines.toArray(new String[lines.size()]); + } + + private Location getHologramLocation(boolean doubleChest, Chest[] chests) { Block b = location.getBlock(); Location holoLocation; @@ -235,14 +298,6 @@ public class Shop { holoLocation.add(0, config.hologram_lift, 0); - if (config.two_line_prices) { - if (holoText.length == 3 && holoText[2] != null) { - holoLocation.add(0, config.two_line_hologram_lift, 0); - } else { - holoLocation.add(0, config.one_line_hologram_lift, 0); - } - } - return holoLocation; } diff --git a/src/main/java/de/epiceric/shopchest/utils/ItemUtils.java b/src/main/java/de/epiceric/shopchest/utils/ItemUtils.java new file mode 100644 index 0000000..da9a10d --- /dev/null +++ b/src/main/java/de/epiceric/shopchest/utils/ItemUtils.java @@ -0,0 +1,71 @@ +package de.epiceric.shopchest.utils; + +import com.google.common.collect.Lists; +import de.epiceric.shopchest.nms.CustomBookMeta; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.PotionMeta; +import org.bukkit.potion.Potion; +import org.bukkit.potion.PotionType; + +import java.util.List; +import java.util.Map; + +public class ItemUtils { + + public static Map getEnchantments(ItemStack itemStack) { + if (itemStack.getItemMeta() instanceof EnchantmentStorageMeta) { + EnchantmentStorageMeta esm = (EnchantmentStorageMeta) itemStack.getItemMeta(); + return esm.getStoredEnchants(); + } else { + return itemStack.getEnchantments(); + } + } + + public static PotionType getPotionEffect(ItemStack itemStack) { + if (itemStack.getItemMeta() instanceof PotionMeta) { + if (Utils.getMajorVersion() < 9) { + return Potion.fromItemStack(itemStack).getType(); + } else { + return ((PotionMeta) itemStack.getItemMeta()).getBasePotionData().getType(); + } + } + + return null; + } + + public static boolean isExtendedPotion(ItemStack itemStack) { + if (itemStack.getItemMeta() instanceof PotionMeta) { + if (Utils.getMajorVersion() >= 9) { + PotionMeta potionMeta = (PotionMeta) itemStack.getItemMeta(); + return potionMeta.getBasePotionData().isExtended(); + } else { + Potion potion = Potion.fromItemStack(itemStack); + return potion.hasExtendedDuration(); + } + } + + return false; + } + + public static boolean isMusicDisc(ItemStack itemStack) { + List musicDiscMaterials = Lists.newArrayList( + Material.GOLD_RECORD, Material.GREEN_RECORD, Material.RECORD_3, Material.RECORD_4, + Material.RECORD_5, Material.RECORD_6, Material.RECORD_7, Material.RECORD_8, + Material.RECORD_9, Material.RECORD_10, Material.RECORD_11, Material.RECORD_12 + ); + + return musicDiscMaterials.contains(itemStack.getType()); + } + + public static CustomBookMeta.Generation getBookGeneration(ItemStack itemStack) { + if (itemStack.getType() == Material.WRITTEN_BOOK) { + return CustomBookMeta.getGeneration(itemStack); + } + + return null; + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3253703..a088e8b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -82,26 +82,10 @@ only-show-shops-in-sight: true # This only has effect if 'only-show-shops-in-sight' is enabled only-show-first-shop-in-sight: true -# Set whether the buy- and sell price should be arranged below each other. -# The first line will be the buy price with the message -# "message.hologram.only-buy", the second line will be the sell price -# with the message "message.hologram.only-sell". -# If buying or selling is disabled, a line for that price will not be created. -# (The messages can be found and modified in the specified language file) -two-line-prices: false - -# Set the amount (may be negative) a hologram should be lifted in the y-axis, -# when "two-line-prices" is set to true and buying and selling is enabled -# at the shop. -# The higher the number, the higher will the hologram be. -# A value of '1' equals to one block, and a value of '0.25' is equal to the -# height of one line. -two-line-hologram-lift: 0.25 - -# Set the amount (may be negative) a hologram should be lifted in the y-axis, -# when "two-line-prices" is set to true and only buying or selling is -# enabled at the shop. -one-line-hologram-lift: 0 +# Set whether the hologram's location should be fixed at the bottom, +# so when it gets more lines, it won't interfere with the item or chest, +# but goes higher. +hologram-fixed-bottom: true # Set the amount (may be negative) a hologram should be lifted in the y-axis. # If the hologram lift is already increased by the values above, diff --git a/src/main/resources/hologram-format.yml b/src/main/resources/hologram-format.yml new file mode 100644 index 0000000..9a77384 --- /dev/null +++ b/src/main/resources/hologram-format.yml @@ -0,0 +1,71 @@ +# =============================================== +# === ShopChest's hologram configuration file === +# =============================================== +# +# Valid requirements are: +# VENDOR, AMOUNT, ITEM_TYPE, ITEM_NAME, HAS_ENCHANTMENT, BUY_PRICE, +# SELL_PRICE, HAS_POTION_EFFECT, IS_MUSIC_DISC, IS_POTION_EXTENDED, +# IS_BOOK, ADMIN_SHOP, NORMAL_SHOP, IN_STOCK, MAX_STACK +# +# You can also use the requirements for conditions. +# ITEM_TYPE will return the type of the item (-> item_names.txt), +# ITEM_NAME can be compared against a custom named item's name (may be null). +# +# Examples: +# - IN_STOCK > 0 +# - VENDOR == "EpicEric" +# - BUY_PRICE <= SELL_PRICE +# - ITEM_TYPE == "STONE:2" +# - ITEM_TYPE != "IRON_INGOT" +# - ITEM_NAME == "The Mighty Sword" +# - (AMOUNT > 10) && (AMOUNT <= 20) +# - (IN_STOCK > 0) || ADMIN_SHOP +# +# Valid placeholders are: +# %VENDOR%, %AMOUNT%, %ITEM-NAME%, %ENCHANTMENT%, %BUY-PRICE%, +# %SELL-PRICE%, %POTION-EFFECT%, %MUSIC-TITLE%, %GENERATION%, +# %STOCK%, %MAX-STACK% +# +# Other information: +# - Options can be called however you want. +# - Color codes can be used in the format. +# - Options are checked from top to bottom; the first to +# fulfill the requirements will be taken. +# - Lines start with 0. + +lines: + 0: + options: + normal-shop: + format: "%VENDOR%" + requirements: + - NORMAL_SHOP + + admin-shop: + format: "&cAdmin Shop" + requirements: + - ADMIN_SHOP + + 1: + options: + default: + format: "%AMOUNT% x %ITEMNAME%" + requirements: + + 2: + options: + buy-and-sell: + format: "Buy %BUY-PRICE% | %SELL-PRICE% Sell" + requirements: + - BUY_PRICE > 0 + - SELL_PRICE > 0 + + only-buy: + format: "Buy %BUY-PRICE%" + requirements: + - BUY_PRICE > 0 + + only-sell: + format: "Sell %SELL-PRICE%" + requirements: + - SELL_PRICE > 0 \ No newline at end of file diff --git a/src/main/resources/lang/de_DE.lang b/src/main/resources/lang/de_DE.lang index 8407ad4..6b2c327 100644 --- a/src/main/resources/lang/de_DE.lang +++ b/src/main/resources/lang/de_DE.lang @@ -7,7 +7,7 @@ message.chest-no-shop=&cTruhe ist kein Shop. message.shop-create-not-enough-money=&cNicht genug Geld. Du brauchst &6%CREATION-PRICE% &cum einen Shop zu erstellen. message.shopInfo.vendor=&6Verkäufer: &e%VENDOR% message.shopInfo.product=&6Produkt: &e%AMOUNT% x %ITEMNAME% -message.shopInfo.stock=&6Auf Lager: &e%AMOUNT% +message.shopInfo.stock=&6Auf Lager: &e%STOCK% message.shopInfo.enchantments=&6Verzauberungen: &e%ENCHANTMENT% message.shopInfo.potion-effect=&6Trank-Effekte: &e%POTION-EFFECT% %EXTENDED% message.shopInfo.music-disc-title=&6Schallplattentitel: &e%MUSIC-TITLE% @@ -60,10 +60,6 @@ message.update.click-to-download=Klicke hier zum Herunterladen message.update.no-update=&6&lKeine neue Aktualisierung verfügbar. message.update.checking=&6&lSuche nach Aktualisierungen... message.update.error=&c&lFehler beim Suchen nach Aktualisierungen. -message.hologram.format=%AMOUNT% * %ITEMNAME% -message.hologram.buy-and-sell=Kauf %BUY-PRICE% | %SELL-PRICE% Verkauf -message.hologram.only-buy=Kauf %BUY-PRICE% -message.hologram.only-sell=Verkauf %SELL-PRICE% message.noPermission.create=&cDu hast keine Berechtigung einen Shop zu erstellen. message.noPermission.create-admin=&cDu hast keine Berechtigung einen Admin-Shop zu erstellen. message.noPermission.create-protected=&cDu hast keine Berechtigung hier einen Shop zu erstellen. diff --git a/src/main/resources/lang/en_US.lang b/src/main/resources/lang/en_US.lang index 5f5ec33..7fbd370 100644 --- a/src/main/resources/lang/en_US.lang +++ b/src/main/resources/lang/en_US.lang @@ -30,8 +30,8 @@ message.shopInfo.vendor=&6Vendor: &e%VENDOR% message.shopInfo.product=&6Product: &e%AMOUNT% x %ITEMNAME% # Set the in-stock message the player after entering '/shop info'. -# Usable Placeholders=%AMOUNT% -message.shopInfo.stock=&6In Stock: &e%AMOUNT% +# Usable Placeholders=%STOCK% +message.shopInfo.stock=&6In Stock: &e%STOCK% # Set the enchantments message the player gets after entering '/shop info' if the product is enchanted # Usable Placeholders: %ENCHANTMENT% @@ -215,22 +215,6 @@ message.update.checking=&6&lChecking for updates... # Set the message when an error occurs while checking for updates. message.update.error=&c&lError while checking for updates. -# Set the text in the first row of the shop's hologram -# Usable Placeholders: %ITEMNAME%, %AMOUNT% -message.hologram.format=%AMOUNT% * %ITEMNAME% - -# Set the text in the second row of the shop's hologram when the player can buy and sell an item. -# Usable Placeholders: %BUY-PRICE%, %SELL-PRICE% -message.hologram.buy-and-sell=Buy %BUY-PRICE% | %SELL-PRICE% Sell - -# Set the text in the second row of the shop's hologram when the player can only buy an item. -# Usable Placeholders: %BUY-PRICE% -message.hologram.only-buy=Buy %BUY-PRICE% - -# Set the text in the second row of the shop's hologram when the player can only sell an item. -# Usable Placeholders: %SELL-PRICE% -message.hologram.only-sell=Sell %SELL-PRICE% - # Set the message when a not permitted player tries to create a shop. message.noPermission.create=&cYou don't have permission to create a shop.