package de.epiceric.shopchest.utils; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.block.Chest; import org.bukkit.block.DoubleChest; import org.bukkit.entity.Player; import org.bukkit.inventory.InventoryHolder; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.util.Vector; import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.config.Config; import de.epiceric.shopchest.event.ShopsLoadedEvent; import de.epiceric.shopchest.shop.Shop; import de.epiceric.shopchest.shop.Shop.ShopType; public class ShopUtils { private final Map playerShopAmount = new HashMap<>(); // concurrent since it is updated in async task private final Map playerLocation = new ConcurrentHashMap<>(); private final Map shopLocation = new ConcurrentHashMap<>(); private final Collection shopLocationValues = Collections.unmodifiableCollection(shopLocation.values()); private final ShopChest plugin; public ShopUtils(ShopChest plugin) { this.plugin = plugin; } /** * Get the shop at a given location * * @param location Location of the shop * @return Shop at the given location or null if no shop is found there */ public Shop getShop(Location location) { Location newLocation = new Location(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ()); return shopLocation.get(newLocation); } /** * Checks whether there is a shop at a given location * @param location Location to check * @return Whether there is a shop at the given location */ public boolean isShop(Location location) { return getShop(location) != null; } /** * 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 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()); } /** * Add a shop * @param shop Shop to add * @param addToDatabase Whether the shop should also be added to the database * @param callback Callback that - if succeeded - returns the ID the shop had or was given (as {@code int}) */ public void addShop(Shop shop, boolean addToDatabase, Callback callback) { InventoryHolder ih = shop.getInventoryHolder(); plugin.debug("Adding shop... (#" + shop.getID() + ")"); if (ih instanceof DoubleChest) { DoubleChest dc = (DoubleChest) ih; Chest r = (Chest) dc.getRightSide(); Chest l = (Chest) dc.getLeftSide(); plugin.debug("Added shop as double chest. (#" + shop.getID() + ")"); shopLocation.put(r.getLocation(), shop); shopLocation.put(l.getLocation(), shop); } else { plugin.debug("Added shop as single chest. (#" + shop.getID() + ")"); shopLocation.put(shop.getLocation(), shop); } if (addToDatabase) { if (shop.getShopType() != ShopType.ADMIN) { playerShopAmount.compute(shop.getVendor().getUniqueId(), (uuid, amount) -> amount == null ? new Counter(1) : amount.increment()); } plugin.getShopDatabase().addShop(shop, callback); } else { if (callback != null) callback.callSyncResult(shop.getID()); } } /** * Add a shop * @param shop Shop to add * @param addToDatabase Whether the shop should also be added to the database */ public void addShop(Shop shop, boolean addToDatabase) { addShop(shop, addToDatabase, null); } /** Remove a shop. May not work properly if double chest doesn't exist! * @param shop Shop to remove * @param removeFromDatabase Whether the shop should also be removed from the database * @param callback Callback that - if succeeded - returns null * @see ShopUtils#removeShopById(int, boolean, Callback) */ public void removeShop(Shop shop, boolean removeFromDatabase, Callback callback) { plugin.debug("Removing shop (#" + shop.getID() + ")"); if (shop.isCreated()) { InventoryHolder ih = shop.getInventoryHolder(); if (ih instanceof DoubleChest) { DoubleChest dc = (DoubleChest) ih; Chest r = (Chest) dc.getRightSide(); Chest l = (Chest) dc.getLeftSide(); shopLocation.remove(r.getLocation()); shopLocation.remove(l.getLocation()); } else { shopLocation.remove(shop.getLocation()); } shop.removeItem(); shop.removeHologram(); } if (removeFromDatabase) { if (shop.getShopType() != ShopType.ADMIN) { playerShopAmount.compute(shop.getVendor().getUniqueId(), (uuid, amount) -> amount == null ? new Counter() : amount.decrement()); } plugin.getShopDatabase().removeShop(shop, callback); } else { if (callback != null) callback.callSyncResult(null); } } /** * Remove a shop. May not work properly if double chest doesn't exist! * @param shop Shop to remove * @param removeFromDatabase Whether the shop should also be removed from the database * @see ShopUtils#removeShopById(int, boolean) */ public void removeShop(Shop shop, boolean removeFromDatabase) { removeShop(shop, removeFromDatabase, null); } /** * Remove a shop by its ID * @param shopId ID of the shop to remove * @param removeFromDatabase Whether the shop should also be removed from the database * @param callback Callback that - if succeeded - returns null */ public void removeShopById(int shopId, boolean removeFromDatabase, Callback callback) { Map toRemove = shopLocation.entrySet().stream() .filter(e -> e.getValue().getID() == shopId) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); plugin.debug(String.format("Removing %d shop(s) with ID %d", toRemove.size(), shopId)); if (toRemove.isEmpty()) { if (callback != null) callback.callSyncResult(null); return; } toRemove.forEach((loc, shop) -> { shopLocation.remove(loc); shop.removeItem(); shop.removeHologram(); }); Shop first = toRemove.values().iterator().next(); boolean isAdmin = first.getShopType() == ShopType.ADMIN; UUID vendorUuid = first.getVendor().getUniqueId(); // Database#removeShop removes shop by ID so this only needs to be called once if (removeFromDatabase) { if (!isAdmin) { playerShopAmount.compute(vendorUuid, (uuid, amount) -> amount == null ? new Counter() : amount.decrement()); } plugin.getShopDatabase().removeShop(toRemove.values().iterator().next(), callback); } else { if (callback != null) callback.callSyncResult(null); } } /** * Remove a shop by its ID * @param shopId ID of the shop to remove * @param removeFromDatabase Whether the shop should also be removed from the database */ public void removeShopById(int shopId, boolean removeFromDatabase) { removeShopById(shopId, removeFromDatabase, null); } /** * Get the shop limits of a player * @param p Player, whose shop limits should be returned * @return The shop limits of the given player */ public int getShopLimit(Player p) { int limit = 0; boolean useDefault = true; for (PermissionAttachmentInfo permInfo : p.getEffectivePermissions()) { if (permInfo.getPermission().startsWith("shopchest.limit.") && p.hasPermission(permInfo.getPermission())) { if (permInfo.getPermission().equalsIgnoreCase(Permissions.NO_LIMIT)) { limit = -1; useDefault = false; break; } else { String[] spl = permInfo.getPermission().split("shopchest.limit."); if (spl.length > 1) { try { int newLimit = Integer.valueOf(spl[1]); if (newLimit < 0) { limit = -1; break; } limit = Math.max(limit, newLimit); useDefault = false; } catch (NumberFormatException ignored) { /* Ignore and continue */ } } } } } if (limit < -1) limit = -1; return (useDefault ?Config.defaultLimit : limit); } /** * Get the amount of shops of a player * @param p Player, whose shops should be counted * @return The amount of a shops a player has (if {@link Config#excludeAdminShops} is true, admin shops won't be counted) */ public int getShopAmount(OfflinePlayer p) { return playerShopAmount.getOrDefault(p.getUniqueId(), new Counter()).get(); } /** * Get all shops of a player from the database without loading them * @param p Player, whose shops should be get * @param callback Callback that returns a collection of the given player's shops */ public void getShops(OfflinePlayer p, Callback> callback) { plugin.getShopDatabase().getShops(p.getUniqueId(), new Callback>(plugin) { @Override public void onResult(Collection result) { Set shops = new HashSet<>(); for (Shop playerShop : result) { Shop loadedShop = getShop(playerShop.getLocation()); if (loadedShop != null && loadedShop.equals(playerShop)) { shops.add(loadedShop); } else { shops.add(playerShop); } } if (callback != null) callback.onResult(shops); } @Override public void onError(Throwable throwable) { if (callback != null) callback.onError(throwable); } }); } /** * Loads the amount of shops for each player * @param callback Callback that returns the amount of shops for each player */ public void loadShopAmounts(final Callback> callback) { plugin.getShopDatabase().getShopAmounts(new Callback>(plugin) { @Override public void onResult(Map result) { playerShopAmount.clear(); result.forEach((uuid, amount) -> playerShopAmount.put(uuid, new Counter(amount))); if (callback != null) callback.onResult(result); } @Override public void onError(Throwable throwable) { if (callback != null) callback.onError(throwable); } }); } /** * Gets all shops in the given chunk from the database and adds them to the server * @param chunk The chunk to load shops from * @param callback Callback that returns the amount of shops added if succeeded * @see ShopUtils#loadShops(Chunk[], Callback) */ public void loadShops(final Chunk chunk, final Callback callback) { loadShops(new Chunk[] {chunk}, callback); } /** * Gets all shops in the given chunks from the database and adds them to the server * @param chunk The chunks to load shops from * @param callback Callback that returns the amount of shops added if succeeded * @see ShopUtils#loadShops(Chunk Callback) */ public void loadShops(final Chunk[] chunks, final Callback callback) { plugin.getShopDatabase().getShopsInChunks(chunks, new Callback>(plugin) { @Override public void onResult(Collection result) { Collection loadedShops = new HashSet<>(); for (Shop shop : result) { Location loc = shop.getLocation(); // Don't add shop if shop is already loaded if (shopLocation.containsKey(loc)) { continue; } int x = loc.getBlockX() / 16; int z = loc.getBlockZ() / 16; // Don't add shop if chunk is no longer loaded if (!loc.getWorld().isChunkLoaded(x, z)) { continue; } if (shop.create(true)) { addShop(shop, false); loadedShops.add(shop); } } if (callback != null) callback.onResult(loadedShops.size()); Bukkit.getPluginManager().callEvent(new ShopsLoadedEvent(Collections.unmodifiableCollection(loadedShops))); } @Override public void onError(Throwable throwable) { if (callback != null) callback.onError(throwable); } }); } /** * Update hologram and item of all shops for a player * @param player Player to show the updates */ public void updateShops(Player player) { updateShops(player, false); } /** * Update hologram and item of all shops for a player * @param player Player to show the updates * @param force Whether update should be forced even if player has not moved */ public void updateShops(Player player, boolean force) { if (!force && player.getLocation().equals(playerLocation.get(player.getUniqueId()))) { // Player has not moved, so don't calculate shops again. return; } if (Config.onlyShowShopsInSight) { updateVisibleShops(player); } else { updateNearestShops(player); } playerLocation.put(player.getUniqueId(), player.getLocation()); } /** * Remove a saved location of a player to force a recalculation * of whether the hologram should be visible. * This should only be called when really needed * @param player Player whose saved location will be reset */ public void resetPlayerLocation(Player player) { playerLocation.remove(player.getUniqueId()); } private void updateVisibleShops(Player player) { double itemDistSquared = Math.pow(Config.maximalItemDistance, 2); double maxDist = Config.maximalDistance; double nearestDistSquared = Double.MAX_VALUE; Shop nearestShop = null; Location pLoc = player.getEyeLocation(); Vector pDir = pLoc.getDirection(); // Display holograms based on sight for (double i = 0; i <= maxDist; i++) { Location loc = pLoc.clone(); Vector dir = pDir.clone(); double factor = Math.min(i, maxDist); loc.add(dir.multiply(factor)); Location locBelow = loc.clone().subtract(0, 1, 0); // Check block below as player may look at hologram Shop shop = getShop(loc); if (shop == null) { shop = getShop(locBelow); } if (shop != null && shop.hasHologram()) { double distSquared = pLoc.distanceSquared(loc); if (distSquared < nearestDistSquared) { nearestDistSquared = distSquared; nearestShop = shop; } } } for (Shop shop : getShops()) { if (!shop.equals(nearestShop) && shop.hasHologram()) { shop.getHologram().hidePlayer(player); } // Display item based on distance Location shopLocation = shop.getLocation(); if (shopLocation.getWorld().getName().equals(player.getWorld().getName())) { double distSquared = shop.getLocation().distanceSquared(player.getLocation()); if (shop.hasItem()) { if (distSquared <= itemDistSquared) { shop.getItem().showPlayer(player); } else { shop.getItem().hidePlayer(player); } } } } if (nearestShop != null) { nearestShop.getHologram().showPlayer(player); } } private void updateNearestShops(Player p) { double holoDistSqr = Math.pow(Config.maximalDistance, 2); double itemDistSqr = Math.pow(Config.maximalItemDistance, 2); Location playerLocation = p.getLocation(); for (Shop shop : getShops()) { if (playerLocation.getWorld().getName().equals(shop.getLocation().getWorld().getName())) { double distSqr = shop.getLocation().distanceSquared(playerLocation); if (shop.hasHologram()) { if (distSqr <= holoDistSqr) { shop.getHologram().showPlayer(p); } else { shop.getHologram().hidePlayer(p); } } if (shop.hasItem()) { if (distSqr <= itemDistSqr) { shop.getItem().showPlayer(p); } else { shop.getItem().hidePlayer(p); } } } } } }