/* * @file Recipes.java * @author Stefan Wilhelm (wile) * @copyright (C) 2020 Stefan Wilhelm * @license MIT (see https://opensource.org/licenses/MIT) * * Recipe utility functionality. */ package dev.zontreck.libzontreck.edlibmc; import net.minecraft.core.NonNullList; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import net.minecraft.util.Tuple; import net.minecraft.world.Container; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.StackedContents; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.CraftingContainer; import net.minecraft.world.item.EnchantedBookItem; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.*; import net.minecraft.world.item.enchantment.Enchantment; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.item.enchantment.EnchantmentInstance; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.ComposterBlock; import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.common.brewing.BrewingRecipeRegistry; import javax.annotation.Nullable; import java.util.*; import java.util.function.BiPredicate; public class Crafting { // ------------------------------------------------------------------------------------------------------------------- /** * Returns a Crafting recipe by registry name. */ public static Optional getCraftingRecipe(Level world, ResourceLocation recipe_id) { Recipe recipe = world.getRecipeManager().byKey(recipe_id).orElse(null); return (recipe instanceof CraftingRecipe) ? Optional.of((CraftingRecipe) recipe) : Optional.empty(); } /** * Returns a list of matching recipes by the first N slots (crafting grid slots) of the given inventory. */ public static List get3x3CraftingRecipes(Level world, Container crafting_grid_slots) { return CraftingGrid.instance3x3.getRecipes(world, crafting_grid_slots); } /** * Returns a recipe by the first N slots (crafting grid slots). */ public static Optional get3x3CraftingRecipe(Level world, Container crafting_grid_slots) { return get3x3CraftingRecipes(world, crafting_grid_slots).stream().findFirst(); } /** * Returns the result item of the recipe with the given grid layout. */ public static ItemStack get3x3CraftingResult(Level world, Container grid, CraftingRecipe recipe) { return CraftingGrid.instance3x3.getCraftingResult(world, grid, recipe); } /** * Returns the items remaining in the grid after crafting 3x3. */ public static List get3x3RemainingItems(Level world, Container grid, CraftingRecipe recipe) { return CraftingGrid.instance3x3.getRemainingItems(world, grid, recipe); } public static List get3x3Placement(Level world, CraftingRecipe recipe, Container item_inventory, @Nullable Container crafting_grid) { final int width = 3; final int height = 3; if (!recipe.canCraftInDimensions(width, height)) return Collections.emptyList(); List used = new ArrayList<>(); //NonNullList.withSize(width*height); for (int i = width * height; i > 0; --i) used.add(ItemStack.EMPTY); Container check_inventory = Inventories.copyOf(item_inventory); Inventories.InventoryRange source = new Inventories.InventoryRange(check_inventory); final List ingredients = recipe.getIngredients(); final List preferred = new ArrayList<>(width * height); if (crafting_grid != null) { for (int i = 0; i < crafting_grid.getContainerSize(); ++i) { ItemStack stack = crafting_grid.getItem(i); if (stack.isEmpty()) continue; stack = stack.copy(); stack.setCount(1); if (!source.extract(stack).isEmpty()) preferred.add(stack); } } for (int i = 0; i < ingredients.size(); ++i) { final Ingredient ingredient = ingredients.get(i); if (ingredient == Ingredient.EMPTY) continue; ItemStack stack = preferred.stream().filter(ingredient).findFirst().orElse(ItemStack.EMPTY); if (!stack.isEmpty()) { preferred.remove(stack); } else { stack = source.stream().filter(ingredient).findFirst().orElse(ItemStack.EMPTY); if (stack.isEmpty()) return Collections.emptyList(); stack = stack.copy(); stack.setCount(1); if (source.extract(stack).isEmpty()) return Collections.emptyList(); } used.set(i, stack); } if (recipe instanceof ShapedRecipe shaped) { List placement = NonNullList.withSize(width * height, ItemStack.EMPTY); for (int row = 0; row < shaped.getRecipeHeight(); ++row) { for (int col = 0; col < shaped.getRecipeWidth(); ++col) { placement.set(width * row + col, used.get(row * shaped.getRecipeWidth() + col)); } } return placement; } else { return used; } } /** * Returns the recipe for a given input stack to smelt, null if there is no recipe * for the given type (SMELTING,BLASTING,SMOKING, etc). */ public static > Optional getFurnaceRecipe(RecipeType recipe_type, Level world, ItemStack input_stack) { if (input_stack.isEmpty()) { return Optional.empty(); } else if (recipe_type == RecipeType.SMELTING) { SimpleContainer inventory = new SimpleContainer(3); inventory.setItem(0, input_stack); SmeltingRecipe recipe = world.getRecipeManager().getRecipeFor(RecipeType.SMELTING, inventory, world).orElse(null); return (recipe == null) ? Optional.empty() : Optional.of(recipe); } else if (recipe_type == RecipeType.BLASTING) { SimpleContainer inventory = new SimpleContainer(3); inventory.setItem(0, input_stack); BlastingRecipe recipe = world.getRecipeManager().getRecipeFor(RecipeType.BLASTING, inventory, world).orElse(null); return (recipe == null) ? Optional.empty() : Optional.of(recipe); } else if (recipe_type == RecipeType.SMOKING) { SimpleContainer inventory = new SimpleContainer(3); inventory.setItem(0, input_stack); SmokingRecipe recipe = world.getRecipeManager().getRecipeFor(RecipeType.SMOKING, inventory, world).orElse(null); return (recipe == null) ? Optional.empty() : Optional.of(recipe); } else { return Optional.empty(); } } // ------------------------------------------------------------------------------------------------------------------- public static > int getSmeltingTimeNeeded(RecipeType recipe_type, Level world, ItemStack stack) { if (stack.isEmpty()) return 0; final int t = getFurnaceRecipe(recipe_type, world, stack).map((AbstractCookingRecipe::getCookingTime)).orElse(0); return (t <= 0) ? 200 : t; } /** * Returns the burn time of an item when used as fuel, 0 if it is no fuel. */ public static int getFuelBurntime(Level world, ItemStack stack) { if (stack.isEmpty()) return 0; int t = ForgeHooks.getBurnTime(stack, null); return Math.max(t, 0); } /** * Returns true if an item can be used as fuel. */ public static boolean isFuel(Level world, ItemStack stack) { return (getFuelBurntime(world, stack) > 0) || (stack.getItem() == Items.LAVA_BUCKET); } /** * Returns burntime and remaining stack then the item shall be used as fuel. */ public static Tuple consumeFuel(Level world, ItemStack stack) { if (stack.isEmpty()) return new Tuple<>(0, stack); int burnime = getFuelBurntime(world, stack); if ((stack.getItem() == Items.LAVA_BUCKET)) { if (burnime <= 0) burnime = 1000 * 20; return new Tuple<>(burnime, new ItemStack(Items.BUCKET)); } else if (burnime <= 0) { return new Tuple<>(0, stack); } else { ItemStack left_over = stack.copy(); left_over.shrink(1); return new Tuple<>(burnime, left_over); } } /** * Returns true if the item can be used as brewing fuel. */ public static boolean isBrewingFuel(Level world, ItemStack stack) { return (stack.getItem() == Items.BLAZE_POWDER) || (stack.getItem() == Items.BLAZE_ROD); } // ------------------------------------------------------------------------------------------------------------------- /** * Returns true if the item can be used as brewing ingredient. */ public static boolean isBrewingIngredient(Level world, ItemStack stack) { return BrewingRecipeRegistry.isValidIngredient(stack); } /** * Returns true if the item can be used as brewing bottle. */ public static boolean isBrewingInput(Level world, ItemStack stack) { return BrewingRecipeRegistry.isValidInput(stack); } /** * Returns the burn time for brewing of the given stack. */ public static int getBrewingFuelBurntime(Level world, ItemStack stack) { if (stack.isEmpty()) return 0; if (stack.getItem() == Items.BLAZE_POWDER) return (400 * 20); if (stack.getItem() == Items.BLAZE_ROD) return (400 * 40); return 0; } /** * Returns brewing burn time and remaining stack if the item shall be used as fuel. */ public static Tuple consumeBrewingFuel(Level world, ItemStack stack) { int burntime = getBrewingFuelBurntime(world, stack); if (burntime <= 0) return new Tuple<>(0, stack.copy()); stack = stack.copy(); stack.shrink(1); return new Tuple<>(burntime, stack.isEmpty() ? ItemStack.EMPTY : stack); } public static double getCompostingChance(ItemStack stack) { return ComposterBlock.COMPOSTABLES.getOrDefault(stack.getItem(), 0); } /** * Returns the enchtments bound to the given stack. */ public static Map getEnchantmentsOnItem(Level world, ItemStack stack) { return (stack.isEmpty() || (stack.getTag() == null)) ? Collections.emptyMap() : EnchantmentHelper.getEnchantments(stack); } // ------------------------------------------------------------------------------------------------------------------- /** * Returns an enchanted book with the given enchantment, emtpy stack if not applicable. */ public static ItemStack getEnchantmentBook(Level world, Enchantment enchantment, int level) { return ((!enchantment.isAllowedOnBooks()) || (level <= 0)) ? ItemStack.EMPTY : EnchantedBookItem.createForEnchantment(new EnchantmentInstance(enchantment, level)); } // ------------------------------------------------------------------------------------------------------------------- /** * Returns the accumulated repair cost for the given enchantments. */ public static int getEnchantmentRepairCost(Level world, Map enchantments) { int repair_cost = 0; for (Map.Entry e : enchantments.entrySet()) repair_cost = repair_cost * 2 + 1; // @see: RepairContainer.getNewRepairCost() return repair_cost; } /** * Trys to add an enchtment to the given stack, returns boolean success. */ public static boolean addEnchantmentOnItem(Level world, ItemStack stack, Enchantment enchantment, int level) { if (stack.isEmpty() || (level <= 0) || (!stack.isEnchantable()) || (level >= enchantment.getMaxLevel())) return false; final Map on_item = getEnchantmentsOnItem(world, stack); if (on_item.keySet().stream().anyMatch(ench -> ench.isCompatibleWith(enchantment))) return false; final ItemStack book = EnchantedBookItem.createForEnchantment(new EnchantmentInstance(enchantment, level)); if ((!(stack.isBookEnchantable(book) && enchantment.isAllowedOnBooks())) && (!stack.canApplyAtEnchantingTable(enchantment)) && (!enchantment.canEnchant(stack))) return false; final int existing_level = on_item.getOrDefault(enchantment, 0); if (existing_level > 0) level = Mth.clamp(level + existing_level, 1, enchantment.getMaxLevel()); on_item.put(enchantment, level); EnchantmentHelper.setEnchantments(on_item, stack); stack.setRepairCost(getEnchantmentRepairCost(world, on_item)); return true; } /** * Removes enchantments from a stack, returns the removed enchantments. */ public static Map removeEnchantmentsOnItem(Level world, ItemStack stack, BiPredicate filter) { if (stack.isEmpty()) return Collections.emptyMap(); final Map on_item = getEnchantmentsOnItem(world, stack); final Map removed = new HashMap<>(); for (Map.Entry e : on_item.entrySet()) { if (filter.test(e.getKey(), e.getValue())) { removed.put(e.getKey(), e.getValue()); } } for (Enchantment e : removed.keySet()) { on_item.remove(e); } EnchantmentHelper.setEnchantments(on_item, stack); stack.setRepairCost(getEnchantmentRepairCost(world, on_item)); return removed; } public static final class CraftingGrid implements CraftingContainer { private static final CraftingGrid instance3x3 = new CraftingGrid(3, 3); final int _width; private CraftingGrid(int width, int height) { _width=width; } private void fill(Container grid) { for (int i = 0; i < getContainerSize(); ++i) setItem(i, i >= grid.getContainerSize() ? ItemStack.EMPTY : grid.getItem(i)); } public List getRecipes(Level world, Container grid) { fill(grid); return world.getRecipeManager().getRecipesFor(RecipeType.CRAFTING, this, world); } public List getRemainingItems(Level world, Container grid, CraftingRecipe recipe) { fill(grid); return recipe.getRemainingItems(this); } public ItemStack getCraftingResult(Level world, Container grid, CraftingRecipe recipe) { fill(grid); return recipe.assemble(this, world.registryAccess()); } @Override public int getWidth() { return _width; } @Override public int getHeight() { return 0; } @Override public List getItems() { return null; } @Override public int getContainerSize() { return 0; } @Override public boolean isEmpty() { return false; } @Override public ItemStack getItem(int i) { return null; } @Override public ItemStack removeItem(int i, int i1) { return null; } @Override public ItemStack removeItemNoUpdate(int i) { return null; } @Override public void setItem(int i, ItemStack itemStack) { } @Override public void setChanged() { } @Override public boolean stillValid(Player player) { return false; } @Override public void clearContent() { } @Override public void fillStackedContents(StackedContents stackedContents) { } } public static final class BrewingOutput { public static final int DEFAULT_BREWING_TIME = 400; public static final BrewingOutput EMPTY = new BrewingOutput(ItemStack.EMPTY, new SimpleContainer(1), new SimpleContainer(1), 0, 0, DEFAULT_BREWING_TIME); public final ItemStack item; public final Container potionInventory; public final Container ingredientInventory; public final int potionSlot; public final int ingredientSlot; public final int brewTime; public BrewingOutput(ItemStack output_potion, Container potion_inventory, Container ingredient_inventory, int potion_slot, int ingredient_slot, int time_needed) { item = output_potion; potionInventory = potion_inventory; ingredientInventory = ingredient_inventory; potionSlot = potion_slot; ingredientSlot = ingredient_slot; brewTime = time_needed; } public static BrewingOutput find(Level world, Container potion_inventory, Container ingredient_inventory) { for (int potion_slot = 0; potion_slot < potion_inventory.getContainerSize(); ++potion_slot) { final ItemStack pstack = potion_inventory.getItem(potion_slot); if (!isBrewingInput(world, pstack)) continue; for (int ingredient_slot = 0; ingredient_slot < ingredient_inventory.getContainerSize(); ++ingredient_slot) { final ItemStack istack = ingredient_inventory.getItem(ingredient_slot); if ((!isBrewingIngredient(world, istack)) || (ingredient_slot == potion_slot) || (isBrewingFuel(world, istack))) continue; final ItemStack result = BrewingRecipeRegistry.getOutput(pstack, istack); if (result.isEmpty()) continue; return new BrewingOutput(result, potion_inventory, ingredient_inventory, potion_slot, ingredient_slot, DEFAULT_BREWING_TIME); } } return BrewingOutput.EMPTY; } } }