ChestShop-3/src/main/java/com/Acrobot/Breeze/Utils/MaterialUtil.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;
}
}
}