package com.nisovin.shopkeepers; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.List; import java.util.Locale; import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.SkullType; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.command.CommandSender; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.permissions.Permissible; import org.bukkit.util.Vector; import com.nisovin.shopkeepers.compat.NMSManager; public class Utils { /** * Creates a clone of the given {@link ItemStack} with amount <code>1</code>. * * @param item * the item to get a normalized version of * @return the normalized item */ public static ItemStack getNormalizedItem(ItemStack item) { if (item == null) return null; ItemStack normalizedClone = item.clone(); normalizedClone.setAmount(1); return normalizedClone; } // private static final ItemStack EMPTY_ITEM = new ItemStack(Material.AIR, 0); public static boolean isEmpty(ItemStack item) { return item == null || item.getType() == Material.AIR || item.getAmount() <= 0; } public static ItemStack getNullIfEmpty(ItemStack item) { return isEmpty(item) ? null : item; } /*public static ItemStack getEmptyIfNull(ItemStack item) { return item == null ? getEmptyItem() : item; } public static ItemStack normalizedIfEmpty(ItemStack item) { return isEmpty(item) ? EMPTY_ITEM : item; } public static ItemStack getEmptyItem() { return EMPTY_ITEM.clone(); }*/ public static boolean isChest(Material material) { return material == Material.CHEST || material == Material.TRAPPED_CHEST; } public static boolean isSign(Material material) { return material == Material.WALL_SIGN || material == Material.SIGN_POST || material == Material.SIGN; } // TODO temporary, due to a bukkit bug custom head item can currently not be saved public static boolean isCustomHeadItem(ItemStack item) { if (item == null) return false; if (item.getType() != Material.SKULL_ITEM) { return false; } if (item.getDurability() != SkullType.PLAYER.ordinal()) { return false; } ItemMeta meta = item.getItemMeta(); if (meta instanceof SkullMeta) { SkullMeta skullMeta = (SkullMeta) meta; if (skullMeta.hasOwner() && skullMeta.getOwner() == null) { // custom head items usually don't have a valid owner return true; } } return false; } /** * Checks if the given {@link BlockFace} is valid to be used for a wall sign. * * @param blockFace * @return */ public static boolean isWallSignFace(BlockFace blockFace) { return blockFace == BlockFace.NORTH || blockFace == BlockFace.SOUTH || blockFace == BlockFace.EAST || blockFace == BlockFace.WEST; } /** * Determines the axis-aligned {@link BlockFace} for the given direction. * If modY is zero only {@link BlockFace}s facing horizontal will be returned. * This method takes into account that the values for EAST/WEST and NORTH/SOUTH * were switched in some past version of bukkit. So it should also properly work * with older bukkit versions. * * @param modX * @param modY * @param modZ * @return */ public static BlockFace getAxisBlockFace(double modX, double modY, double modZ) { double xAbs = Math.abs(modX); double yAbs = Math.abs(modY); double zAbs = Math.abs(modZ); if (xAbs >= zAbs) { if (xAbs >= yAbs) { if (modX >= 0.0D) { // EAST/WEST and NORTH/SOUTH values were switched in some past bukkit version: // with this additional checks it should work across different versions if (BlockFace.EAST.getModX() == 1) { return BlockFace.EAST; } else { return BlockFace.WEST; } } else { if (BlockFace.EAST.getModX() == 1) { return BlockFace.WEST; } else { return BlockFace.EAST; } } } else { if (modY >= 0.0D) { return BlockFace.UP; } else { return BlockFace.DOWN; } } } else { if (zAbs >= yAbs) { if (modZ >= 0.0D) { if (BlockFace.SOUTH.getModZ() == 1) { return BlockFace.SOUTH; } else { return BlockFace.NORTH; } } else { if (BlockFace.SOUTH.getModZ() == 1) { return BlockFace.NORTH; } else { return BlockFace.SOUTH; } } } else { if (modY >= 0.0D) { return BlockFace.UP; } else { return BlockFace.DOWN; } } } } /** * Tries to find the nearest wall sign {@link BlockFace} facing towards the given direction. * * @param direction * @return a valid wall sign face */ public static BlockFace toWallSignFace(Vector direction) { assert direction != null; return getAxisBlockFace(direction.getX(), 0.0D, direction.getZ()); } /** * Gets the block face a player is looking at. * * @param player * the player * @param targetBlock * the block the player is looking at * @return the block face, or null if none was found */ public static BlockFace getTargetBlockFace(Player player, Block targetBlock) { Location intersection = getBlockIntersection(player, targetBlock); if (intersection == null) return null; Location blockCenter = targetBlock.getLocation().add(0.5D, 0.5D, 0.5D); Vector centerToIntersection = intersection.subtract(blockCenter).toVector(); double x = centerToIntersection.getX(); double y = centerToIntersection.getY(); double z = centerToIntersection.getZ(); return getAxisBlockFace(x, y, z); } /** * Determines the exact intersection point of a players view and a targeted block. * * @param player * the player * @param targetBlock * the block the player is looking at * @return the intersection point of the players view and the target block, * or null if no intersection was found */ public static Location getBlockIntersection(Player player, Block targetBlock) { if (player == null || targetBlock == null) return null; // block bounds: double minX = targetBlock.getX(); double minY = targetBlock.getY(); double minZ = targetBlock.getZ(); double maxX = minX + 1.0D; double maxY = minY + 1.0D; double maxZ = minZ + 1.0D; // ray origin: Location origin = player.getEyeLocation(); double originX = origin.getX(); double originY = origin.getY(); double originZ = origin.getZ(); // ray direction Vector dir = origin.getDirection(); double dirX = dir.getX(); double dirY = dir.getY(); double dirZ = dir.getZ(); // tiny improvement to save a few divisions below: double divX = 1.0D / dirX; double divY = 1.0D / dirY; double divZ = 1.0D / dirZ; // intersection interval: double t0 = 0.0D; double t1 = Double.MAX_VALUE; double tmin; double tmax; double tymin; double tymax; double tzmin; double tzmax; if (dirX >= 0.0D) { tmin = (minX - originX) * divX; tmax = (maxX - originX) * divX; } else { tmin = (maxX - originX) * divX; tmax = (minX - originX) * divX; } if (dirY >= 0.0D) { tymin = (minY - originY) * divY; tymax = (maxY - originY) * divY; } else { tymin = (maxY - originY) * divY; tymax = (minY - originY) * divY; } if ((tmin > tymax) || (tymin > tmax)) { return null; } if (tymin > tmin) tmin = tymin; if (tymax < tmax) tmax = tymax; if (dirZ >= 0.0D) { tzmin = (minZ - originZ) * divZ; tzmax = (maxZ - originZ) * divZ; } else { tzmin = (maxZ - originZ) * divZ; tzmax = (minZ - originZ) * divZ; } if ((tmin > tzmax) || (tzmin > tmax)) { return null; } if (tzmin > tmin) tmin = tzmin; if (tzmax < tmax) tmax = tzmax; if ((tmin >= t1) || (tmax <= t0)) { return null; } // intersection: Location intersection = origin.add(dir.multiply(tmin)); return intersection; } // messages: public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.##", new DecimalFormatSymbols(Locale.US)); static { DECIMAL_FORMAT.setGroupingUsed(false); } public static String getLocationString(Location location) { return getLocationString(location.getWorld().getName(), location.getX(), location.getY(), location.getZ()); } public static String getLocationString(Block block) { return getLocationString(block.getWorld().getName(), block.getX(), block.getY(), block.getZ()); } public static String getLocationString(String worldName, double x, double y, double z) { return worldName + "," + DECIMAL_FORMAT.format(x) + "," + DECIMAL_FORMAT.format(y) + "," + DECIMAL_FORMAT.format(z); } public static String translateColorCodesToAlternative(char altColorChar, String textToTranslate) { char[] b = textToTranslate.toCharArray(); for (int i = 0; i < b.length - 1; i++) { if (b[i] == ChatColor.COLOR_CHAR && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(b[i + 1]) > -1) { b[i] = altColorChar; b[i + 1] = Character.toLowerCase(b[i + 1]); } } return new String(b); } public static String decolorize(String colored) { if (colored == null) return null; return Utils.translateColorCodesToAlternative('&', colored); } public static List<String> decolorize(List<String> colored) { if (colored == null) return null; List<String> decolored = new ArrayList<String>(colored.size()); for (String string : colored) { decolored.add(Utils.translateColorCodesToAlternative('&', string)); } return decolored; } public static String colorize(String message) { if (message == null || message.isEmpty()) return message; return ChatColor.translateAlternateColorCodes('&', message); } public static List<String> colorize(List<String> messages) { if (messages == null) return messages; List<String> colored = new ArrayList<String>(messages.size()); for (String message : messages) { colored.add(Utils.colorize(message)); } return colored; } public static void sendMessage(CommandSender sender, String message, String... args) { // skip if sender is null or message is "empty": if (sender == null || message == null || message.isEmpty()) return; if (args != null && args.length >= 2) { // replace arguments (key-value replacement): String key; String value; for (int i = 1; i < args.length; i += 2) { key = args[i - 1]; value = args[i]; if (key == null || value == null) continue; // skip invalid arguments message = message.replace(key, value); } } String[] msgs = message.split("\n"); for (String msg : msgs) { sender.sendMessage(msg); } } public static boolean isEmpty(String string) { return string == null || string.isEmpty(); } public static String normalize(String identifier) { if (identifier == null) return null; return identifier.trim().replace('_', '-').replace(' ', '-').toLowerCase(Locale.ROOT); } public static List<String> normalize(List<String> identifiers) { if (identifiers == null) return null; List<String> normalized = new ArrayList<String>(identifiers.size()); for (String identifier : identifiers) { normalized.add(normalize(identifier)); } return normalized; } /** * Performs a permissions check and logs debug information about it. * * @param permissible * @param permission * @return */ public static boolean hasPermission(Permissible permissible, String permission) { assert permissible != null; boolean hasPerm = permissible.hasPermission(permission); if (!hasPerm && (permissible instanceof Player)) { Log.debug("Player '" + ((Player) permissible).getName() + "' does not have permission '" + permission + "'."); } return hasPerm; } // entity utilities: public static boolean isNPC(Entity entity) { return entity.hasMetadata("NPC"); } public static List<Entity> getNearbyEntities(Location location, double radius, EntityType... types) { List<Entity> entities = new ArrayList<Entity>(); if (location == null) return entities; if (radius <= 0.0D) return entities; double radius2 = radius * radius; int chunkRadius = ((int) (radius / 16)) + 1; Chunk center = location.getChunk(); int startX = center.getX() - chunkRadius; int endX = center.getX() + chunkRadius; int startZ = center.getZ() - chunkRadius; int endZ = center.getZ() + chunkRadius; World world = location.getWorld(); for (int chunkX = startX; chunkX <= endX; chunkX++) { for (int chunkZ = startZ; chunkZ <= endZ; chunkZ++) { if (!world.isChunkLoaded(chunkX, chunkZ)) continue; Chunk chunk = world.getChunkAt(chunkX, chunkZ); for (Entity entity : chunk.getEntities()) { Location entityLoc = entity.getLocation(); // TODO: this is a workaround: for some yet unknown reason entities sometimes report to be in a // different world.. if (!entityLoc.getWorld().equals(world)) { Log.debug("Found an entity which reports to be in a different world than the chunk we got it from:"); Log.debug("Location=" + location + ", Chunk=" + chunk + ", ChunkWorld=" + chunk.getWorld() + ", entityType=" + entity.getType() + ", entityLocation=" + entityLoc); continue; // skip this entity } if (entityLoc.distanceSquared(location) <= radius2) { if (types == null) { entities.add(entity); } else { EntityType type = entity.getType(); for (EntityType t : types) { if (type.equals(t)) { entities.add(entity); break; } } } } } } } return entities; } // itemstack utilities: public static ItemStack createItemStack(Material type, int amount, short data, String displayName, List<String> lore) { // TODO return null in case of type AIR? ItemStack item = new ItemStack(type, amount, data); return setItemStackNameAndLore(item, displayName, lore); } public static ItemStack setItemStackNameAndLore(ItemStack item, String displayName, List<String> lore) { if (item == null) return null; ItemMeta meta = item.getItemMeta(); if (meta != null) { meta.setDisplayName(displayName); meta.setLore(lore); item.setItemMeta(meta); } return item; } public static String getSimpleItemInfo(ItemStack item) { if (item == null) return "none"; StringBuilder sb = new StringBuilder(); sb.append(item.getType()).append('~').append(item.getDurability()); return sb.toString(); } public static String getSimpleRecipeInfo(ItemStack[] recipe) { if (recipe == null) return "none"; StringBuilder sb = new StringBuilder(); sb.append("[0=").append(getSimpleItemInfo(recipe[0])) .append(",1=").append(getSimpleItemInfo(recipe[1])) .append(",2=").append(getSimpleItemInfo(recipe[2])).append("]"); return sb.toString(); } /** * Same as {@link ItemStack#isSimilar(ItemStack)}, but taking into account that both given ItemStacks might be * <code>null</code>. * * @param item1 * an itemstack * @param item2 * another itemstack * @return <code>true</code> if the given item stacks are both <code>null</code> or similar */ public static boolean isSimilar(ItemStack item1, ItemStack item2) { if (item1 == null) return (item2 == null); return item1.isSimilar(item2); } /** * Checks if the given item matches the specified attributes. * * @param item * the item * @param type * The item type. * @param data * The data value/durability. If -1 is is ignored. * @param displayName * The displayName. If null or empty it is ignored. * @param lore * The item lore. If null or empty it is ignored. * @return <code>true</code> if the item has similar attributes */ public static boolean isSimilar(ItemStack item, Material type, short data, String displayName, List<String> lore) { if (item == null) return false; if (item.getType() != type) return false; if (data != -1 && item.getDurability() != data) return false; ItemMeta itemMeta = null; // compare display name: if (displayName != null && !displayName.isEmpty()) { if (!item.hasItemMeta()) return false; itemMeta = item.getItemMeta(); if (itemMeta == null) return false; if (!itemMeta.hasDisplayName() || !displayName.equals(itemMeta.getDisplayName())) { return false; } } // compare lore: if (lore != null && !lore.isEmpty()) { if (itemMeta == null) { if (!item.hasItemMeta()) return false; itemMeta = item.getItemMeta(); if (itemMeta == null) return false; } if (!itemMeta.hasLore() || !lore.equals(itemMeta.getLore())) { return false; } } return true; } // save and load itemstacks from config, including attributes: /** * Saves the given {@link ItemStack} to the given configuration section. * Also saves the item's attributes in the same section at '{node}_attributes'. * * @param section * a configuration section * @param node * where to save the item stack inside the section * @param item * the item stack to save, can be null */ public static void saveItem(ConfigurationSection section, String node, ItemStack item) { assert section != null && node != null; section.set(node, item); // saving attributes manually, as they weren't saved by bukkit in the past: String attributes = NMSManager.getProvider().saveItemAttributesToString(item); if (attributes != null && !attributes.isEmpty()) { String attributesNode = node + "_attributes"; section.set(attributesNode, attributes); } } /** * Loads an {@link ItemStack} from the given configuration section. * Also attempts to load attributes saved at '{node}_attributes'. * * @param section * a configuration section * @param node * where to load the item stack from inside the section * @return the loaded item stack, possibly null */ public static ItemStack loadItem(ConfigurationSection section, String node) { assert section != null && node != null; ItemStack item = section.getItemStack(node); // loading separately stored attributes: String attributesNode = node + "_attributes"; if (item != null && section.contains(attributesNode)) { String attributes = section.getString(attributesNode); if (attributes != null && !attributes.isEmpty()) { item = NMSManager.getProvider().loadItemAttributesFromString(item, attributes); } } return item; } // inventory utilities: public static List<ItemCount> getItemCountsFromInventory(Inventory inventory, Filter<ItemStack> filter) { List<ItemCount> itemCounts = new ArrayList<ItemCount>(); if (inventory != null) { ItemStack[] contents = inventory.getContents(); for (ItemStack item : contents) { if (isEmpty(item)) continue; if (filter != null && !filter.accept(item)) continue; // check if we already have a counter for this type of item: ItemCount itemCount = ItemCount.findSimilar(itemCounts, item); if (itemCount != null) { // increase item count: itemCount.addAmount(item.getAmount()); } else { // add new item entry: itemCounts.add(new ItemCount(item, item.getAmount())); } } } return itemCounts; } /** * Checks if the given inventory contains at least a certain amount of items which match the specified attributes. * * @param inv * @param type * The item type. * @param data * The data value/durability. If -1 is is ignored. * @param displayName * The displayName. If null it is ignored. * @param lore * The item lore. If null or empty it is ignored. * @param ignoreNameAndLore * @param amount * @return */ public static boolean hasInventoryItemsAtLeast(Inventory inv, Material type, short data, String displayName, List<String> lore, int amount) { for (ItemStack is : inv.getContents()) { if (!Utils.isSimilar(is, type, data, displayName, lore)) continue; int currentAmount = is.getAmount() - amount; if (currentAmount >= 0) { return true; } else { amount = -currentAmount; } } return false; } /** * Adds the given {@link ItemStack} to the given contents. * * <p> * This will first try to fill similar partial {@link ItemStack}s in the contents up to the item's max stack size. * Afterwards it will insert the remaining amount into empty slots, splitting at the item's max stack size.<br> * This does not modify the original item stacks. If it has to modify the amount of an item stack, it first replaces * it with a copy. So in case those item stacks are mirroring changes to their minecraft counterpart, those don't * get affected directly. * </p> * * @param contents * The contents to add the given {@link ItemStack} to. * @param item * The {@link ItemStack} to add to the given contents. * @return The amount of items which couldn't be added (0 on full success). */ public static int addItems(ItemStack[] contents, ItemStack item) { Validate.notNull(contents); Validate.notNull(item); int amount = item.getAmount(); Validate.isTrue(amount >= 0); if (amount == 0) return 0; // search for partially fitting item stacks: int maxStackSize = item.getMaxStackSize(); int size = contents.length; for (int slot = 0; slot < size; slot++) { ItemStack slotItem = contents[slot]; // slot empty? - skip, because we are currently filling existing item stacks up if (isEmpty(slotItem)) continue; // slot already full? int slotAmount = slotItem.getAmount(); if (slotAmount >= maxStackSize) continue; if (slotItem.isSimilar(item)) { // copy itemstack, so we don't modify the original itemstack: slotItem = slotItem.clone(); contents[slot] = slotItem; int newAmount = slotAmount + amount; if (newAmount <= maxStackSize) { // remaining amount did fully fit into this stack: slotItem.setAmount(newAmount); return 0; } else { // did not fully fit: slotItem.setAmount(maxStackSize); amount -= (maxStackSize - slotAmount); assert amount != 0; } } } // we have items remaining: assert amount > 0; // search for free slots: for (int slot = 0; slot < size; slot++) { ItemStack slotItem = contents[slot]; if (isEmpty(slotItem)) { // found free slot: if (amount > maxStackSize) { // add full stack: ItemStack stack = item.clone(); stack.setAmount(maxStackSize); contents[slot] = stack; amount -= maxStackSize; } else { // completely fits: ItemStack stack = item.clone(); // create a copy, just in case stack.setAmount(amount); // stack of remaining amount contents[slot] = stack; return 0; } } } // not all items did fit into the inventory: return amount; } /** * Removes the given {@link ItemStack} from the given contents. * * <p> * If the amount of the given {@link ItemStack} is {@link Integer#MAX_VALUE}, then all similar items are being * removed from the contents.<br> * This does not modify the original item stacks. If it has to modify the amount of an item stack, it first replaces * it with a copy. So in case those item stacks are mirroring changes to their minecraft counterpart, those don't * get affected directly. * </p> * * @param contents * The contents to remove the given {@link ItemStack} from. * @param item * The {@link ItemStack} to remove from the given contents. * @return The amount of items which couldn't be removed (0 on full success). */ public static int removeItems(ItemStack[] contents, ItemStack item) { Validate.notNull(contents); Validate.notNull(item); int amount = item.getAmount(); Validate.isTrue(amount >= 0); if (amount == 0) return 0; boolean removeAll = (amount == Integer.MAX_VALUE); int size = contents.length; for (int slot = 0; slot < size; slot++) { ItemStack slotItem = contents[slot]; if (slotItem == null) continue; if (item.isSimilar(slotItem)) { if (removeAll) { contents[slot] = null; } else { int newAmount = slotItem.getAmount() - amount; if (newAmount > 0) { // copy itemstack, so we don't modify the original itemstack: slotItem = slotItem.clone(); contents[slot] = slotItem; slotItem.setAmount(newAmount); // all items were removed: return 0; } else { contents[slot] = null; amount = -newAmount; if (amount == 0) { // all items were removed: return 0; } } } } } if (removeAll) return 0; return amount; } /** * Removes the specified amount of items which match the specified attributes from the given inventory. * * @param inv * @param type * The item type. * @param data * The data value/durability. If -1 is is ignored. * @param displayName * The displayName. If null it is ignored. * @param lore * The item lore. If null or empty it is ignored. * @param ignoreNameAndLore * @param amount */ public static void removeItemsFromInventory(Inventory inv, Material type, short data, String displayName, List<String> lore, int amount) { for (ItemStack is : inv.getContents()) { if (!Utils.isSimilar(is, type, data, displayName, lore)) continue; int newamount = is.getAmount() - amount; if (newamount > 0) { is.setAmount(newamount); break; } else { inv.remove(is); amount = -newamount; if (amount == 0) break; } } } @SuppressWarnings("deprecation") public static void updateInventoryLater(final Player player) { Bukkit.getScheduler().runTaskLater(ShopkeepersPlugin.getInstance(), new Runnable() { @Override public void run() { player.updateInventory(); } }, 3L); } public static Integer parseInt(String intString) { try { return Integer.parseInt(intString); } catch (NumberFormatException e) { return null; } } }