package cn.nukkit.inventory; import cn.nukkit.item.Item; import cn.nukkit.utils.Utils; import io.netty.util.collection.CharObjectHashMap; import java.util.*; /** * author: MagicDroidX * Nukkit Project */ public class ShapedRecipe implements CraftingRecipe { private String recipeId; private final Item primaryResult; private final List<Item> extraResults = new ArrayList<>(); private final List<Item> ingredientsAggregate; private long least,most; private final String[] shape; private final int priority; private final CharObjectHashMap<Item> ingredients = new CharObjectHashMap<>(); public ShapedRecipe(Item primaryResult, String[] shape, Map<Character, Item> ingredients, List<Item> extraResults) { this(null, 1, primaryResult, shape, ingredients, extraResults); } /** * Constructs a ShapedRecipe instance. * * @param primaryResult Primary result of the recipe * @param shape<br> Array of 1, 2, or 3 strings representing the rows of the recipe. * This accepts an array of 1, 2 or 3 strings. Each string should be of the same length and must be at most 3 * characters long. Each character represents a unique type of ingredient. Spaces are interpreted as air. * @param ingredients<br> Char => Item map of items to be set into the shape. * This accepts an array of Items, indexed by character. Every unique character (except space) in the shape * array MUST have a corresponding item in this list. Space character is automatically treated as air. * @param extraResults<br> List of additional result items to leave in the crafting grid afterwards. Used for things like cake recipe * empty buckets. * <p> * Note: Recipes **do not** need to be square. Do NOT add padding for empty rows/columns. */ public ShapedRecipe(String recipeId, int priority, Item primaryResult, String[] shape, Map<Character, Item> ingredients, List<Item> extraResults) { this.recipeId = recipeId; this.priority = priority; int rowCount = shape.length; if (rowCount > 3 || rowCount <= 0) { throw new RuntimeException("Shaped recipes may only have 1, 2 or 3 rows, not " + rowCount); } int columnCount = shape[0].length(); if (columnCount > 3 || columnCount <= 0) { throw new RuntimeException("Shaped recipes may only have 1, 2 or 3 columns, not " + columnCount); } //for($shape as $y => $row) { for (String row : shape) { if (row.length() != columnCount) { throw new RuntimeException("Shaped recipe rows must all have the same length (expected " + columnCount + ", got " + row.length() + ")"); } for (int x = 0; x < columnCount; ++x) { char c = row.charAt(x); if (c != ' ' && !ingredients.containsKey(c)) { throw new RuntimeException("No item specified for symbol '" + c + "'"); } } } this.primaryResult = primaryResult.clone(); this.extraResults.addAll(extraResults); this.shape = shape; for (Map.Entry<Character, Item> entry : ingredients.entrySet()) { this.setIngredient(entry.getKey(), entry.getValue()); } this.ingredientsAggregate = new ArrayList<>(); for (char c : String.join("", this.shape).toCharArray()) { if (c == ' ') continue; Item ingredient = this.ingredients.get(c).clone(); for (Item existingIngredient : this.ingredientsAggregate) { if (existingIngredient.equals(ingredient, ingredient.hasMeta(), ingredient.hasCompoundTag())) { existingIngredient.setCount(existingIngredient.getCount() + ingredient.getCount()); ingredient = null; break; } } if (ingredient != null) this.ingredientsAggregate.add(ingredient); } this.ingredientsAggregate.sort(CraftingManager.recipeComparator); } public int getWidth() { return this.shape[0].length(); } public int getHeight() { return this.shape.length; } @Override public Item getResult() { return this.primaryResult; } @Override public String getRecipeId() { return this.recipeId; } @Override public UUID getId() { return new UUID(least, most); } @Override public void setId(UUID uuid) { this.least = uuid.getLeastSignificantBits(); this.most = uuid.getMostSignificantBits(); if (this.recipeId == null) { this.recipeId = getId().toString(); } } public ShapedRecipe setIngredient(String key, Item item) { return this.setIngredient(key.charAt(0), item); } public ShapedRecipe setIngredient(char key, Item item) { if (String.join("", this.shape).indexOf(key) < 0) { throw new RuntimeException("Symbol does not appear in the shape: " + key); } this.ingredients.put(key, item); return this; } public List<Item> getIngredientList() { List<Item> items = new ArrayList<>(); for (int y = 0, y2 = getHeight(); y < y2; ++y) { for (int x = 0, x2 = getWidth(); x < x2; ++x) { items.add(getIngredient(x, y)); } } return items; } public Map<Integer, Map<Integer, Item>> getIngredientMap() { Map<Integer, Map<Integer, Item>> ingredients = new LinkedHashMap<>(); for (int y = 0, y2 = getHeight(); y < y2; ++y) { Map<Integer, Item> m = new LinkedHashMap<>(); for (int x = 0, x2 = getWidth(); x < x2; ++x) { m.put(x, getIngredient(x, y)); } ingredients.put(y, m); } return ingredients; } public Item getIngredient(int x, int y) { Item item = this.ingredients.get(this.shape[y].charAt(x)); return item != null ? item.clone() : Item.get(Item.AIR); } public String[] getShape() { return shape; } @Override public void registerToCraftingManager(CraftingManager manager) { manager.registerShapedRecipe(this); } @Override public RecipeType getType() { return RecipeType.SHAPED; } @Override public List<Item> getExtraResults() { return extraResults; } @Override public List<Item> getAllResults() { List<Item> list = new ArrayList<>(); list.add(primaryResult); list.addAll(extraResults); return list; } @Override public int getPriority() { return this.priority; } public boolean matchItems(List<Item> inputList, List<Item> extraOutputList, int multiplier) { List<Item> haveInputs = new ArrayList<>(); for (Item item : inputList) { if (item.isNull()) continue; haveInputs.add(item.clone()); } List<Item> needInputs = new ArrayList<>(); if(multiplier != 1){ for (Item item : ingredientsAggregate) { if (item.isNull()) continue; Item itemClone = item.clone(); itemClone.setCount(itemClone.getCount() * multiplier); needInputs.add(itemClone); } } else { for (Item item : ingredientsAggregate) { if (item.isNull()) continue; needInputs.add(item.clone()); } } if (!matchItemList(haveInputs, needInputs)) { return false; } List<Item> haveOutputs = new ArrayList<>(); for (Item item : extraOutputList) { if (item.isNull()) continue; haveOutputs.add(item.clone()); } haveOutputs.sort(CraftingManager.recipeComparator); List<Item> needOutputs = new ArrayList<>(); if(multiplier != 1){ for (Item item : getExtraResults()) { if (item.isNull()) continue; Item itemClone = item.clone(); itemClone.setCount(itemClone.getCount() * multiplier); needOutputs.add(itemClone); } } else { for (Item item : getExtraResults()) { if (item.isNull()) continue; needOutputs.add(item.clone()); } } needOutputs.sort(CraftingManager.recipeComparator); return this.matchItemList(haveOutputs, needOutputs); } /** * Returns whether the specified list of crafting grid inputs and outputs matches this recipe. Outputs DO NOT * include the primary result item. * * @param inputList list of items taken from the crafting grid * @param extraOutputList list of items put back into the crafting grid (secondary results) * @return bool */ @Override public boolean matchItems(List<Item> inputList, List<Item> extraOutputList) { return matchItems(inputList, extraOutputList, 1); } private boolean matchItemList(List<Item> haveItems, List<Item> needItems) { for (Item needItem : new ArrayList<>(needItems)) { for (Item haveItem : new ArrayList<>(haveItems)) { if (needItem.equals(haveItem, needItem.hasMeta(), needItem.hasCompoundTag())) { int amount = Math.min(haveItem.getCount(), needItem.getCount()); needItem.setCount(needItem.getCount() - amount); haveItem.setCount(haveItem.getCount() - amount); if (haveItem.getCount() == 0) { haveItems.remove(haveItem); } if (needItem.getCount() == 0) { needItems.remove(needItem); break; } } } } return haveItems.isEmpty() && needItems.isEmpty(); } @Override public String toString() { StringJoiner joiner = new StringJoiner(", "); ingredients.forEach((character, item) -> joiner.add(item.getName() + ":" + item.getDamage())); return joiner.toString(); } @Override public boolean requiresCraftingTable() { return this.getHeight() > 2 || this.getWidth() > 2; } @Override public List<Item> getIngredientsAggregate() { return ingredientsAggregate; } public static class Entry { public final int x; public final int y; public Entry(int x, int y) { this.x = x; this.y = y; } } }