blob: 046e5009ddf0769fac743824b810966c3116fcc3 [file] [log] [blame] [raw]
package net.glowstone.inventory.crafting;
import com.google.common.collect.Iterators;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import net.glowstone.block.MaterialUtil;
import net.glowstone.constants.ItemIds;
import net.glowstone.i18n.ConsoleMessages;
import net.glowstone.inventory.GlowCraftingInventory;
import net.glowstone.util.InventoryUtil;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.FurnaceRecipe;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.ShapelessRecipe;
/**
* Manager for crafting and smelting recipes.
*/
public final class CraftingManager implements Iterable<Recipe> {
private final ArrayList<ShapedRecipe> shapedRecipes = new ArrayList<>();
private final ArrayList<ShapelessRecipe> shapelessRecipes = new ArrayList<>();
private final ArrayList<DynamicRecipe> dynamicRecipes = new ArrayList<>();
private final ArrayList<FurnaceRecipe> furnaceRecipes = new ArrayList<>();
private final Map<Material, Integer> furnaceFuels = new HashMap<>();
/**
* Get the amount of layers in the crafting matrix. This assumes all Minecraft recipes have an
* item stack of 1 for all items in the recipe.
*
* @param items The items in the crafting matrix.
* @return The number of stacks for a recipe.
*/
public static int getLayers(ItemStack... items) {
int layers = 0;
for (ItemStack item : items) {
if (!InventoryUtil.isEmpty(item) && (item.getAmount() < layers || layers == 0)) {
layers = item.getAmount();
}
}
return layers;
}
/**
* Loads all recipes.
*/
public void initialize() {
resetRecipes();
// Report stats
ConsoleMessages.Info.RECIPE_COUNTS.log(shapedRecipes.size(),
shapelessRecipes.size(), furnaceRecipes.size(),
dynamicRecipes.size(), furnaceFuels.size());
}
/**
* Adds a recipe to the crafting manager.
*
* @param recipe The recipe to add.
* @return Whether adding the recipe was successful.
*/
public boolean addRecipe(Recipe recipe) {
if (recipe instanceof ShapedRecipe) {
return shapedRecipes.add((ShapedRecipe) recipe);
}
if (recipe instanceof ShapelessRecipe) {
return shapelessRecipes.add((ShapelessRecipe) recipe);
}
if (recipe instanceof DynamicRecipe) {
return dynamicRecipes.add((DynamicRecipe) recipe);
}
return recipe instanceof FurnaceRecipe && furnaceRecipes.add((FurnaceRecipe) recipe);
}
/**
* Get a furnace recipe from the crafting manager.
*
* @param input The furnace input.
* @return The FurnaceRecipe, or null if none is found.
*/
public FurnaceRecipe getFurnaceRecipe(ItemStack input) {
for (FurnaceRecipe recipe : furnaceRecipes) {
if (matchesWildcard(recipe.getInput(), input)) {
return recipe;
}
}
return null;
}
/**
* Get how long a given fuel material will burn for.
*
* @param material The fuel material.
* @return The time in ticks, or 0 if that material does not burn.
*/
public int getFuelTime(Material material) {
return furnaceFuels.getOrDefault(material, 0);
}
public boolean isFuel(Material material) {
return furnaceFuels.containsKey(material);
}
/**
* Remove a layer of items from the crafting matrix and recipe result.
*
* @param items The items to remove the ingredients from.
* @param inv The inventory to remove the items from.
*/
public void removeItems(ItemStack[] items, GlowCraftingInventory inv) {
this.removeItems(items, inv, 1);
}
/**
* Remove a specific amount of layers from the crafting matrix and recipe result.
*
* @param items The items to remove the ingredients from.
* @param inv The inventory to remove the items from.
* @param amount The amount of items you want to remove.
*/
public void removeItems(final ItemStack[] items, final GlowCraftingInventory inv,
final int amount) {
if (amount < 0) {
throw new IllegalArgumentException("Can not remove negative amount of layers.");
}
for (int i = 0; i < items.length; i++) {
if (!InventoryUtil.isEmpty(items[i])) {
int itemAmount = items[i].getAmount();
if (itemAmount > amount) {
items[i].setAmount(itemAmount - amount);
inv.updateResultSlot();
} else {
inv.setItem(i + 1, InventoryUtil.createEmptyStack());
}
}
}
}
/**
* Get a crafting recipe from the crafting manager.
*
* @param items An array of items with null being empty slots. Length should be a perfect
* square.
* @return The Recipe that matches the input, or null if none match.
*/
public Recipe getCraftingRecipe(ItemStack... items) {
for (int i = 0; i < items.length; i++) {
if (InventoryUtil.isEmpty(items[i])) {
// TODO: rewrite recipe matcher to respect empty items
items[i] = null;
}
}
int size = (int) Math.sqrt(items.length);
if (size * size != items.length) {
throw new IllegalArgumentException(
"ItemStack list was not square (was " + items.length + ")");
}
ShapedRecipe result = getShapedRecipe(size, items);
if (result != null) {
return result;
}
ItemStack[] reversedItems = new ItemStack[items.length];
for (int row = 0; row < size; ++row) {
for (int col = 0; col < size; ++col) {
int col2 = size - 1 - col;
reversedItems[row * size + col] = items[row * size + col2];
}
}
// this check saves the trouble of iterating through all the recipes again
if (!Arrays.equals(items, reversedItems)) {
result = getShapedRecipe(size, reversedItems);
if (result != null) {
return result;
}
}
for (DynamicRecipe dynamicRecipe : dynamicRecipes) {
if (dynamicRecipe.matches(items)) {
return dynamicRecipe;
}
}
return getShapelessRecipe(items);
}
private ShapedRecipe getShapedRecipe(int size, ItemStack... items) {
for (ShapedRecipe recipe : shapedRecipes) {
Map<Character, ItemStack> ingredients = recipe.getIngredientMap();
String[] shape = recipe.getShape();
int rows = shape.length;
int cols = 0;
for (String row : shape) {
if (row.length() > cols) {
cols = row.length();
}
}
if (rows == 0 || cols == 0) {
continue;
}
// outer loop: try at each possible starting position
for (int rowStart = 0; rowStart <= size - rows; ++rowStart) {
position:
for (int colStart = 0; colStart <= size - cols; ++colStart) {
// inner loop: verify recipe against this position
for (int row = 0; row < rows; ++row) {
for (int col = 0; col < cols; ++col) {
ItemStack given = items[(rowStart + row) * size + colStart + col];
char ingredientChar =
shape[row].length() > col ? shape[row].charAt(col) : ' ';
ItemStack expected = ingredients.get(ingredientChar);
// check for mismatch in presence of an item in that slot at all
if (expected == null) {
if (given != null) {
continue position;
} else {
continue; // good match
}
} else if (given == null) {
continue position;
}
// check for type and data match
if (!matchesWildcard(expected, given)) {
continue position;
}
}
}
// also check that no items outside the recipe size are present
for (int row = 0; row < size; row++) {
for (int col = 0; col < size; col++) {
// if this position is outside the recipe and non-null, fail
if ((row < rowStart || row >= rowStart + rows || col < colStart
|| col >= colStart + cols)
&& items[row * size + col] != null) {
continue position;
}
}
}
// recipe matches and zero items outside the recipe part.
return recipe;
}
} // end position loop
} // end recipe loop
return null;
}
private ShapelessRecipe getShapelessRecipe(ItemStack... items) {
recipe:
for (ShapelessRecipe recipe : shapelessRecipes) {
boolean[] accountedFor = new boolean[items.length];
// Mark empty item slots accounted for
for (int i = 0; i < items.length; ++i) {
accountedFor[i] = items[i] == null;
}
// Make sure each ingredient in the recipe exists in the inventory
ingredient:
for (ItemStack ingredient : recipe.getIngredientList()) {
for (int i = 0; i < items.length; ++i) {
// if this item is not already used and it matches this ingredient...
if (!accountedFor[i] && matchesWildcard(ingredient, items[i])) {
// ... this item is accounted for and this ingredient is found.
accountedFor[i] = true;
continue ingredient;
}
}
// no item matched this ingredient, so the recipe fails
continue recipe;
}
// Make sure inventory has no leftover items
for (int i = 0; i < items.length; ++i) {
if (!accountedFor[i]) {
continue recipe;
}
}
return recipe;
}
return null;
}
@Override
public Iterator<Recipe> iterator() {
return Iterators.concat(shapedRecipes.iterator(), shapelessRecipes.iterator(),
dynamicRecipes.iterator(), furnaceRecipes.iterator());
}
private boolean isWildcard(short data) {
// old-style wildcards (byte -1) not supported
return data == Short.MAX_VALUE;
}
private boolean matchesWildcard(ItemStack expected, ItemStack actual) {
return actual != null && expected.getType() == actual.getType() && (
isWildcard(expected.getDurability()) || expected.getDurability() == actual
.getDurability());
}
/**
* Get a list of all recipes for a given item. The stack size is ignored in comparisons.
* If the durability is -1, it will match any data value.
*
* @param result The item whose recipes you want
* @return The list of recipes
*/
public List<Recipe> getRecipesFor(ItemStack result) {
// handling for old-style wildcards
if (result.getDurability() == -1) {
result = result.clone();
result.setDurability(Short.MAX_VALUE);
}
List<Recipe> recipes = new LinkedList<>();
for (Recipe recipe : this) {
if (matchesWildcard(result, recipe.getResult())) {
recipes.add(recipe);
}
}
return recipes;
}
/**
* Look up a recipe.
*
* @param key the key to look up
* @return the recipe with the given key, or null if none match
*/
public Recipe getRecipeByKey(NamespacedKey key) {
// FIXME: Linear time; use Map<NamespacedKey,?> instances instead
for (ShapedRecipe shapedRecipe : shapedRecipes) {
if (shapedRecipe.getKey().equals(key)) {
return shapedRecipe;
}
}
for (ShapelessRecipe shapelessRecipe : shapelessRecipes) {
if (shapelessRecipe.getKey().equals(key)) {
return shapelessRecipe;
}
}
return null;
}
/**
* Clear all recipes.
*/
public void clearRecipes() {
shapedRecipes.clear();
shapelessRecipes.clear();
dynamicRecipes.clear();
furnaceRecipes.clear();
furnaceFuels.clear();
}
/**
* Reset the crafting recipe lists to their default states.
*/
public void resetRecipes() {
clearRecipes();
loadRecipes();
// Dynamic recipes
dynamicRecipes.add(new DynamicRecipe(new GlowBannerMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowBannerCopyMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowRepairMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowArmorDyeMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowChargeMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowChargeFadeMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowFireworkMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowBookCopyMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowMapCopyMatcher()));
dynamicRecipes.add(new DynamicRecipe(new GlowMapZoomMatcher()));
// Smelting fuels (time is in ticks)
for (Material wool : MaterialUtil.BANNERS) {
furnaceFuels.put(wool, 300);
}
furnaceFuels.put(Material.BLAZE_ROD, 2400);
furnaceFuels.put(Material.COAL_BLOCK, 16000);
furnaceFuels.put(Material.OAK_BOAT, 400);
furnaceFuels.put(Material.ACACIA_BOAT, 400);
furnaceFuels.put(Material.BIRCH_BOAT, 400);
furnaceFuels.put(Material.DARK_OAK_BOAT, 400);
furnaceFuels.put(Material.JUNGLE_BOAT, 400);
furnaceFuels.put(Material.SPRUCE_BOAT, 400);
furnaceFuels.put(Material.BOOKSHELF, 300);
furnaceFuels.put(Material.BOW, 300);
furnaceFuels.put(Material.BOWL, 100);
furnaceFuels.put(Material.CARPET, 67);
furnaceFuels.put(Material.COAL, 1600);
furnaceFuels.put(Material.CHEST, 300);
furnaceFuels.put(Material.WORKBENCH, 300);
furnaceFuels.put(Material.DAYLIGHT_DETECTOR, 300);
furnaceFuels.put(Material.DAYLIGHT_DETECTOR_INVERTED, 300);
furnaceFuels.put(Material.FENCE, 300);
furnaceFuels.put(Material.FENCE_GATE, 300);
furnaceFuels.put(Material.FISHING_ROD, 300);
furnaceFuels.put(Material.JUKEBOX, 300);
furnaceFuels.put(Material.LADDER, 300);
furnaceFuels.put(Material.LAVA_BUCKET, 20000);
furnaceFuels.put(Material.HUGE_MUSHROOM_1, 300);
furnaceFuels.put(Material.HUGE_MUSHROOM_2, 300);
furnaceFuels.put(Material.NOTE_BLOCK, 300);
furnaceFuels.put(Material.SAPLING, 100);
furnaceFuels.put(Material.SIGN, 200);
furnaceFuels.put(Material.STICK, 100);
furnaceFuels.put(Material.TRAP_DOOR, 300);
furnaceFuels.put(Material.TRAPPED_CHEST, 300);
furnaceFuels.put(Material.LOG, 300);
furnaceFuels.put(Material.WOOD, 300);
furnaceFuels.put(Material.WOOD_BUTTON, 300);
furnaceFuels.put(Material.WOOD_PLATE, 300);
furnaceFuels.put(Material.WOOD_STAIRS, 300);
furnaceFuels.put(Material.WOODEN_AXE, 200);
furnaceFuels.put(Material.WOODEN_HOE, 200);
furnaceFuels.put(Material.WOODEN_PICKAXE, 200);
furnaceFuels.put(Material.WOODEN_SHOVEL, 200);
furnaceFuels.put(Material.WOODEN_SWORD, 200);
furnaceFuels.put(Material.WOOD_STEP, 150);
for (Material wool : MaterialUtil.WOOLS) {
furnaceFuels.put(wool, 100);
}
}
/**
* Load default recipes from built-in recipes.yml file.
*/
@SuppressWarnings("unchecked")
private void loadRecipes() {
// Load recipes from recipes.yml file
InputStream in = getClass().getClassLoader().getResourceAsStream("builtin/recipes.yml");
if (in == null) {
ConsoleMessages.Warn.Recipe.NO_DEFAULTS.log();
return;
}
ConfigurationSection config = YamlConfiguration
.loadConfiguration(new InputStreamReader(in));
// shaped
for (Map<?, ?> data : config.getMapList("shaped")) { // NON-NLS
ItemStack resultStack =
ItemStack.deserialize((Map<String, Object>) data.get("result")); // NON-NLS
NamespacedKey key = readKey(data, resultStack);
ShapedRecipe recipe = new ShapedRecipe(key, resultStack);
List<String> shape = (List<String>) data.get("shape"); // NON-NLS
recipe.shape(shape.toArray(new String[shape.size()]));
Map<String, Map<String, Object>> ingredients = (Map<String, Map<String, Object>>) data
.get("ingredients"); // NON-NLS
for (Entry<String, Map<String, Object>> entry : ingredients.entrySet()) {
ItemStack stack = ItemStack.deserialize(entry.getValue());
recipe.setIngredient(entry.getKey().charAt(0), stack.getData());
}
shapedRecipes.add(recipe);
}
// shapeless
for (Map<?, ?> data : config.getMapList("shapeless")) { // NON-NLS
ItemStack resultStack =
ItemStack.deserialize((Map<String, Object>) data.get("result")); // NON-NLS
NamespacedKey key = readKey(data, resultStack);
ShapelessRecipe recipe = new ShapelessRecipe(key, resultStack);
List<Map<String, Object>> ingreds
= (List<Map<String, Object>>) data.get("ingredients"); // NON-NLS
for (Map<String, Object> entry : ingreds) {
recipe.addIngredient(ItemStack.deserialize(entry).getData());
}
shapelessRecipes.add(recipe);
}
// furnace
for (Map<?, ?> data : config.getMapList("furnace")) { // NON-NLS
ItemStack inputStack
= ItemStack.deserialize((Map<String, Object>) data.get("input")); // NON-NLS
ItemStack resultStack
= ItemStack.deserialize((Map<String, Object>) data.get("result")); // NON-NLS
float xp = ((Number) data.get("xp")).floatValue(); // NON-NLS
furnaceRecipes.add(
new FurnaceRecipe(resultStack, inputStack.getType(), inputStack.getDurability(),
xp));
}
}
private NamespacedKey readKey(Map<?, ?> data, ItemStack result) {
NamespacedKey key;
if (data.containsKey("key")) { // NON-NLS
String keyRaw = (String) data.get("key"); // NON-NLS
if (keyRaw.indexOf(':') == -1) {
key = new NamespacedKey(NamespacedKey.MINECRAFT, keyRaw);
} else {
key = new NamespacedKey(keyRaw.substring(0, keyRaw.indexOf(':')),
keyRaw.substring(keyRaw.indexOf(':') + 1, keyRaw.length()));
}
} else {
// todo: remove ambiguity of items with 'data/damage' values inside recipes.yml (will
// take a long time...)
key = NamespacedKey.minecraft(ItemIds.getKeyName(result.getType()));
}
return key;
}
}