diff --git a/pom.xml b/pom.xml index 790fdd6..211c45a 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ ${jdkVersion} - 1.12.3 + 1.12.4 ${version.number}-${version.git} diff --git a/src/main/java/de/epiceric/shopchest/ShopChest.java b/src/main/java/de/epiceric/shopchest/ShopChest.java index 2a1b9fa..0f65aa2 100644 --- a/src/main/java/de/epiceric/shopchest/ShopChest.java +++ b/src/main/java/de/epiceric/shopchest/ShopChest.java @@ -204,7 +204,7 @@ public class ShopChest extends JavaPlugin { } if (database != null) { - for (Shop shop : shopUtils.getShops()) { + for (Shop shop : shopUtils.getShopsCopy()) { shopUtils.removeShop(shop, false); debug("Removed shop (#" + shop.getID() + ")"); } diff --git a/src/main/java/de/epiceric/shopchest/config/HologramFormat.java b/src/main/java/de/epiceric/shopchest/config/HologramFormat.java index cf63ee6..ad8ac1f 100644 --- a/src/main/java/de/epiceric/shopchest/config/HologramFormat.java +++ b/src/main/java/de/epiceric/shopchest/config/HologramFormat.java @@ -1,6 +1,7 @@ package de.epiceric.shopchest.config; import de.epiceric.shopchest.ShopChest; +import de.epiceric.shopchest.utils.Operator; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; @@ -21,6 +22,13 @@ public class HologramFormat { NORMAL_SHOP, IN_STOCK, MAX_STACK, CHEST_SPACE, DURABILITY } + // no "-" sign since no variable can be negative + // e.g.: 100.0 >= 50.0 + private static final Pattern SIMPLE_NUMERIC_CONDITION = Pattern.compile("^(\\d+(?:\\.\\d+)?) ([<>][=]?|[=!]=) (\\d+(?:\\.\\d+)?)$"); + + // e.g.: "STONE" == "DIAMOND_SWORD" + private static final Pattern SIMPLE_STRING_CONDITION = Pattern.compile("^\"([^\"]*)\" ([=!]=) \"([^\"]*)\"$"); + private ShopChest plugin; private File configFile; private YamlConfiguration config; @@ -110,32 +118,78 @@ public class HologramFormat { * @return Result of the condition */ public boolean evalRequirement(String condition, Map values) { - try { - ScriptEngineManager manager = new ScriptEngineManager(); - ScriptEngine engine = manager.getEngineByName("JavaScript"); + String cond = condition; - String cond = condition; + for (HologramFormat.Requirement req : HologramFormat.Requirement.values()) { + if (cond.contains(req.toString()) && values.containsKey(req)) { + Object val = values.get(req); + String sVal = String.valueOf(val); - for (HologramFormat.Requirement req : HologramFormat.Requirement.values()) { - if (cond.contains(req.toString()) && values.containsKey(req)) { - Object val = values.get(req); - String sVal = String.valueOf(val); + if (val instanceof String && !(sVal.startsWith("\"") && sVal.endsWith("\""))) { + sVal = String.format("\"%s\"", sVal); + } - if (val instanceof String && !(sVal.startsWith("\"") && sVal.endsWith("\""))) { - sVal = String.format("\"%s\"", sVal); + cond = cond.replace(req.toString(), sVal); + } + } + + if (cond.equals("true")) { + // e.g.: ADMIN_SHOP + return true; + } else if (cond.equals("false")) { + return false; + } else { + char firstChar = cond.charAt(0); + + // numeric cond: first char must be a digit (no variable can be negative) + if (firstChar >= '0' && firstChar <= '9') { + Matcher matcher = SIMPLE_NUMERIC_CONDITION.matcher(cond); + + if (matcher.find()) { + Double a, b; + Operator operator; + try { + a = Double.valueOf(matcher.group(1)); + operator = Operator.from(matcher.group(2)); + b = Double.valueOf(matcher.group(3)); + + return operator.compare(a, b); + } catch (IllegalArgumentException ignored) { + // should not happen, since regex checked that there is valid number and valid operator } - - cond = cond.replace(req.toString(), sVal); } } - return (boolean) engine.eval(cond); - } catch (ScriptException e) { - plugin.debug("Failed to eval condition: " + condition); - plugin.debug(e); - } + // string cond: first char must be a: " + if (firstChar == '"') { + Matcher matcher = SIMPLE_STRING_CONDITION.matcher(cond); - return false; + if (matcher.find()) { + String a, b; + Operator operator; + try { + a = matcher.group(1); + operator = Operator.from(matcher.group(2)); + b = matcher.group(3); + + return operator.compare(a, b); + } catch (IllegalArgumentException | UnsupportedOperationException ignored) { + // should not happen, since regex checked that there is valid operator + } + } + } + + // complex comparison + try { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("JavaScript"); + return (boolean) engine.eval(cond); + } catch (ScriptException e) { + plugin.debug("Failed to eval condition: " + condition); + plugin.debug(e); + return false; + } + } } /** diff --git a/src/main/java/de/epiceric/shopchest/listeners/ShopInteractListener.java b/src/main/java/de/epiceric/shopchest/listeners/ShopInteractListener.java index 967bb74..446dd67 100644 --- a/src/main/java/de/epiceric/shopchest/listeners/ShopInteractListener.java +++ b/src/main/java/de/epiceric/shopchest/listeners/ShopInteractListener.java @@ -691,17 +691,9 @@ public class ShopInteractListener implements Listener { executor.sendMessage(LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_CREATED, placeholder)); } - for (Player p : location.getWorld().getPlayers()) { - if (p.getLocation().distanceSquared(location) <= Math.pow(config.maximal_distance, 2)) { - if (shop.getHologram() != null) { - shop.getHologram().showPlayer(p); - } - } - if (p.getLocation().distanceSquared(location) <= Math.pow(config.maximal_item_distance, 2)) { - if (shop.getItem() != null) { - shop.getItem().setVisible(p, true); - } - } + // next update will display the new shop + for (Player player : location.getWorld().getPlayers()) { + plugin.getShopUtils().resetPlayerLocation(player); } } diff --git a/src/main/java/de/epiceric/shopchest/listeners/ShopItemListener.java b/src/main/java/de/epiceric/shopchest/listeners/ShopItemListener.java index 016bc1f..154bac8 100644 --- a/src/main/java/de/epiceric/shopchest/listeners/ShopItemListener.java +++ b/src/main/java/de/epiceric/shopchest/listeners/ShopItemListener.java @@ -27,18 +27,6 @@ public class ShopItemListener implements Listener { this.shopUtils = plugin.getShopUtils(); } - @EventHandler - public void onPlayerLeave(PlayerQuitEvent e) { - for (Shop shop : plugin.getShopUtils().getShops()) { - if (shop.getItem() != null) { - shop.getItem().setVisible(e.getPlayer(), false); - } - if (shop.getHologram() != null) { - shop.getHologram().hidePlayer(e.getPlayer()); - } - } - } - @EventHandler(priority = EventPriority.HIGH) public void onBlockPlace(BlockPlaceEvent e) { Block b = e.getBlockPlaced(); diff --git a/src/main/java/de/epiceric/shopchest/listeners/ShopUpdateListener.java b/src/main/java/de/epiceric/shopchest/listeners/ShopUpdateListener.java index 063f7b9..28599fa 100644 --- a/src/main/java/de/epiceric/shopchest/listeners/ShopUpdateListener.java +++ b/src/main/java/de/epiceric/shopchest/listeners/ShopUpdateListener.java @@ -8,6 +8,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.scheduler.BukkitRunnable; @@ -20,6 +21,20 @@ public class ShopUpdateListener implements Listener { this.plugin = plugin; } + @EventHandler + public void onPlayerLeave(PlayerQuitEvent e) { + for (Shop shop : plugin.getShopUtils().getShops()) { + if (shop.hasItem()) { + shop.getItem().resetVisible(e.getPlayer()); + } + if (shop.hasHologram()) { + shop.getHologram().resetVisible(e.getPlayer()); + } + } + + plugin.getShopUtils().resetPlayerLocation(e.getPlayer()); + } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPlayerTeleport(PlayerTeleportEvent e) { Location from = e.getFrom(); @@ -28,7 +43,7 @@ public class ShopUpdateListener implements Listener { // Wait till the chunk should have loaded on the client // Update IF worlds are different OR chunks are different (as many teleports are in same chunk) - if (!from.getWorld().equals(to.getWorld()) + if (!from.getWorld().getName().equals(to.getWorld().getName()) || from.getChunk().getX() != to.getChunk().getX() || from.getChunk().getZ() != to.getChunk().getZ()) { // Wait for 15 ticks before we actually put it in the queue @@ -40,13 +55,15 @@ public class ShopUpdateListener implements Listener { public void run() { if (p.isOnline()) { for (Shop shop : plugin.getShopUtils().getShops()) { - if (shop.getItem() != null) { - shop.getItem().setVisible(p, false); + if (shop.hasItem()) { + shop.getItem().hidePlayer(p); } - if (shop.getHologram() != null) { + if (shop.hasHologram()) { shop.getHologram().hidePlayer(p); } } + // so next update will update correctly + plugin.getShopUtils().resetPlayerLocation(p); } } }); diff --git a/src/main/java/de/epiceric/shopchest/nms/ArmorStandWrapper.java b/src/main/java/de/epiceric/shopchest/nms/ArmorStandWrapper.java index ec5fe7d..d76f596 100644 --- a/src/main/java/de/epiceric/shopchest/nms/ArmorStandWrapper.java +++ b/src/main/java/de/epiceric/shopchest/nms/ArmorStandWrapper.java @@ -32,7 +32,7 @@ public class ArmorStandWrapper { private UUID uuid; private int entityId; - public ArmorStandWrapper(ShopChest plugin, Location location, String customName) { + public ArmorStandWrapper(ShopChest plugin, Location location, String customName, boolean interactable) { this.plugin = plugin; this.location = location; this.customName = customName; @@ -58,9 +58,12 @@ public class ArmorStandWrapper { entityArmorStandClass.getMethod("setInvisible", boolean.class).invoke(entity, 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(nmsWorld), entity); + // It will also automatically load/unload it when far away + if (interactable) { + Method addEntityMethod = worldServerClass.getDeclaredMethod((Utils.getMajorVersion() == 8 ? "a" : "b"), entityClass); + addEntityMethod.setAccessible(true); + addEntityMethod.invoke(worldServerClass.cast(nmsWorld), entity); + } uuid = (UUID) entityClass.getMethod("getUniqueID").invoke(entity); entityId = (int) entityArmorStandClass.getMethod("getId").invoke(entity); diff --git a/src/main/java/de/epiceric/shopchest/nms/Hologram.java b/src/main/java/de/epiceric/shopchest/nms/Hologram.java index a4dd3c0..04b831e 100644 --- a/src/main/java/de/epiceric/shopchest/nms/Hologram.java +++ b/src/main/java/de/epiceric/shopchest/nms/Hologram.java @@ -2,27 +2,49 @@ package de.epiceric.shopchest.nms; import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.config.Config; -import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class Hologram { - private static List holograms = new ArrayList<>(); + private static final List HOLOGRAMS = new ArrayList<>(); - private final Set visibility = Collections.newSetFromMap(new ConcurrentHashMap()); + /** + * @param armorStand Armor stand that's part of a hologram + * @return Hologram, the armor stand is part of + */ + public static Hologram getHologram(ArmorStand armorStand) { + for (Hologram hologram : HOLOGRAMS) { + if (hologram.contains(armorStand)) { + return hologram; + } + } + + return null; + } + + /** + * @param armorStand Armor stand to check + * @return Whether the armor stand is part of a hologram + */ + public static boolean isPartOfHologram(ArmorStand armorStand) { + return getHologram(armorStand) != null; + } + + // concurrent since update task is in async thread + // since this is a fake entity, hologram is hidden per default + private final Set viewers = Collections.newSetFromMap(new ConcurrentHashMap()); private final List wrappers = new ArrayList<>(); private final Location location; private final ShopChest plugin; private final Config config; - private boolean exists = false; + private boolean exists; private ArmorStandWrapper interactArmorStandWrapper; public Hologram(ShopChest plugin, String[] lines, Location location) { @@ -30,98 +52,6 @@ public class Hologram { this.config = plugin.getShopChestConfig(); this.location = location; - create(lines); - } - - public void addLine(int line, String text) { - addLine(line, text, false); - } - - private void addLine(int line, String text, boolean forceUpdateLine) { - if (text == null || text.isEmpty()) return; - - if (line >= wrappers.size()) { - line = wrappers.size(); - } - - text = ChatColor.translateAlternateColorCodes('&', text); - - if (config.hologram_fixed_bottom) { - for (int i = 0; i < line; i++) { - ArmorStandWrapper wrapper = wrappers.get(i); - wrapper.setLocation(wrapper.getLocation().add(0, 0.25, 0)); - } - } else { - for (int i = line; i < wrappers.size(); i++) { - ArmorStandWrapper wrapper = wrappers.get(i); - wrapper.setLocation(wrapper.getLocation().subtract(0, 0.25, 0)); - } - } - - Location loc = getLocation(); - - if (!config.hologram_fixed_bottom) { - loc.subtract(0, line * 0.25, 0); - } - - ArmorStandWrapper wrapper = new ArmorStandWrapper(plugin, loc, text); - wrappers.add(line, wrapper); - - if (forceUpdateLine) { - for (UUID uuid : visibility) { - Player player = Bukkit.getPlayer(uuid); - if (player != null) { - wrapper.setVisible(player, true); - } - } - } - } - - public void setLine(int line, String text) { - if (text == null ||text.isEmpty()) { - removeLine(line); - return; - } - - text = ChatColor.translateAlternateColorCodes('&', text); - - if (line >= wrappers.size()) { - addLine(line, text, true); - return; - } - - wrappers.get(line).setCustomName(text); - } - - public void removeLine(int line) { - if (line < wrappers.size()) { - if (config.hologram_fixed_bottom) { - for (int i = 0; i < line; i++) { - ArmorStandWrapper wrapper = wrappers.get(i); - wrapper.setLocation(wrapper.getLocation().subtract(0, 0.25, 0)); - } - } else { - for (int i = line + 1; i < wrappers.size(); i++) { - ArmorStandWrapper wrapper = wrappers.get(i); - wrapper.setLocation(wrapper.getLocation().add(0, 0.25, 0)); - } - } - - wrappers.get(line).remove(); - wrappers.remove(line); - } - } - - public String[] getLines() { - List lines = new ArrayList<>(); - for (ArmorStandWrapper wrapper : wrappers) { - lines.add(wrapper.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]); } @@ -131,15 +61,11 @@ public class Hologram { if (config.hologram_fixed_bottom) y = 0.85; Location loc = getLocation().add(0, y, 0); - interactArmorStandWrapper = new ArmorStandWrapper(plugin, loc, null); + interactArmorStandWrapper = new ArmorStandWrapper(plugin, loc, null, true); } - for (Player player : location.getWorld().getPlayers()) { - plugin.getShopUtils().updateShops(player, true); - } - - holograms.add(this); - exists = true; + this.exists = true; + HOLOGRAMS.add(this); } /** @@ -149,58 +75,6 @@ public class Hologram { return location.clone(); } - /** - * @param p Player to which the hologram should be shown - */ - public void showPlayer(final Player p) { - if (!isVisible(p)) { - new BukkitRunnable() { - @Override - public void run() { - for (ArmorStandWrapper wrapper : wrappers) { - wrapper.setVisible(p, true); - } - - if (interactArmorStandWrapper != null) { - interactArmorStandWrapper.setVisible(p, true); - } - } - }.runTaskAsynchronously(plugin); - - visibility.add(p.getUniqueId()); - } - } - - /** - * @param p Player from which the hologram should be hidden - */ - public void hidePlayer(final Player p) { - if (isVisible(p)) { - new BukkitRunnable() { - @Override - public void run() { - for (ArmorStandWrapper wrapper : wrappers) { - wrapper.setVisible(p, false); - } - - if (interactArmorStandWrapper != null) { - interactArmorStandWrapper.setVisible(p, false); - } - } - }.runTaskAsynchronously(plugin); - - visibility.remove(p.getUniqueId()); - } - } - - /** - * @param p Player to check - * @return Whether the hologram is visible to the player - */ - public boolean isVisible(Player p) { - return visibility.contains(p.getUniqueId()); - } - /** * @return Whether the hologram exists and is not dead */ @@ -231,45 +105,189 @@ public class Hologram { return interactArmorStandWrapper; } + /** + * @param p Player to check + * @return Whether the hologram is visible to the player + */ + public boolean isVisible(Player p) { + return viewers.contains(p.getUniqueId()); + } + + /** + * @param p Player to which the hologram should be shown + */ + public void showPlayer(Player p) { + showPlayer(p, false); + } + + /** + * @param p Player to which the hologram should be shown + * @param force whether to force or not + */ + public void showPlayer(Player p, boolean force) { + if (viewers.add(p.getUniqueId()) || force) { + togglePlayer(p, true); + } + } + + /** + * @param p Player from which the hologram should be hidden + */ + public void hidePlayer(Player p) { + hidePlayer(p, false); + } + + /** + * @param p Player from which the hologram should be hidden + * @param force whether to force or not + */ + public void hidePlayer(Player p, boolean force) { + if (viewers.remove(p.getUniqueId()) || force) { + togglePlayer(p, false); + } + } + /** * Removes the hologram.
* Hologram will be hidden from all players and will be killed */ public void remove() { + viewers.clear(); + for (ArmorStandWrapper wrapper : wrappers) { wrapper.remove(); } + wrappers.clear(); if (interactArmorStandWrapper != null) { interactArmorStandWrapper.remove(); } - - wrappers.clear(); + interactArmorStandWrapper = null; exists = false; - holograms.remove(this); + HOLOGRAMS.remove(this); + } + + public void resetVisible(Player p) { + viewers.remove(p.getUniqueId()); + } + + private void togglePlayer(Player p, boolean visible) { + for (ArmorStandWrapper wrapper : wrappers) { + wrapper.setVisible(p, visible); + } + + if (interactArmorStandWrapper != null) { + interactArmorStandWrapper.setVisible(p, visible); + } } /** - * @param armorStand Armor stand that's part of a hologram - * @return Hologram, the armor stand is part of + * Get all hologram lines + * + * @return Hologram lines */ - public static Hologram getHologram(ArmorStand armorStand) { - for (Hologram hologram : holograms) { - if (hologram.contains(armorStand)) { - return hologram; + public String[] getLines() { + List lines = new ArrayList<>(); + for (ArmorStandWrapper wrapper : wrappers) { + lines.add(wrapper.getCustomName()); + } + + return lines.toArray(new String[lines.size()]); + } + + /** + * Add a line + * + * @param line where to insert + * @param text text to display + */ + public void addLine(int line, String text) { + addLine(line, text, false); + } + + private void addLine(int line, String text, boolean forceUpdateLine) { + if (text == null || text.isEmpty()) return; + + if (line >= wrappers.size()) { + line = wrappers.size(); + } + + text = ChatColor.translateAlternateColorCodes('&', text); + + if (config.hologram_fixed_bottom) { + for (int i = 0; i < line; i++) { + ArmorStandWrapper wrapper = wrappers.get(i); + wrapper.setLocation(wrapper.getLocation().add(0, 0.25, 0)); + } + } else { + for (int i = line; i < wrappers.size(); i++) { + ArmorStandWrapper wrapper = wrappers.get(i); + wrapper.setLocation(wrapper.getLocation().subtract(0, 0.25, 0)); } } - return null; + Location loc = getLocation(); + + if (!config.hologram_fixed_bottom) { + loc.subtract(0, line * 0.25, 0); + } + + ArmorStandWrapper wrapper = new ArmorStandWrapper(plugin, loc, text, false); + wrappers.add(line, wrapper); + + if (forceUpdateLine) { + for (Player player : location.getWorld().getPlayers()) { + if (viewers.contains(player.getUniqueId())) { + wrapper.setVisible(player, true); + } + } + } } /** - * @param armorStand Armor stand to check - * @return Whether the armor stand is part of a hologram + * Set a line + * + * @param line index to change + * @param text text to display */ - public static boolean isPartOfHologram(ArmorStand armorStand) { - return getHologram(armorStand) != null; + public void setLine(int line, String text) { + if (text == null ||text.isEmpty()) { + removeLine(line); + return; + } + + text = ChatColor.translateAlternateColorCodes('&', text); + + if (line >= wrappers.size()) { + addLine(line, text, true); + return; + } + + wrappers.get(line).setCustomName(text); } + /** + * Remove a line + * + * @param line index to remove + */ + public void removeLine(int line) { + if (line < wrappers.size()) { + if (config.hologram_fixed_bottom) { + for (int i = 0; i < line; i++) { + ArmorStandWrapper wrapper = wrappers.get(i); + wrapper.setLocation(wrapper.getLocation().subtract(0, 0.25, 0)); + } + } else { + for (int i = line + 1; i < wrappers.size(); i++) { + ArmorStandWrapper wrapper = wrappers.get(i); + wrapper.setLocation(wrapper.getLocation().add(0, 0.25, 0)); + } + } + + wrappers.get(line).remove(); + wrappers.remove(line); + } + } } diff --git a/src/main/java/de/epiceric/shopchest/shop/Shop.java b/src/main/java/de/epiceric/shopchest/shop/Shop.java index 2a4d438..e75ddfa 100644 --- a/src/main/java/de/epiceric/shopchest/shop/Shop.java +++ b/src/main/java/de/epiceric/shopchest/shop/Shop.java @@ -10,7 +10,6 @@ import de.epiceric.shopchest.language.LanguageUtils; 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; import org.bukkit.OfflinePlayer; @@ -19,29 +18,31 @@ import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.Chest; import org.bukkit.block.DoubleChest; -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; +import java.util.*; public class Shop { + public enum ShopType { + NORMAL, + ADMIN, + } + + private final ShopChest plugin; + private final OfflinePlayer vendor; + private final ItemStack product; + private final Location location; + private final double buyPrice; + private final double sellPrice; + private final ShopType shopType; + private final Config config; + private boolean created; private int id; - private ShopChest plugin; - private OfflinePlayer vendor; - private ItemStack product; - private Location location; private Hologram hologram; private ShopItem item; - private double buyPrice; - private double sellPrice; - private ShopType shopType; - private Config config; public Shop(int id, ShopChest plugin, OfflinePlayer vendor, ItemStack product, Location location, double buyPrice, double sellPrice, ShopType shopType) { this.id = id; @@ -59,6 +60,34 @@ public class Shop { this(-1, plugin, vendor, product, location, buyPrice, sellPrice, shopType); } + /** + * Test if this shop is equals to another + * + * @param o Other object to test against + * @return true if we are sure they are the same, false otherwise + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Shop shop = (Shop) o; + + // id = -1 means temp shop + return id != -1 && id == shop.id; + } + + @Override + public int hashCode() { + return id != -1 ? id : super.hashCode(); + } + + /** + * Create the shop + * + * @param showConsoleMessages to log exceptions to console + * @return Whether is was created or not + */ public boolean create(boolean showConsoleMessages) { if (created) return false; @@ -123,11 +152,7 @@ public class Shop { itemStack = product.clone(); itemStack.setAmount(1); - this.item = new ShopItem(plugin, itemStack, itemLocation); - - for (Player p : Bukkit.getOnlinePlayers()) { - item.setVisible(p, true); - } + item = new ShopItem(plugin, itemStack, itemLocation); } } @@ -163,6 +188,9 @@ public class Shop { hologram = new Hologram(plugin, holoText, holoLocation); } + /** + * Keep hologram text up to date + */ public void updateHologramText() { String[] lines = getHologramText(); String[] currentLines = hologram.getLines(); @@ -181,7 +209,7 @@ public class Shop { private String[] getHologramText() { List lines = new ArrayList<>(); - Map requirements = new HashMap<>(); + Map requirements = new EnumMap<>(HologramFormat.Requirement.class); 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() : "")); @@ -200,7 +228,7 @@ public class Shop { requirements.put(HologramFormat.Requirement.CHEST_SPACE, Utils.getFreeSpaceForItem(getInventoryHolder().getInventory(), getProduct())); requirements.put(HologramFormat.Requirement.DURABILITY, getProduct().getDurability()); - Map placeholders = new HashMap<>(); + Map placeholders = new EnumMap<>(Placeholder.class); placeholders.put(Placeholder.VENDOR, getVendor().getName()); placeholders.put(Placeholder.AMOUNT, getProduct().getAmount()); placeholders.put(Placeholder.ITEM_NAME, LanguageUtils.getItemName(getProduct())); @@ -220,7 +248,7 @@ public class Shop { for (int i = 0; i < lineCount; i++) { String format = plugin.getHologramFormat().getFormat(i, requirements, placeholders); for (Placeholder placeholder : placeholders.keySet()) { - String replace = ""; + String replace; switch (placeholder) { case BUY_PRICE: @@ -241,7 +269,7 @@ public class Shop { } } - return lines.toArray(new String[lines.size()]); + return lines.toArray(new String[0]); } private Location getHologramLocation(boolean doubleChest, Chest[] chests) { @@ -374,6 +402,14 @@ public class Shop { return item; } + public boolean hasHologram() { + return hologram != null; + } + + public boolean hasItem() { + return item != null; + } + /** * @return {@link InventoryHolder} of the shop or null if the shop has no chest. */ @@ -388,9 +424,4 @@ public class Shop { return null; } - public enum ShopType { - NORMAL, - ADMIN - } - } diff --git a/src/main/java/de/epiceric/shopchest/shop/ShopItem.java b/src/main/java/de/epiceric/shopchest/shop/ShopItem.java index add75dc..e1c06c1 100644 --- a/src/main/java/de/epiceric/shopchest/shop/ShopItem.java +++ b/src/main/java/de/epiceric/shopchest/shop/ShopItem.java @@ -10,39 +10,40 @@ import org.bukkit.inventory.ItemStack; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; -import java.util.Collections; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class ShopItem { private final ShopChest plugin; - private final Set visibility = Collections.newSetFromMap(new ConcurrentHashMap()); + + // concurrent since update task is in async thread + // since this is a fake entity, item is hidden per default + private final Set viewers = Collections.newSetFromMap(new ConcurrentHashMap()); private final ItemStack itemStack; private final Location location; - private Object entityItem; - private int entityId; - private Object[] creationPackets = new Object[3]; - - private Class nmsWorldClass = Utils.getNMSClass("World"); - private Class craftWorldClass = Utils.getCraftClass("CraftWorld"); - private Class nmsItemStackClass = Utils.getNMSClass("ItemStack"); - private Class craftItemStackClass = Utils.getCraftClass("inventory.CraftItemStack"); - private Class entityItemClass = Utils.getNMSClass("EntityItem"); - private Class entityClass = Utils.getNMSClass("Entity"); - private Class packetPlayOutSpawnEntityClass = Utils.getNMSClass("PacketPlayOutSpawnEntity"); - private Class packetPlayOutEntityMetadataClass = Utils.getNMSClass("PacketPlayOutEntityMetadata"); - private Class dataWatcherClass = Utils.getNMSClass("DataWatcher"); - private Class packetPlayOutEntityDestroyClass = Utils.getNMSClass("PacketPlayOutEntityDestroy"); - private Class packetPlayOutEntityVelocityClass = Utils.getNMSClass("PacketPlayOutEntityVelocity"); + private final Class packetPlayOutEntityDestroyClass = Utils.getNMSClass("PacketPlayOutEntityDestroy"); + private final Object[] creationPackets = new Object[3]; + private final Object entityItem; + private final int entityId; public ShopItem(ShopChest plugin, ItemStack itemStack, Location location) { this.plugin = plugin; this.itemStack = itemStack; this.location = location; + Class packetPlayOutEntityVelocityClass = Utils.getNMSClass("PacketPlayOutEntityVelocity"); + Class dataWatcherClass = Utils.getNMSClass("DataWatcher"); + Class packetPlayOutEntityMetadataClass = Utils.getNMSClass("PacketPlayOutEntityMetadata"); + Class packetPlayOutSpawnEntityClass = Utils.getNMSClass("PacketPlayOutSpawnEntity"); + Class entityClass = Utils.getNMSClass("Entity"); + Class entityItemClass = Utils.getNMSClass("EntityItem"); + Class craftItemStackClass = Utils.getCraftClass("inventory.CraftItemStack"); + Class nmsItemStackClass = Utils.getNMSClass("ItemStack"); + Class craftWorldClass = Utils.getCraftClass("CraftWorld"); + Class nmsWorldClass = Utils.getNMSClass("World"); + Class[] requiredClasses = new Class[] { nmsWorldClass, craftWorldClass, nmsItemStackClass, craftItemStackClass, entityItemClass, packetPlayOutSpawnEntityClass, packetPlayOutEntityMetadataClass, dataWatcherClass, @@ -52,14 +53,15 @@ public class ShopItem { for (Class c : requiredClasses) { if (c == null) { plugin.debug("Failed to create shop item: Could not find all required classes"); + entityItem = null; + entityId = -1; return; } } - create(); - } + Object tmpEntityItem = null; + int tmpEntityId = -1; - private void create() { try { Object craftWorld = craftWorldClass.cast(location.getWorld()); Object nmsWorld = craftWorldClass.getMethod("getHandle").invoke(craftWorld); @@ -67,74 +69,32 @@ public class ShopItem { Object nmsItemStack = craftItemStackClass.getMethod("asNMSCopy", ItemStack.class).invoke(null, itemStack); Constructor entityItemConstructor = entityItemClass.getConstructor(nmsWorldClass); - entityItem = entityItemConstructor.newInstance(nmsWorld); + tmpEntityItem = entityItemConstructor.newInstance(nmsWorld); - entityItemClass.getMethod("setPosition", double.class, double.class, double.class).invoke(entityItem, location.getX(), location.getY(), location.getZ()); - entityItemClass.getMethod("setItemStack", nmsItemStackClass).invoke(entityItem, nmsItemStack); - if (Utils.getMajorVersion() >= 10) entityItemClass.getMethod("setNoGravity", boolean.class).invoke(entityItem, true); + entityItemClass.getMethod("setPosition", double.class, double.class, double.class).invoke(tmpEntityItem, location.getX(), location.getY(), location.getZ()); + entityItemClass.getMethod("setItemStack", nmsItemStackClass).invoke(tmpEntityItem, nmsItemStack); + if (Utils.getMajorVersion() >= 10) entityItemClass.getMethod("setNoGravity", boolean.class).invoke(tmpEntityItem, true); Field ageField = entityItemClass.getDeclaredField("age"); ageField.setAccessible(true); - ageField.setInt(entityItem, -32768); + ageField.setInt(tmpEntityItem, -32768); - entityId = (int) entityItemClass.getMethod("getId").invoke(entityItem); - Object dataWatcher = entityItemClass.getMethod("getDataWatcher").invoke(entityItem); + tmpEntityId = (int) entityItemClass.getMethod("getId").invoke(tmpEntityItem); + Object dataWatcher = entityItemClass.getMethod("getDataWatcher").invoke(tmpEntityItem); - creationPackets[0] = packetPlayOutSpawnEntityClass.getConstructor(entityClass, int.class).newInstance(entityItem, 2); - creationPackets[1] = packetPlayOutEntityMetadataClass.getConstructor(int.class, dataWatcherClass, boolean.class).newInstance(entityId, dataWatcher, true); - creationPackets[2] = packetPlayOutEntityVelocityClass.getConstructor(int.class, double.class, double.class, double.class).newInstance(entityId, 0D, 0D, 0D); + creationPackets[0] = packetPlayOutSpawnEntityClass.getConstructor(entityClass, int.class).newInstance(tmpEntityItem, 2); + creationPackets[1] = packetPlayOutEntityMetadataClass.getConstructor(int.class, dataWatcherClass, boolean.class).newInstance(tmpEntityId, dataWatcher, true); + creationPackets[2] = packetPlayOutEntityVelocityClass.getConstructor(int.class, double.class, double.class, double.class).newInstance(tmpEntityId, 0D, 0D, 0D); } catch (NoSuchMethodException | NoSuchFieldException | InstantiationException | IllegalAccessException | InvocationTargetException e) { plugin.getLogger().severe("Failed to create shop item"); plugin.debug("Failed to create shop item with reflection"); plugin.debug(e); } + + entityItem = tmpEntityItem; + entityId = tmpEntityId; } - public void remove() { - for (UUID uuid : visibility) { - Player p = Bukkit.getPlayer(uuid); - if (p != null) setVisible(p, false); - } - } - - /** - * Respawns the item at the set location for a player - * @param p Player, for which the item should be reset - */ - public void resetForPlayer(Player p) { - setVisible(p, false); - setVisible(p, true); - } - - public boolean isVisible(Player p) { - return visibility.contains(p.getUniqueId()); - } - - public void setVisible(final Player p, boolean visible) { - if (isVisible(p) == visible) - return; - - if (visible) { - for (Object packet : this.creationPackets) { - Utils.sendPacket(plugin, packet, p); - } - visibility.add(p.getUniqueId()); - } else { - try { - if (p.isOnline()) { - Object packetPlayOutEntityDestroy = packetPlayOutEntityDestroyClass.getConstructor(int[].class).newInstance((Object) new int[]{entityId}); - Utils.sendPacket(plugin, packetPlayOutEntityDestroy, p); - } - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { - plugin.getLogger().severe("Failed to destroy shop item"); - plugin.debug("Failed to destroy shop item with reflection"); - plugin.debug(e); - } - visibility.remove(p.getUniqueId()); - } - } - - /** * @return Clone of the location, where the shop item should be (it could have been moved by something, even though it shouldn't) * To get the exact location, use reflection and extract the location of the {@code EntityItem} @@ -157,4 +117,83 @@ public class ShopItem { public ItemStack getItemStack() { return itemStack.clone(); } + + /** + * @param p Player to check + * @return Whether the item is visible to the player + */ + public boolean isVisible(Player p) { + return viewers.contains(p.getUniqueId()); + } + + /** + * @param p Player to which the item should be shown + */ + public void showPlayer(Player p) { + showPlayer(p, false); + } + + /** + * @param p Player to which the item should be shown + * @param force whether to force or not + */ + public void showPlayer(Player p, boolean force) { + if (viewers.add(p.getUniqueId()) || force) { + for (Object packet : creationPackets) { + Utils.sendPacket(plugin, packet, p); + } + } + } + + /** + * @param p Player from which the item should be hidden + */ + public void hidePlayer(Player p) { + hidePlayer(p, false); + } + + /** + * @param p Player from which the item should be hidden + * @param force whether to force or not + */ + public void hidePlayer(Player p, boolean force) { + if (viewers.remove(p.getUniqueId()) || force) { + try { + if (p.isOnline()) { + Object packetPlayOutEntityDestroy = packetPlayOutEntityDestroyClass.getConstructor(int[].class).newInstance((Object) new int[]{entityId}); + Utils.sendPacket(plugin, packetPlayOutEntityDestroy, p); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { + plugin.getLogger().severe("Failed to destroy shop item"); + plugin.debug("Failed to destroy shop item with reflection"); + plugin.debug(e); + } + } + } + + public void resetVisible(Player p) { + viewers.remove(p.getUniqueId()); + } + + /** + * Removes the item.
+ * Item will be hidden from all players + */ + public void remove() { + // Avoid ConcurrentModificationException + for (UUID uuid : new ArrayList<>(viewers)) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) hidePlayer(p); + } + } + + /** + * Respawns the item at the set location for a player + * @param p Player, for which the item should be reset + */ + public void resetForPlayer(Player p) { + hidePlayer(p); + showPlayer(p); + } + } diff --git a/src/main/java/de/epiceric/shopchest/utils/FastMath.java b/src/main/java/de/epiceric/shopchest/utils/FastMath.java new file mode 100644 index 0000000..a99d8a4 --- /dev/null +++ b/src/main/java/de/epiceric/shopchest/utils/FastMath.java @@ -0,0 +1,42 @@ +package de.epiceric.shopchest.utils; + +public class FastMath { + + /** + * Fast sqrt, 1.57% precision + * + * @param n value to calculate square root from + * @return the square root of n + */ + public static double sqrt(double n) { + return n * Double.longBitsToDouble(6910470738111508698L - (Double.doubleToRawLongBits(n) >> 1)); + } + + /** + * Fast acos, 2.9% precision + * + * @param n value to calculate arc cosine from + * @return the arc cosine of n + */ + public static double acos(double n) { + int v = (int) (n * MULTIPLIER + OFFSET); + while (v > PRECISION) v -= PRECISION; + while (v < 0) v += PRECISION; + return acos[v]; + } + + // Below is lookup table generation + // It is only executed once at initialization + + private static final int PRECISION = 512; + private static final double MULTIPLIER = PRECISION / 2D; + private static final double OFFSET = MULTIPLIER + 0.5D; // + 0.5 as cast truncate and don't round + private static final double[] acos = new double[PRECISION + 1]; + + static { + for (int i = 0; i <= PRECISION; i++) { + acos[i] = Math.acos(i * (2D / PRECISION) - 1); + } + } + +} diff --git a/src/main/java/de/epiceric/shopchest/utils/Operator.java b/src/main/java/de/epiceric/shopchest/utils/Operator.java new file mode 100644 index 0000000..ebb7f96 --- /dev/null +++ b/src/main/java/de/epiceric/shopchest/utils/Operator.java @@ -0,0 +1,75 @@ +package de.epiceric.shopchest.utils; + +public enum Operator { + + EQUAL("==") { + @Override + public boolean compare(double a, double b) { + return Double.compare(a, b) == 0; + } + @Override + public boolean compare(String a, String b) { + return a.equals(b); + } + }, + + NOT_EQUAL("!=") { + @Override + public boolean compare(double a, double b) { + return Double.compare(a, b) != 0; + } + @Override + public boolean compare(String a, String b) { + return !a.equals(b); + } + }, + + GREATER_THAN(">") { + @Override + public boolean compare(double a, double b) { + return a > b; + } + }, + + GREATER_THAN_OR_EQUAL(">=") { + @Override + public boolean compare(double a, double b) { + return a >= b; + } + }, + + LESS_THAN("<") { + @Override + public boolean compare(double a, double b) { + return a < b; + } + }, + + LESS_THAN_OR_EQUAL("<=") { + @Override + public boolean compare(double a, double b) { + return a <= b; + } + }; + + private final String symbol; + + Operator(String symbol) { + this.symbol = symbol; + } + + public static Operator from(String symbol) { + for (Operator operator : values()) { + if (operator.symbol.equals(symbol)) { + return operator; + } + } + throw new IllegalArgumentException(); + } + + public abstract boolean compare(double a, double b); + + public boolean compare(String a, String b) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/de/epiceric/shopchest/utils/ShopUtils.java b/src/main/java/de/epiceric/shopchest/utils/ShopUtils.java index 9b78364..ac03584 100644 --- a/src/main/java/de/epiceric/shopchest/utils/ShopUtils.java +++ b/src/main/java/de/epiceric/shopchest/utils/ShopUtils.java @@ -13,18 +13,15 @@ import org.bukkit.inventory.InventoryHolder; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.util.Vector; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class ShopUtils { - private final HashMap shopLocation = new HashMap<>(); - private final HashMap playerLocation = new HashMap<>(); + // concurrent since it is updated in async task + private final Map playerLocation = new ConcurrentHashMap<>(); + private final Map shopLocation = new HashMap<>(); + private final Collection shopLocationValues = Collections.unmodifiableCollection(shopLocation.values()); private final ShopChest plugin; public ShopUtils(ShopChest plugin) { @@ -55,10 +52,24 @@ public class ShopUtils { /** * Get all shops + * Do not use for removing while iteration! + * + * @see #getShopsCopy() * @return Read-only collection of all shops, may contain duplicates */ public Collection getShops() { - return Collections.unmodifiableCollection(new ArrayList<>(shopLocation.values())); + return shopLocationValues; + } + + /** + * Get all shops + * Same as {@link #getShops()} but this is safe to remove while iterating + * + * @see #getShops() + * @return Copy of collection of all shops, may contain duplicates + */ + public Collection getShopsCopy() { + return new ArrayList<>(getShops()); } /** @@ -226,7 +237,7 @@ public class ShopUtils { plugin.getShopDatabase().connect(new Callback(plugin) { @Override public void onResult(Integer result) { - for (Shop shop : getShops()) { + for (Shop shop : getShopsCopy()) { removeShop(shop, false); plugin.debug("Removed shop (#" + shop.getID() + ")"); } @@ -278,36 +289,7 @@ public class ShopUtils { } if (plugin.getShopChestConfig().only_show_shops_in_sight) { - Set sight = getShopsInSight(player); - - Set _sight = new HashSet<>(); - - for (Shop shop : sight) { - _sight.add(shop); - if (shop.getHologram() != null && !shop.getHologram().isVisible(player)) { - shop.getHologram().showPlayer(player); - } - - if (plugin.getShopChestConfig().only_show_first_shop_in_sight) break; - } - - double itemDistSqr = Math.pow(plugin.getShopChestConfig().maximal_item_distance, 2); - - for (Shop shop : getShops()) { - if (shop.getItem() != null && shop.getLocation().getWorld().getName().equals(player.getWorld().getName())) { - if (shop.getLocation().distanceSquared(player.getEyeLocation()) <= itemDistSqr) { - shop.getItem().setVisible(player, true); - } else { - shop.getItem().setVisible(player, false); - } - } - - if (!_sight.contains(shop)) { - if (shop.getHologram() != null) { - shop.getHologram().hidePlayer(player); - } - } - } + updateVisibleShops(player); } else { updateNearestShops(player); } @@ -315,55 +297,109 @@ public class ShopUtils { playerLocation.put(player.getUniqueId(), player.getLocation()); } - private Set getShopsInSight(Player player) { - double dist = plugin.getShopChestConfig().maximal_distance; - - Location loc = player.getEyeLocation(); - Vector direction = loc.getDirection(); - - Set shops = new HashSet<>(); - - double i = 0; - - do { - Location below = loc.clone().subtract(0, 1, 0); - - Shop shop = getShop(loc); - if (shop != null) { - shops.add(shop); - } else if ((shop = getShop(below)) != null) { - shops.add(shop); - } - - loc.add(direction); - i++; - } while (i <= dist - (dist%1)); - - direction.multiply(dist - (dist%1)); - loc.add(direction); - - Location below = loc.clone().subtract(0, 1, 0); - - Shop shop = getShop(loc); - if (shop != null) { - shops.add(shop); - } else if ((shop = getShop(below)) != null) { - shops.add(shop); - } - - return shops; + /** + * Remove a player from the {@code playerLocation} map. + * This should only be called when really needed + */ + public void resetPlayerLocation(Player player) { + playerLocation.remove(player.getUniqueId()); } - /** - * Update hologram and item of the shop for a player based on their distance to each other - * @param shop Shop to update - * @param player Player to show the update - */ - public void updateShop(Shop shop, Player player) { - double holoDistSqr = Math.pow(plugin.getShopChestConfig().maximal_distance, 2); - double itemDistSqr = Math.pow(plugin.getShopChestConfig().maximal_item_distance, 2); + private static final double TARGET_THRESHOLD = 1; - updateShop(shop, player, holoDistSqr, itemDistSqr); + private void updateVisibleShops(Player player) { + double itemDistSquared = Math.pow(plugin.getShopChestConfig().maximal_item_distance, 2); + double hologramDistSquared = Math.pow(plugin.getShopChestConfig().maximal_distance, 2); + + boolean firstShopInSight = plugin.getShopChestConfig().only_show_first_shop_in_sight; + + // used if only_show_first_shop_in_sight + List otherShopsInSight = firstShopInSight ? new ArrayList() : null; + double nearestDistance = 0; + Shop nearestShop = null; + + Location pLoc = player.getEyeLocation(); + double pX = pLoc.getX(); + double pY = pLoc.getY(); + double pZ = pLoc.getZ(); + Vector pDir = pLoc.getDirection(); + double dirLength = pDir.length(); + + for (Shop shop : getShops()) { + Location shopLocation = shop.getLocation(); + + if (shopLocation.getWorld().getName().equals(player.getWorld().getName())) { + double distanceSquared = shop.getLocation().distanceSquared(player.getLocation()); + + // Display item based on distance + if (shop.hasItem()) { + if (distanceSquared <= itemDistSquared) { + shop.getItem().showPlayer(player); + } else { + shop.getItem().hidePlayer(player); + } + } + + // Display hologram based on sight + if (shop.hasHologram()) { + if (distanceSquared < hologramDistSquared) { + Location holoLocation = shop.getHologram().getLocation(); + + double x = holoLocation.getX() - pX; + double y = shopLocation.getY() - pY + 1.15; // chest block + item offset + double z = holoLocation.getZ() - pZ; + + // See: org.bukkit.util.Vector#angle(Vector) + double angle = FastMath.acos( + (x * pDir.getX() + y * pDir.getY() + z * pDir.getZ()) + / (FastMath.sqrt(x * x + y * y + z * z) * dirLength) + ); + + double distance = FastMath.sqrt(distanceSquared); + + // Check if is targeted + if (angle * distance < TARGET_THRESHOLD) { + // Display even if not the nearest + if (!firstShopInSight) { + shop.getHologram().showPlayer(player); + continue; + } + + if (nearestShop == null) { + // nearestShop is null + // => we guess this one will be the nearest + nearestShop = shop; + nearestDistance = distance; + continue; + } else if (distance < nearestDistance) { + // nearestShop is NOT null && this shop is nearest + // => we'll hide nearestShop, and guess this one will be the nearest + otherShopsInSight.add(nearestShop); + nearestShop = shop; + nearestDistance = distance; + continue; + } + // else: hologram is farther than nearest, so we hide it + } + } + + // If not in sight + shop.getHologram().hidePlayer(player); + } + } + } + + if (firstShopInSight) { + // we hide other shop as we wan't to display only the first + for (Shop shop : otherShopsInSight) { + // we already checked hasHologram() before adding it + shop.getHologram().hidePlayer(player); + } + } + + if (nearestShop != null && nearestShop.hasHologram()) { + nearestShop.getHologram().showPlayer(player); + } } private void updateNearestShops(Player p) { @@ -371,27 +407,23 @@ public class ShopUtils { double itemDistSqr = Math.pow(plugin.getShopChestConfig().maximal_item_distance, 2); for (Shop shop : getShops()) { - updateShop(shop, p, holoDistSqr, itemDistSqr); - } - } + if (p.getLocation().getWorld().getName().equals(shop.getLocation().getWorld().getName())) { + double distSqr = shop.getLocation().distanceSquared(p.getLocation()); - private void updateShop(Shop shop, Player player, double holoDistSqr, double itemDistSqr) { - if (player.getLocation().getWorld().getName().equals(shop.getLocation().getWorld().getName())) { - double distSqr = shop.getLocation().distanceSquared(player.getLocation()); - - if (shop.getHologram() != null) { - if (distSqr <= holoDistSqr) { - shop.getHologram().showPlayer(player); - } else { - shop.getHologram().hidePlayer(player); + if (shop.hasHologram()) { + if (distSqr <= holoDistSqr) { + shop.getHologram().showPlayer(p); + } else { + shop.getHologram().hidePlayer(p); + } } - } - if (shop.getItem() != null) { - if (distSqr <= itemDistSqr) { - shop.getItem().setVisible(player, true); - } else { - shop.getItem().setVisible(player, false); + if (shop.hasItem()) { + if (distSqr <= itemDistSqr) { + shop.getItem().showPlayer(p); + } else { + shop.getItem().hidePlayer(p); + } } } }