package net.knarcraft.blacksmith.util; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.Recipe; import org.bukkit.inventory.ShapedRecipe; import org.bukkit.inventory.ShapelessRecipe; import org.bukkit.inventory.meta.ArmorMeta; import org.bukkit.inventory.meta.trim.ArmorTrim; import org.bukkit.inventory.meta.trim.TrimMaterial; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; /** * A helper class for deciding the salvage returned if salvaging an item */ public final class SalvageHelper { private static final Random random = new Random(); private static final Map trimMaterialToMaterial; static { trimMaterialToMaterial = new HashMap<>(); trimMaterialToMaterial.put(TrimMaterial.AMETHYST, Material.AMETHYST_SHARD); trimMaterialToMaterial.put(TrimMaterial.COPPER, Material.COPPER_INGOT); trimMaterialToMaterial.put(TrimMaterial.DIAMOND, Material.DIAMOND); trimMaterialToMaterial.put(TrimMaterial.EMERALD, Material.EMERALD); trimMaterialToMaterial.put(TrimMaterial.GOLD, Material.GOLD_INGOT); trimMaterialToMaterial.put(TrimMaterial.IRON, Material.IRON_INGOT); trimMaterialToMaterial.put(TrimMaterial.LAPIS, Material.LAPIS_LAZULI); trimMaterialToMaterial.put(TrimMaterial.NETHERITE, Material.NETHERITE_INGOT); trimMaterialToMaterial.put(TrimMaterial.QUARTZ, Material.QUARTZ); trimMaterialToMaterial.put(TrimMaterial.REDSTONE, Material.REDSTONE); } /** * Gets salvage for the given armor trim * * @param item

The item to have its armor trim salvaged

* @param armorMeta

The armor meta of the item to salvage

* @return

The salvage, or null if salvage could not be calculated

*/ @Nullable public static List getTrimSalvage(@NotNull ItemStack item, @NotNull ArmorMeta armorMeta) { ArmorTrim armorTrim = armorMeta.getTrim(); if (armorTrim == null) { return null; } List result = new ArrayList<>(); Material trimMaterial = trimMaterialToMaterial.get(armorTrim.getMaterial()); if (trimMaterial != null) { result.add(new ItemStack(trimMaterial, 1)); } else { return null; } Material patternMaterial = Material.matchMaterial(armorTrim.getPattern().getKey().getKey() + "_ARMOR_TRIM_SMITHING_TEMPLATE"); if (patternMaterial != null) { result.add(new ItemStack(patternMaterial, 1)); } else { return null; } // Return a clone of the input item, with the armor trim removed ItemStack strippedItem = item.clone(); armorMeta.setTrim(null); strippedItem.setItemMeta(armorMeta); result.add(strippedItem); return result; } /** * Gets whether the given item has a valid crafting recipe * * @param item

The item to check

* @return

True if the item has a valid crafting recipe

*/ public static boolean hasRecipe(@NotNull ItemStack item) { List recipes = Bukkit.getRecipesFor(new ItemStack(item.getType(), item.getAmount())); for (Recipe recipe : recipes) { if (recipe instanceof ShapedRecipe || recipe instanceof ShapelessRecipe) { return true; } } return false; } /** * Gets the sum of all enchantment levels for the given item * * @param item

The item to check

* @return

The total amount of enchantment levels on the item

*/ public static int getTotalEnchantmentLevels(@NotNull ItemStack item) { int expLevels = 0; for (Enchantment enchantment : item.getEnchantments().keySet()) { expLevels += item.getEnchantmentLevel(enchantment); } return expLevels; } /** * Gets the items to return if salvaging the given item stack * *

Note: Only items craft-able in a crafting table are salvageable. Netherite gear is therefore not salvageable.

* * @param server

The server to get recipes from

* @param salvagedItem

The item stack to salvage

* @param trashSalvage

Any material treated as trash salvage

* @param extended

Whether to enable extended salvage, ignoring the repairable restriction

* @return

The items to return to the user, or null if not salvageable

*/ public static @Nullable List getSalvage(@NotNull Server server, @Nullable ItemStack salvagedItem, @NotNull Collection trashSalvage, boolean extended) { if (salvagedItem == null || salvagedItem.getAmount() < 1 || (!extended && !ItemHelper.isRepairable(salvagedItem))) { return null; } for (Recipe recipe : server.getRecipesFor(new ItemStack(salvagedItem.getType(), salvagedItem.getAmount()))) { if (recipe instanceof ShapedRecipe || recipe instanceof ShapelessRecipe) { List salvage = getRecipeSalvage(recipe, salvagedItem, trashSalvage); if (salvage != null && !salvage.isEmpty()) { return salvage; } } } return null; } /** * Gets the salvage resulting from the given recipe and the given item * * @param recipe

The recipe to get salvage for

* @param salvagedItem

The item to be salvaged

* @param trashSalvage

Any material treated as trash salvage

* @return

A list of items, or null if not a valid type of recipe

*/ private static @Nullable List getRecipeSalvage(@NotNull Recipe recipe, @NotNull ItemStack salvagedItem, @NotNull Collection trashSalvage) { List ingredients; if (recipe instanceof ShapedRecipe shapedRecipe) { ingredients = getIngredients(shapedRecipe); } else if (recipe instanceof ShapelessRecipe shapelessRecipe) { ingredients = shapelessRecipe.getIngredientList(); } else { //Recipes other than crafting shouldn't be considered for salvaging return null; } //Make things easier by eliminating identical stacks ingredients = combineStacks(ingredients); return combineStacks(getSalvage(copyItems(ingredients), salvagedItem, trashSalvage)); } /** * Copies a list of items * *

Note: This does not copy any metadata. It only copies the item type and the amount.

* * @param itemsToCopy

The items to make a copy of

* @return

A copy of the given items

*/ private static @NotNull List copyItems(@NotNull List itemsToCopy) { List copies = new ArrayList<>(itemsToCopy.size()); for (ItemStack itemStack : itemsToCopy) { copies.add(new ItemStack(itemStack.getType(), itemStack.getAmount())); } return copies; } /** * Gets the salvage resulting from the given item, and the given recipe items * * @param recipeItems

All items required for crafting the item to salvage

* @param salvagedItem

The item to be salvaged

* @param trashSalvage

The types of materials considered trash for this recipe

* @return

The items to be returned to the user as salvage

*/ @NotNull private static List getSalvage(@NotNull List recipeItems, @NotNull ItemStack salvagedItem, @NotNull Collection trashSalvage) { int durability = ItemHelper.getDurability(salvagedItem); int maxDurability = ItemHelper.getMaxDurability(salvagedItem); // Prevent divide by zero for items that don't have a set max durability if (maxDurability == 0) { maxDurability = 1; durability = 1; } double percentageRemaining = (double) durability / maxDurability; int totalItems = totalItemAmount(recipeItems); //Get the amount of recipe items to be returned int itemsToReturn = (int) Math.floor(percentageRemaining * totalItems); int bound = recipeItems.size(); List goodItems = copyItems(recipeItems); goodItems.removeIf((item) -> trashSalvage.contains(item.getType())); int goodSalvage = totalItemAmount(goodItems); List salvage = new ArrayList<>(); for (int i = 0; i < itemsToReturn; i++) { // Pick random item int itemIndex = SalvageHelper.random.nextInt(bound); ItemStack itemStack = recipeItems.get(itemIndex); // The selected item is trash, so skip it if (trashSalvage.contains(itemStack.getType()) && i < goodSalvage) { i--; continue; } //Make sure to never give more of one item than the amount which exists in the recipe if (itemStack.getAmount() <= 0) { i--; continue; } itemStack.setAmount(itemStack.getAmount() - 1); salvage.add(new ItemStack(itemStack.getType(), 1)); } return salvage; } /** * Gets the total sum of items in the given list of items * * @param items

The items to get the sum of

* @return

The total number of items

*/ private static int totalItemAmount(@NotNull List items) { int sum = 0; for (ItemStack itemStack : items) { sum += itemStack.getAmount(); } return sum; } /** * Combines all items of the same type in the given list * *

Basically, if the input is two item stacks containing one diamond each, the output will be an item stack with * two diamonds instead.

* * @param items

The items to combine

* @return

The given items, but combined

*/ private static @NotNull List combineStacks(@NotNull List items) { Map itemAmounts = new HashMap<>(); for (ItemStack item : items) { Material itemType = item.getType(); itemAmounts.put(itemType, itemAmounts.getOrDefault(itemType, 0) + 1); } List combined = new ArrayList<>(); for (Material material : itemAmounts.keySet()) { combined.add(new ItemStack(material, itemAmounts.get(material))); } return combined; } /** * Gets all ingredients contained in the given shaped recipe * * @param shapedRecipe

The shaped recipe to get ingredients for

* @return

The items contained in the recipe

*/ @NotNull private static List getIngredients(@NotNull ShapedRecipe shapedRecipe) { List ingredients = new ArrayList<>(); Map ingredientMap = shapedRecipe.getIngredientMap(); //The shape is a list of the three rows' strings. Each string contains 3 characters. String[] shape = shapedRecipe.getShape(); for (String row : shape) { for (int column = 0; column < row.length(); column++) { ItemStack item = ingredientMap.get(row.charAt(column)); if (item != null && item.getType() != Material.AIR && item.getAmount() > 0) { ingredients.add(item); } } } return ingredients; } }