/* * Copyright (c) 2015 NOVA, All rights reserved. * This library is free software, licensed under GNU Lesser General Public License version 3 * * This file is part of NOVA. * * NOVA is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * NOVA is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with NOVA. If not, see <http://www.gnu.org/licenses/>. */ package nova.core.recipes.crafting; import nova.core.item.Item; import nova.core.item.ItemFactory; import nova.core.recipes.ingredient.ItemIngredient; import nova.core.util.math.MathUtil; import org.apache.commons.math3.geometry.euclidean.twod.Vector2D; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; /** * Contains a single shaped crafting recipe. Can contain custom crafting logic. * <p> * Crafting recipes can be specified as a 2-dimensional array of ingredients (with Optional.empty() being used for empty spots) * or as a 1-dimensional array of ingredients and a pattern string (lines are separated by -, spaces for empty spots, A-Z for ingredients. * <p> * For instance, to define a stick recipe with a pattern string: * <p> * new ShapedCraftingRecipe("A - A", ItemIngredient.forDictionary("plankWood")); * <p> * Two kinds of recipes can be defined: basic or advanced. Basic recipes always return the same item, while advanced * recipes have their output defined by a lambda expression. RecipeFunctions will receive information about the * @author Stan Hebben */ public class ShapedCraftingRecipe implements CraftingRecipe { /* * This class contains an optimized recipe resolution algorithm. * * This algorithms works as follows: * A: During construction, the 2D ingredients array is transformed to a flat list of ingredients and their positions * B: During recipe resolution, the first non-empty crafting grid spot is searched * C: That position marks the position of the recipe inside the crafting grid (a 2x2 recipe has 4 possible positions in a 3x3 crafting grid) * D: The list of ingredients is run over and filled into an array for later processing * E: If any of the ingredients doesn't match, the crafting is rejected * F: For mirrored recipes B-E are repeated in the mirrored direction */ private final int width; private final int height; private final int[] posx; private final int[] posy; private final boolean mirrored; private final int lastIngredientIndexOnFirstLine; // only actually matters for mirrored recipes private final RecipeFunction recipeFunction; private final ItemFactory output; private final ItemIngredient[] ingredients; /** * Defines a basic structured crafting recipe, using a format string. * @param output Output {@link Item} of the recipe * @param format Format * @param ingredients {@link ItemIngredient ItemIngredients} */ public ShapedCraftingRecipe(ItemFactory output, String format, ItemIngredient... ingredients) { this(output, format, false, ingredients); } /** * Defines a basic structured crafting recipe, possibly mirrored, using a format string. * @param output Output {@link Item} of the recipe * @param format Format * @param mirrored Whether this recipe is mirrored * @param ingredients {@link ItemIngredient ItemIngredients} */ public ShapedCraftingRecipe(ItemFactory output, String format, boolean mirrored, ItemIngredient... ingredients) { this(output, (grid, tagged, o) -> Optional.of(o.build()), format, mirrored, ingredients); } /** * Defines an advanced crafting recipe, using a format string. * @param output Nominal output of the recipe * @param recipeFunction {@link RecipeFunction} * @param format Format * @param ingredients {@link ItemIngredient ItemIngredients} */ public ShapedCraftingRecipe(ItemFactory output, RecipeFunction recipeFunction, String format, ItemIngredient... ingredients) { this(output, recipeFunction, format, false, ingredients); } /** * Defines an advanced crafting recipe, using a format string. * @param output Nominal output of the recipe * @param recipeFunction {@link RecipeFunction} * @param format Format * @param mirrored Whether this recipe is mirrored * @param ingredients {@link ItemIngredient ItemIngredients} */ public ShapedCraftingRecipe(ItemFactory output, RecipeFunction recipeFunction, String format, boolean mirrored, ItemIngredient... ingredients) { this.output = output; String[] formatLines = format.split("\\-"); int numIngredients = 0; int width = 0; for (String formatLine : formatLines) { width = Math.max(width, formatLine.length()); for (char c : formatLine.toCharArray()) { if (c == ' ') { continue; } else if (c >= 'A' && c <= 'Z') { numIngredients++; } else { throw new IllegalArgumentException("Invalid character in format string " + format + ": " + c); } } } this.width = width; this.height = formatLines.length; this.posx = new int[numIngredients]; this.posy = new int[numIngredients]; this.ingredients = new ItemIngredient[numIngredients]; this.mirrored = mirrored; int ingredientIndex = 0; for (int y = 0; y < this.height; y++) { String formatLine = formatLines[y]; for (int x = 0; x < formatLine.length(); x++) { char c = formatLine.charAt(x); if (c == ' ') { continue; } this.posx[ingredientIndex] = x; this.posy[ingredientIndex] = y; this.ingredients[ingredientIndex] = ingredients[c - 'A']; ingredientIndex++; } } this.recipeFunction = recipeFunction; this.lastIngredientIndexOnFirstLine = getLastIngredientIndexOnFirstLine(); } /** * Defines a basic crafting recipe, using a 2D ingredients array. * @param output Output {@link Item} of the recipe * @param ingredients {@link ItemIngredient ItemIngredients} */ public ShapedCraftingRecipe(ItemFactory output, Optional<ItemIngredient>[][] ingredients) { this(output, ingredients, false); } /** * Defines a basic crafting recipe, using a 2D ingredients array. * @param output Output {@link Item} of the recipe * @param ingredients {@link ItemIngredient ItemIngredients} * @param mirrored Whether this recipe is mirrored */ public ShapedCraftingRecipe(ItemFactory output, Optional<ItemIngredient>[][] ingredients, boolean mirrored) { this(output, (grid, tagged, o) -> Optional.of(o.build()), ingredients, mirrored); } /** * Defines a basic crafting recipe, using a 2D ingredients array. * @param output Output {@link Item} of the recipe * @param recipeFunction {@link RecipeFunction} * @param ingredients {@link ItemIngredient ItemIngredients} */ public ShapedCraftingRecipe(ItemFactory output, RecipeFunction recipeFunction, Optional<ItemIngredient>[][] ingredients) { this(output, recipeFunction, ingredients, false); } /** * Defines an advanced crafting recipe, using a 2D ingredients array. * @param output Nominal output of the recipe * @param recipeFunction {@link RecipeFunction} * @param ingredients {@link ItemIngredient ItemIngredients} * @param mirrored Whether this recipe is mirrored */ public ShapedCraftingRecipe(ItemFactory output, RecipeFunction recipeFunction, Optional<ItemIngredient>[][] ingredients, boolean mirrored) { this.output = output; int numIngredients = 0; for (Optional<ItemIngredient>[] row : ingredients) { for (Optional<ItemIngredient> ingredient : row) { if (ingredient.isPresent()) { numIngredients++; } } } if (numIngredients == 0) { throw new IllegalArgumentException("Recipe has no ingredients"); } // translate 2d ingredient array to ingredient list this.posx = new int[numIngredients]; this.posy = new int[numIngredients]; this.ingredients = new ItemIngredient[numIngredients]; this.recipeFunction = recipeFunction; int width1 = 0; int height1 = ingredients.length; int ix = 0; for (int j = 0; j < ingredients.length; j++) { Optional<ItemIngredient>[] row = ingredients[j]; width1 = Math.max(width1, row.length); for (int i = 0; i < row.length; i++) { if (row[i].isPresent()) { this.posx[ix] = (byte) i; this.posy[ix] = (byte) j; this.ingredients[ix] = row[i].get(); ix++; } } } this.width = width1; this.height = height1; this.mirrored = mirrored; this.lastIngredientIndexOnFirstLine = getLastIngredientIndexOnFirstLine(); } public int getWidth() { return width; } public int getHeight() { return height; } public boolean isMirrored() { return mirrored; } public ItemIngredient[] getIngredients() { return ingredients; } public int[] getIngredientsX() { return posx; } public int[] getIngredientsY() { return posy; } @Override public boolean matches(CraftingGrid inventory) { return findIngredientMapping(inventory) != null; } @Override public Optional<Item> getCraftingResult(CraftingGrid craftingGrid) { ShapedMapping mapping = findIngredientMapping(craftingGrid); if (mapping == null) { return Optional.empty(); } return getRecipeOutput(craftingGrid, mapping); } @Override public void consumeItems(CraftingGrid craftingGrid) { ShapedMapping mapping = findIngredientMapping(craftingGrid); if (mapping == null) { return; } for (int i = 0; i < ingredients.length; i++) { Item original = mapping.items[i]; Optional<Item> consumed = ingredients[i].consumeOnCrafting(original, craftingGrid); Objects.requireNonNull(consumed, "The result of 'ItemIngredient.consumeOnCrafting' can't be null"); mapping.setStack(craftingGrid, i, consumed.filter(item -> item.count() > 0)); } } @Override public Collection<String> getPossibleItemsInFirstSlot() { if (isMirrored()) { Collection<String> optionsForFirstItem = ingredients[0].getPossibleItemIds(); if (optionsForFirstItem.isEmpty()) { return Collections.emptyList(); } Collection<String> optionsForSecondItem = ingredients[lastIngredientIndexOnFirstLine].getPossibleItemIds(); if (optionsForFirstItem.isEmpty()) { return Collections.emptyList(); } Set<String> result = new HashSet<>(); result.addAll(optionsForFirstItem); result.addAll(optionsForSecondItem); return result; } else { return ingredients[0].getPossibleItemIds(); } } @Override public Optional<Item> getExampleOutput() { return Optional.of(output.build()); } // ####################### // ### Private methods ### // ####################### private int getLastIngredientIndexOnFirstLine() { if (ingredients.length == 0) { return -1; } int firstLineIndex = Arrays.stream(posy).min().orElse(0); int result = 0; for (int i = 0; i < ingredients.length; i++) { if (posy[i] == firstLineIndex) { result = i; } } return result; } private ShapedMapping findIngredientMapping(CraftingGrid craftingGrid) { if (ingredients.length == 0) { return null; } if (craftingGrid.countFilledStacks() != ingredients.length) { return null; } ShapedMapping mapping = findIngredientMapping(craftingGrid, false); if (mapping == null && isMirrored()) { mapping = findIngredientMapping(craftingGrid, true); } return mapping; } private ShapedMapping findIngredientMapping(CraftingGrid craftingGrid, boolean mirrored) { Optional<Vector2D> optOffset = craftingGrid.getFirstNonEmptyPosition(); if (!optOffset.isPresent()) { return null; } ShapedMapping mapping; if (mirrored) { mapping = new MirroredMapping(optOffset.get()); } else { mapping = new NonMirroredMapping(optOffset.get()); } if (!mapping.fitsInCraftingGrid(craftingGrid)) { return null; } for (int i = 0; i < ingredients.length; i++) { Optional<Item> item = mapping.getStack(craftingGrid, i); if (!item.isPresent()) { return null; } if (!ingredients[i].matches(item.get())) { return null; } mapping.items[i] = item.get(); } return mapping; } private Optional<Item> getRecipeOutput( CraftingGrid craftingGrid, ShapedMapping shapedMapping) { Map<String, Item> tagged = new HashMap<>(); for (int k = 0; k < ingredients.length; k++) { if (ingredients[k].getTag().isPresent()) { tagged.put(ingredients[k].getTag().get(), shapedMapping.items[k]); } } return recipeFunction.doCrafting(craftingGrid, tagged, output); } private abstract class ShapedMapping { public final int offsetX; public final int offsetY; public final Item[] items; private ShapedMapping(Vector2D offset) { this.offsetX = (int) offset.getX(); this.offsetY = (int) offset.getY(); this.items = new Item[ingredients.length]; } public boolean fitsInCraftingGrid(CraftingGrid craftingGrid) { return offsetX >= 0 && offsetX + getWidth() <= craftingGrid.getWidth() && offsetY >= 0 && offsetY + getHeight() <= craftingGrid.getHeight(); } public abstract Optional<Item> getStack(CraftingGrid craftingGrid, int ingredient); public abstract void setStack(CraftingGrid craftingGrid, int ingredient, Optional<Item> value); @Override public String toString() { return String.format("%s{offsetX=%s, offsetY=%s, items=%s}", getClass().getSimpleName(), offsetX, offsetY, Arrays.toString(items)); } } private class NonMirroredMapping extends ShapedMapping { private NonMirroredMapping(Vector2D firstItemOffset) { super(new Vector2D( firstItemOffset.getX() - posx[0], firstItemOffset.getY() - posy[0])); } @Override public Optional<Item> getStack(CraftingGrid craftingGrid, int ingredient) { return craftingGrid.getCrafting( offsetX + posx[ingredient], offsetY + posy[ingredient]); } @Override public void setStack(CraftingGrid craftingGrid, int ingredient, Optional<Item> value) { craftingGrid.setCrafting( offsetX + posx[ingredient], offsetY + posy[ingredient], value); } } private class MirroredMapping extends ShapedMapping { private MirroredMapping(Vector2D firstItemOffset) { super(new Vector2D( firstItemOffset.getX() + posx[lastIngredientIndexOnFirstLine] - Arrays.stream(posx).max().orElse(0), firstItemOffset.getY() - posy[0])); } @Override public Optional<Item> getStack(CraftingGrid craftingGrid, int ingredient) { return craftingGrid.getCrafting( offsetX + getWidth() - posx[ingredient] - 1, offsetY + posy[ingredient]); } @Override public void setStack(CraftingGrid craftingGrid, int ingredient, Optional<Item> value) { craftingGrid.setCrafting( offsetX + getWidth() - posx[ingredient] - 1, offsetY + posy[ingredient], value); } } }