511 lines
18 KiB
Java
511 lines
18 KiB
Java
package com.Acrobot.Breeze.Utils;
|
|
|
|
import com.Acrobot.Breeze.Collection.SimpleCache;
|
|
import com.Acrobot.ChestShop.ChestShop;
|
|
import com.Acrobot.ChestShop.Configuration.Messages;
|
|
import com.Acrobot.ChestShop.Configuration.Properties;
|
|
import com.Acrobot.ChestShop.Events.MaterialParseEvent;
|
|
import com.Acrobot.ChestShop.Utils.ItemUtil;
|
|
import de.themoep.ShowItem.api.ShowItem;
|
|
import de.themoep.minedown.adventure.Replacer;
|
|
import net.kyori.adventure.text.Component;
|
|
import net.kyori.adventure.text.TextComponent;
|
|
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.configuration.file.YamlConstructor;
|
|
import org.bukkit.configuration.file.YamlRepresenter;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.inventory.ItemStack;
|
|
import org.bukkit.inventory.meta.Damageable;
|
|
import org.bukkit.inventory.meta.ItemMeta;
|
|
import org.bukkit.plugin.Plugin;
|
|
import org.yaml.snakeyaml.DumperOptions;
|
|
import org.yaml.snakeyaml.Yaml;
|
|
import org.yaml.snakeyaml.nodes.Tag;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.logging.Level;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import static com.Acrobot.Breeze.Utils.StringUtil.getMinecraftCharWidth;
|
|
import static com.Acrobot.Breeze.Utils.StringUtil.getMinecraftStringWidth;
|
|
|
|
/**
|
|
* @author Acrobot
|
|
*/
|
|
public class MaterialUtil {
|
|
public static final Pattern DURABILITY = Pattern.compile(":(\\d)*");
|
|
public static final Pattern METADATA = Pattern.compile("#([0-9a-zA-Z])*");
|
|
|
|
public static final boolean LONG_NAME = true;
|
|
public static final boolean SHORT_NAME = false;
|
|
/**
|
|
* @deprecated Use {@link MaterialUtil#MAXIMUM_SIGN_WIDTH}
|
|
*/
|
|
@Deprecated
|
|
public static final short MAXIMUM_SIGN_LETTERS = 15;
|
|
// 15 dashes fit on one sign line with the default resource pack:
|
|
public static final int MAXIMUM_SIGN_WIDTH = (short) getMinecraftStringWidth("---------------");
|
|
|
|
private static final SimpleCache<String, Material> MATERIAL_CACHE = new SimpleCache<>(Properties.CACHE_SIZE);
|
|
|
|
private static final Yaml YAML = new Yaml(new YamlBukkitConstructor(), new YamlRepresenter(), new DumperOptions());
|
|
|
|
private static class YamlBukkitConstructor extends YamlConstructor {
|
|
public YamlBukkitConstructor() {
|
|
this.yamlConstructors.put(new Tag(Tag.PREFIX + "org.bukkit.inventory.ItemStack"), yamlConstructors.get(Tag.MAP));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the itemStack is empty or null
|
|
*
|
|
* @param item Item to check
|
|
* @return Is the itemStack empty?
|
|
*/
|
|
public static boolean isEmpty(ItemStack item) {
|
|
return item == null || item.getType() == Material.AIR;
|
|
}
|
|
|
|
/**
|
|
* Checks if the itemStacks are equal, ignoring their amount
|
|
*
|
|
* @param one first itemStack
|
|
* @param two second itemStack
|
|
* @return Are they equal?
|
|
*/
|
|
public static boolean equals(ItemStack one, ItemStack two) {
|
|
if (one == null || two == null) {
|
|
return one == two;
|
|
}
|
|
if (one.isSimilar(two)) {
|
|
return true;
|
|
}
|
|
|
|
// Additional checks as serialisation and de-serialisation might lead to different item meta
|
|
// This would only be done if the items share the same item meta type so it shouldn't be too inefficient
|
|
// Special check for books as their pages might change when serialising (See SPIGOT-3206 and ChestShop#250)
|
|
// Special check for explorer maps/every item with a localised name (See SPIGOT-4672)
|
|
// Special check for legacy spawn eggs (See ChestShop#264)
|
|
if (one.getType() != two.getType()
|
|
|| one.getDurability() != two.getDurability()
|
|
|| (one.hasItemMeta() && two.hasItemMeta() && one.getItemMeta().getClass() != two.getItemMeta().getClass())) {
|
|
return false;
|
|
}
|
|
if (!one.hasItemMeta() && !two.hasItemMeta()) {
|
|
return true;
|
|
}
|
|
Map<String, Object> oneSerMeta = one.getItemMeta().serialize();
|
|
Map<String, Object> twoSerMeta = two.getItemMeta().serialize();
|
|
if (oneSerMeta.equals(twoSerMeta)) {
|
|
return true;
|
|
}
|
|
|
|
// Try to use same parsing as the YAML dumper in the ItemDatabase when generating the code as the last resort
|
|
ItemStack oneDumped = YAML.loadAs(YAML.dump(one), ItemStack.class);
|
|
if (oneDumped.isSimilar(two) || oneDumped.getItemMeta().serialize().equals(twoSerMeta)) {
|
|
return true;
|
|
}
|
|
|
|
ItemStack twoDumped = YAML.loadAs(YAML.dump(two), ItemStack.class);
|
|
if (oneDumped.isSimilar(twoDumped) || oneDumped.getItemMeta().serialize().equals(twoDumped.getItemMeta().serialize())) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gives you a Material from a String (doesn't have to be fully typed in)
|
|
*
|
|
* @param name Name of the material
|
|
* @return Material found
|
|
*/
|
|
public static Material getMaterial(String name) {
|
|
String formatted = name.replaceAll("(?<!^)([A-Z1-9])", "_$1").replace(' ', '_').toUpperCase(Locale.ROOT);
|
|
|
|
Material material = MATERIAL_CACHE.get(formatted);
|
|
if (material != null) {
|
|
return material;
|
|
}
|
|
|
|
material = Material.matchMaterial(name);
|
|
|
|
if (material != null) {
|
|
MATERIAL_CACHE.put(formatted, material);
|
|
return material;
|
|
}
|
|
|
|
material = new EnumParser<Material>().parse(name, Material.values());
|
|
if (material != null) {
|
|
MATERIAL_CACHE.put(formatted, material);
|
|
}
|
|
|
|
return material;
|
|
}
|
|
|
|
/**
|
|
* Get a list with item information
|
|
*
|
|
* @param items The items to get the information from
|
|
* @return The list, including the amount and names of the items
|
|
* @deprecated Use {@link ItemUtil#getItemList(ItemStack[])} instead!
|
|
*/
|
|
@Deprecated
|
|
public static String getItemList(ItemStack[] items) {
|
|
ItemStack[] mergedItems = InventoryUtil.mergeSimilarStacks(items);
|
|
|
|
List<String> itemText = new ArrayList<>();
|
|
|
|
for (ItemStack item : mergedItems) {
|
|
itemText.add(item.getAmount() + " " + getName(item));
|
|
}
|
|
|
|
return String.join(", ", itemText);
|
|
}
|
|
|
|
/**
|
|
* Returns item's name
|
|
* Use {@link ItemUtil#getName(ItemStack, int)} if you want to get name aliases too!
|
|
*
|
|
* @param itemStack ItemStack to name
|
|
* @return ItemStack's name
|
|
*/
|
|
public static String getName(ItemStack itemStack) {
|
|
return getName(itemStack, 0);
|
|
}
|
|
|
|
/**
|
|
* Returns item's name
|
|
*
|
|
* @param itemStack ItemStack to name
|
|
* @param showDataValue Should we also show the data value?
|
|
* @return ItemStack's name
|
|
* @deprecated Use {@link #getName(ItemStack, int)}
|
|
*/
|
|
@Deprecated
|
|
public static String getName(ItemStack itemStack, boolean showDataValue) {
|
|
return getName(itemStack, 0);
|
|
}
|
|
|
|
/**
|
|
* Returns item's name, just like on the sign
|
|
* Use {@link ItemUtil#getSignName(ItemStack)} if you want to get name aliases too!
|
|
*
|
|
* @param itemStack ItemStack to name
|
|
* @return ItemStack's name
|
|
*/
|
|
public static String getSignName(ItemStack itemStack) {
|
|
return getName(itemStack, MAXIMUM_SIGN_WIDTH);
|
|
}
|
|
|
|
/**
|
|
* Returns item's name, with a maximum width.
|
|
* Use {@link ItemUtil#getName(ItemStack, int)} if you want to get name aliases too!
|
|
*
|
|
* @param itemStack ItemStack to name
|
|
* @param maxWidth The max width that the name should have; 0 or below if it should be unlimited
|
|
* @return ItemStack's name
|
|
*/
|
|
public static String getName(ItemStack itemStack, int maxWidth) {
|
|
String itemName = itemStack.getType().toString();
|
|
|
|
String durability = "";
|
|
if (itemStack.getDurability() != 0) {
|
|
durability = ":" + itemStack.getDurability();
|
|
}
|
|
|
|
String metaData = "";
|
|
if (itemStack.hasItemMeta()) {
|
|
metaData = "#" + Metadata.getItemCode(itemStack);
|
|
}
|
|
|
|
String code = StringUtil.capitalizeFirstLetter(itemName, '_');
|
|
int codeWidth = getMinecraftStringWidth(code + durability + metaData);
|
|
if (maxWidth > 0 && codeWidth > maxWidth) {
|
|
int exceeding = codeWidth - maxWidth;
|
|
code = getShortenedName(code, getMinecraftStringWidth(code) - exceeding);
|
|
}
|
|
|
|
return code + durability + metaData;
|
|
}
|
|
|
|
/**
|
|
* Get an item name shortened to a max length that is still reversable by {@link #getMaterial(String)}
|
|
*
|
|
* @param itemName The name of the item
|
|
* @param maxWidth The max width
|
|
* @return The name shortened to the max length
|
|
*/
|
|
public static String getShortenedName(String itemName, int maxWidth) {
|
|
itemName = StringUtil.capitalizeFirstLetter(itemName.replace('_', ' '), ' ');
|
|
int width = getMinecraftStringWidth(itemName);
|
|
if (width <= maxWidth) {
|
|
return itemName;
|
|
}
|
|
String[] itemParts = itemName.split(" ");
|
|
itemName = String.join("", itemParts);
|
|
width = getMinecraftStringWidth(itemName);
|
|
if (width <= maxWidth) {
|
|
return itemName;
|
|
}
|
|
int exceeding = width - maxWidth;
|
|
int shortestIndex = 0;
|
|
int longestIndex = 0;
|
|
for (int i = 0; i < itemParts.length; i++) {
|
|
if (getMinecraftStringWidth(itemParts[longestIndex]) < getMinecraftStringWidth(itemParts[i])) {
|
|
longestIndex = i;
|
|
}
|
|
if (getMinecraftStringWidth(itemParts[shortestIndex]) > getMinecraftStringWidth(itemParts[i])) {
|
|
shortestIndex = i;
|
|
}
|
|
}
|
|
int shortestWidth = getMinecraftStringWidth(itemParts[shortestIndex]);
|
|
int longestWidth = getMinecraftStringWidth(itemParts[longestIndex]);
|
|
int remove = longestWidth - shortestWidth;
|
|
while (remove > 0 && exceeding > 0) {
|
|
int endWidth = getMinecraftCharWidth(itemParts[longestIndex].charAt(itemParts[longestIndex].length() - 1));
|
|
itemParts[longestIndex] = itemParts[longestIndex].substring(0, itemParts[longestIndex].length() - 1);
|
|
remove -= endWidth;
|
|
exceeding -= endWidth;
|
|
}
|
|
|
|
for (int i = itemParts.length - 1; i >= 0 && exceeding > 0; i--) {
|
|
int partWidth = getMinecraftStringWidth(itemParts[i]);
|
|
|
|
if (partWidth > shortestWidth) {
|
|
remove = partWidth - shortestWidth;
|
|
}
|
|
|
|
if (remove > exceeding) {
|
|
remove = exceeding;
|
|
}
|
|
|
|
while (remove > 0) {
|
|
int endWidth = getMinecraftCharWidth(itemParts[i].charAt(itemParts[i].length() - 1));
|
|
itemParts[i] = itemParts[i].substring(0, itemParts[i].length() - 1);
|
|
remove -= endWidth;
|
|
exceeding -= endWidth;
|
|
}
|
|
}
|
|
|
|
while (exceeding > 0) {
|
|
for (int i = itemParts.length - 1; i >= 0 && exceeding > 0; i--) {
|
|
int endWidth = getMinecraftCharWidth(itemParts[i].charAt(itemParts[i].length() - 1));
|
|
itemParts[i] = itemParts[i].substring(0, itemParts[i].length() - 1);
|
|
exceeding -= endWidth;
|
|
}
|
|
}
|
|
return String.join("", itemParts);
|
|
}
|
|
|
|
/**
|
|
* Gives you an ItemStack from a String
|
|
*
|
|
* @param itemName Item name
|
|
* @return ItemStack
|
|
*/
|
|
public static ItemStack getItem(String itemName) {
|
|
String[] split = itemName.split("[:\\-#]");
|
|
for (int i = 0; i < split.length; i++) {
|
|
split[i] = split[i].trim();
|
|
}
|
|
|
|
short durability = getDurability(itemName);
|
|
MaterialParseEvent parseEvent = new MaterialParseEvent(split[0], durability);
|
|
Bukkit.getPluginManager().callEvent(parseEvent);
|
|
Material material = parseEvent.getMaterial();
|
|
if (material == null) {
|
|
return null;
|
|
}
|
|
|
|
ItemStack itemStack = new ItemStack(material);
|
|
|
|
ItemMeta meta = getMetadata(itemName);
|
|
|
|
if (meta != null) {
|
|
if (meta instanceof Damageable) {
|
|
((Damageable) meta).setDamage(durability);
|
|
}
|
|
itemStack.setItemMeta(meta);
|
|
}
|
|
|
|
return itemStack;
|
|
}
|
|
|
|
/**
|
|
* Returns the durability from a string
|
|
*
|
|
* @param itemName Item name
|
|
* @return Durability found
|
|
*/
|
|
public static short getDurability(String itemName) {
|
|
Matcher m = DURABILITY.matcher(itemName);
|
|
|
|
if (!m.find()) {
|
|
return 0;
|
|
}
|
|
|
|
String data = m.group();
|
|
|
|
if (data == null || data.isEmpty()) {
|
|
return 0;
|
|
}
|
|
|
|
data = data.substring(1);
|
|
|
|
return NumberUtil.isShort(data) ? Short.valueOf(data) : 0;
|
|
}
|
|
|
|
/**
|
|
* Returns metadata from a string
|
|
*
|
|
* @param itemName Item name
|
|
* @return Metadata found
|
|
*/
|
|
public static ItemMeta getMetadata(String itemName) {
|
|
Matcher m = METADATA.matcher(itemName);
|
|
|
|
if (!m.find()) {
|
|
return null;
|
|
}
|
|
|
|
String group = m.group().substring(1);
|
|
return Metadata.getFromCode(group);
|
|
}
|
|
|
|
private static class EnumParser<E extends Enum<E>> {
|
|
private E parse(String name, E[] values) {
|
|
try {
|
|
return E.valueOf(values[0].getDeclaringClass(), name.toUpperCase(Locale.ROOT));
|
|
} catch (IllegalArgumentException exception) {
|
|
E currentEnum = null;
|
|
String[] typeParts = name.replaceAll("(?<!^)([A-Z1-9])", "_$1").toUpperCase(Locale.ROOT).split("[ _]");
|
|
int length = Short.MAX_VALUE;
|
|
for (E e : values) {
|
|
String enumName = e.name();
|
|
if (enumName.length() < length && enumName.startsWith(name)) {
|
|
length = (short) enumName.length();
|
|
currentEnum = e;
|
|
} else if (typeParts.length > 1) {
|
|
String[] nameParts = enumName.split("_");
|
|
if (typeParts.length == nameParts.length) {
|
|
boolean matched = true;
|
|
for (int i = 0; i < nameParts.length; i++) {
|
|
if (!nameParts[i].startsWith(typeParts[i])) {
|
|
matched = false;
|
|
break;
|
|
}
|
|
}
|
|
if (matched) {
|
|
currentEnum = e;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return currentEnum;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class Metadata {
|
|
/**
|
|
* Returns the ItemMeta represented by this code
|
|
*
|
|
* @param code Code representing the ItemMeta
|
|
* @return ItemMeta represented by code
|
|
*/
|
|
public static ItemMeta getFromCode(String code) {
|
|
ItemStack item = ChestShop.getItemDatabase().getFromCode(code);
|
|
|
|
if (item == null) {
|
|
return null;
|
|
} else {
|
|
return item.getItemMeta();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the code for this item
|
|
*
|
|
* @param item Item being represented
|
|
* @return Code representing the item
|
|
*/
|
|
public static String getItemCode(ItemStack item) {
|
|
return ChestShop.getItemDatabase().getItemCode(item);
|
|
}
|
|
}
|
|
|
|
public static class Show {
|
|
private static ShowItem showItem = null;
|
|
|
|
/**
|
|
* Lets the class know that it's safe to use the ShowItem methods now
|
|
*
|
|
* @param plugin
|
|
*/
|
|
public static void initialize(Plugin plugin) {
|
|
showItem = (ShowItem) plugin;
|
|
}
|
|
|
|
/**
|
|
* Send a message with hover info and icons
|
|
*
|
|
* @param player The player to send the message to
|
|
* @param message The raw message
|
|
* @param stock The items in stock
|
|
*/
|
|
public static boolean sendMessage(Player player, Messages.Message message, ItemStack[] stock, Map<String, String> replacementMap, String... replacements) {
|
|
return sendMessage(player, player.getName(), message, stock, replacementMap, replacements);
|
|
}
|
|
|
|
/**
|
|
* Send a message with hover info and icons
|
|
*
|
|
* @param player The player to send the message to
|
|
* @param playerName The name of the player in case he is offline and bungee messages are enabled
|
|
* @param message The raw message
|
|
* @param stock The items in stock
|
|
*/
|
|
public static boolean sendMessage(Player player, String playerName, Messages.Message message, ItemStack[] stock, Map<String, String> replacementMap, String... replacements) {
|
|
if (showItem == null) {
|
|
return false;
|
|
}
|
|
|
|
TextComponent.Builder itemComponent = Component.text();
|
|
for (ItemStack item : InventoryUtil.mergeSimilarStacks(stock)) {
|
|
try {
|
|
itemComponent.append(GsonComponentSerializer.gson().deserialize(showItem.getItemConverter().createComponent(item, Level.FINE).toJsonString(player)));
|
|
} catch (Exception e) {
|
|
ChestShop.getPlugin().getLogger().log(Level.WARNING, "Error while trying to send message '" + message + "' to player " + player.getName() + ": " + e.getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Map<String, String> newMap = new LinkedHashMap<>(replacementMap);
|
|
newMap.put("material", "item");
|
|
Component component = new Replacer()
|
|
.placeholderSuffix("")
|
|
.replace("item",itemComponent.build())
|
|
.replaceIn(message.getComponent(player, true, newMap, replacements));
|
|
if (player != null) {
|
|
ChestShop.getAudiences().player(player).sendMessage(component);
|
|
return true;
|
|
} else if (playerName != null) {
|
|
ChestShop.sendBungeeMessage(playerName, component);
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|