/* * This file is part of Applied Energistics 2. * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Applied Energistics 2 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Applied Energistics 2. If not, see . */ package appeng.helpers; import appeng.api.AEApi; import appeng.api.networking.crafting.ICraftingPatternDetails; import appeng.api.storage.channels.IItemStorageChannel; import appeng.api.storage.data.IAEItemStack; import appeng.container.ContainerNull; import appeng.util.Platform; import appeng.util.item.AEItemStack; import net.minecraft.inventory.InventoryCrafting; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.crafting.CraftingManager; import net.minecraft.item.crafting.IRecipe; import net.minecraft.item.crafting.Ingredient; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.NonNullList; import net.minecraft.world.World; import net.minecraftforge.common.crafting.IShapedRecipe; import java.util.*; import static appeng.helpers.ItemStackHelper.stackFromNBT; public class PatternHelper implements ICraftingPatternDetails, Comparable { public static final int CRAFTING_GRID_DIMENSION = 3; private static final int CRAFTING_INPUT_LIMIT = CRAFTING_GRID_DIMENSION * CRAFTING_GRID_DIMENSION; public static final int PROCESSING_INPUT_HEIGHT = 4; public static final int PROCESSING_INPUT_WIDTH = 4; public static final int PROCESSING_INPUT_LIMIT = PROCESSING_INPUT_HEIGHT * PROCESSING_INPUT_WIDTH; public static final int CRAFTING_OUTPUT_LIMIT = 1; public static final int PROCESSING_OUTPUT_LIMIT = 6; private final ItemStack patternItem; private final InventoryCrafting crafting; private final InventoryCrafting testFrame; private final ItemStack correctOutput; private final IRecipe standardRecipe; private final IAEItemStack[] condensedInputs; private final IAEItemStack[] condensedOutputs; private final IAEItemStack[] inputs; private final IAEItemStack[] outputs; private final Map> substituteInputs; private final boolean isCrafting; private final boolean canSubstitute; private final Set failCache = new HashSet<>(); private final Set passCache = new HashSet<>(); private final IAEItemStack pattern; private int priority = 0; public PatternHelper(final ItemStack is, final World w) { final NBTTagCompound encodedValue = is.getTagCompound(); if (encodedValue == null) { throw new IllegalArgumentException("No pattern here!"); } final NBTTagList inTag = encodedValue.getTagList("in", 10); final NBTTagList outTag = encodedValue.getTagList("out", 10); this.isCrafting = encodedValue.getBoolean("crafting"); crafting = new InventoryCrafting(new ContainerNull(), isCrafting ? 3 : 4, isCrafting ? 3 : 4); testFrame = new InventoryCrafting(new ContainerNull(), isCrafting ? 3 : 4, isCrafting ? 3 : 4); this.canSubstitute = this.isCrafting && encodedValue.getBoolean("substitute"); this.patternItem = is; this.pattern = AEItemStack.fromItemStack(is); final List in = new ArrayList<>(); final List out = new ArrayList<>(); for (int x = 0; x < inTag.tagCount(); x++) { NBTTagCompound ingredient = inTag.getCompoundTagAt(x); final ItemStack gs = stackFromNBT(ingredient); if (!ingredient.hasNoTags() && gs.isEmpty()) { throw new IllegalArgumentException("No pattern here!"); } this.crafting.setInventorySlotContents(x, gs); if (!gs.isEmpty() && (!this.isCrafting || !gs.hasTagCompound())) { this.markItemAs(x, gs, TestStatus.ACCEPT); } in.add(AEApi.instance().storage().getStorageChannel(IItemStorageChannel.class).createStack(gs)); this.testFrame.setInventorySlotContents(x, gs); } if (this.isCrafting) { this.standardRecipe = CraftingManager.findMatchingRecipe(this.crafting, w); if (this.standardRecipe != null) { this.correctOutput = this.standardRecipe.getCraftingResult(this.crafting); out.add(AEApi.instance().storage().getStorageChannel(IItemStorageChannel.class).createStack(this.correctOutput)); } else { throw new IllegalStateException("No pattern here!"); } } else { this.standardRecipe = null; this.correctOutput = ItemStack.EMPTY; for (int x = 0; x < outTag.tagCount(); x++) { NBTTagCompound resultItemTag = outTag.getCompoundTagAt(x); final ItemStack gs = stackFromNBT(resultItemTag); if (!resultItemTag.hasNoTags() && gs.isEmpty()) { throw new IllegalArgumentException("No pattern here!"); } if (!gs.isEmpty()) { out.add(AEApi.instance().storage().getStorageChannel(IItemStorageChannel.class).createStack(gs)); } } } final int outputLength = out.size(); this.inputs = in.toArray(new IAEItemStack[isCrafting ? CRAFTING_INPUT_LIMIT : PROCESSING_INPUT_LIMIT]); this.outputs = out.toArray(new IAEItemStack[outputLength]); this.substituteInputs = new HashMap<>(CRAFTING_INPUT_LIMIT); final Map tmpOutputs = new HashMap<>(); for (final IAEItemStack io : this.outputs) { if (io == null) { continue; } final IAEItemStack g = tmpOutputs.get(io); if (g == null) { tmpOutputs.put(io, io.copy()); } else { g.add(io); } } final Map tmpInputs = new HashMap<>(); for (final IAEItemStack io : this.inputs) { if (io == null) { continue; } final IAEItemStack g = tmpInputs.get(io); if (g == null) { tmpInputs.put(io, io.copy()); } else { g.add(io); } } if (tmpOutputs.isEmpty() || tmpInputs.isEmpty()) { throw new IllegalStateException("No pattern here!"); } this.condensedInputs = new IAEItemStack[tmpInputs.size()]; int offset = 0; for (final IAEItemStack io : tmpInputs.values()) { this.condensedInputs[offset] = io; offset++; } offset = 0; this.condensedOutputs = new IAEItemStack[tmpOutputs.size()]; for (final IAEItemStack io : tmpOutputs.values()) { this.condensedOutputs[offset] = io; offset++; } } private void markItemAs(final int slotIndex, final ItemStack i, final TestStatus b) { if (b == TestStatus.TEST || i.hasTagCompound()) { return; } (b == TestStatus.ACCEPT ? this.passCache : this.failCache).add(new TestLookup(slotIndex, i)); } @Override public ItemStack getPattern() { return this.patternItem; } @Override public synchronized boolean isValidItemForSlot(final int slotIndex, final ItemStack i, final World w) { if (!this.isCrafting) { throw new IllegalStateException("Only crafting recipes supported."); } final TestStatus result = this.getStatus(slotIndex, i); switch (result) { case ACCEPT: return true; case DECLINE: return false; case TEST: default: break; } for (int x = 0; x < this.crafting.getSizeInventory(); x++) { this.testFrame.setInventorySlotContents(x, this.crafting.getStackInSlot(x)); } this.testFrame.setInventorySlotContents(slotIndex, i); // If we cannot substitute, the items must match exactly if ((!(i.getItem().isDamageable() || Platform.isGTDamageableItem(i.getItem())) && !canSubstitute) && slotIndex < inputs.length) { if (!inputs[slotIndex].isSameType(i)) { this.markItemAs(slotIndex, i, TestStatus.DECLINE); return false; } } if (this.standardRecipe.matches(this.testFrame, w)) { final ItemStack testOutput = this.standardRecipe.getCraftingResult(this.testFrame); if (Platform.itemComparisons().isSameItem(this.correctOutput, testOutput)) { this.testFrame.setInventorySlotContents(slotIndex, this.crafting.getStackInSlot(slotIndex)); this.markItemAs(slotIndex, i, TestStatus.ACCEPT); return true; } } this.markItemAs(slotIndex, i, TestStatus.DECLINE); return false; } @Override public boolean isCraftable() { return this.isCrafting; } @Override public IAEItemStack[] getInputs() { return this.inputs; } @Override public IAEItemStack[] getCondensedInputs() { return this.condensedInputs; } @Override public IAEItemStack[] getCondensedOutputs() { return this.condensedOutputs; } @Override public IAEItemStack[] getOutputs() { return this.outputs; } @Override public boolean canSubstitute() { return this.canSubstitute; } @Override public List getSubstituteInputs(int slot) { if (this.inputs[slot] == null) { return Collections.emptyList(); } return this.substituteInputs.computeIfAbsent(slot, value -> { ItemStack[] matchingStacks = getRecipeIngredient(slot).getMatchingStacks(); List itemList = new ArrayList<>(matchingStacks.length + 1); for (ItemStack matchingStack : matchingStacks) { itemList.add(AEItemStack.fromItemStack(matchingStack)); } // Ensure that the specific item put in by the user is at the beginning, // so that it takes precedence over substitutions itemList.add(0, this.inputs[slot]); return itemList; }); } /** * Gets the {@link Ingredient} from the actual used recipe for a given slot-index into {@link #getInputs()}. *

* Conversion is needed for two reasons: our sparse ingredients are always organized in a 3x3 grid, while Vanilla's * ingredient list will be condensed to the actual recipe's grid size. In addition, in our 3x3 grid, the user can * shift the actual recipe input to the right and down. */ private Ingredient getRecipeIngredient(int slot) { if (standardRecipe instanceof IShapedRecipe) { IShapedRecipe shapedRecipe = (IShapedRecipe) standardRecipe; return getShapedRecipeIngredient(slot, shapedRecipe.getRecipeWidth()); } else { return getShapelessRecipeIngredient(slot); } } private Ingredient getShapedRecipeIngredient(int slot, int recipeWidth) { // Compute the offset of the user's input vs. crafting grid origin // Which is >0 if they have empty rows above or to the left of their input int topOffset = 0; if (inputs[0] == null && inputs[1] == null && inputs[2] == null) { topOffset++; // First row is fully empty if (inputs[3] == null && inputs[4] == null && inputs[5] == null) { topOffset++; // Second row is fully empty } } int leftOffset = 0; if (inputs[0] == null && inputs[3] == null && inputs[6] == null) { leftOffset++; // First column is fully empty if (inputs[1] == null && inputs[4] == null && inputs[7] == null) { leftOffset++; // Second column is fully empty } } // Compute the x,y of the slot, as-if the recipe was anchored to 0,0 int slotX = slot % CRAFTING_GRID_DIMENSION - leftOffset; int slotY = slot / CRAFTING_GRID_DIMENSION - topOffset; // Compute the index into the recipe's ingredient list now int ingredientIndex = slotY * recipeWidth + slotX; NonNullList ingredients = standardRecipe.getIngredients(); if (ingredientIndex < 0 || ingredientIndex > ingredients.size()) { return Ingredient.EMPTY; } return ingredients.get(ingredientIndex); } private Ingredient getShapelessRecipeIngredient(int slot) { // We map the list of *filled* sparse inputs to the shapeless (ergo unordered) // ingredients. While these do not actually correspond to each other, // since both lists have the same length, the mapping is at least stable. int ingredientIndex = 0; for (int i = 0; i < slot; i++) { if (inputs[i] != null) { ingredientIndex++; } } NonNullList ingredients = standardRecipe.getIngredients(); if (ingredientIndex < ingredients.size()) { return ingredients.get(ingredientIndex); } return Ingredient.EMPTY; } @Override public ItemStack getOutput(final InventoryCrafting craftingInv, final World w) { if (!this.isCrafting) { throw new IllegalStateException("Only crafting recipes supported."); } for (int x = 0; x < craftingInv.getSizeInventory(); x++) { if (!this.isValidItemForSlot(x, craftingInv.getStackInSlot(x), w)) { return ItemStack.EMPTY; } } if (this.outputs != null && this.outputs.length > 0) { return this.outputs[0].createItemStack(); } return ItemStack.EMPTY; } private TestStatus getStatus(final int slotIndex, final ItemStack i) { if (this.crafting.getStackInSlot(slotIndex).isEmpty()) { return i.isEmpty() ? TestStatus.ACCEPT : TestStatus.DECLINE; } if (i.isEmpty()) { return TestStatus.DECLINE; } if (i.hasTagCompound()) { return TestStatus.TEST; } if (this.passCache.contains(new TestLookup(slotIndex, i))) { return TestStatus.ACCEPT; } if (this.failCache.contains(new TestLookup(slotIndex, i))) { return TestStatus.DECLINE; } return TestStatus.TEST; } @Override public int getPriority() { return this.priority; } @Override public void setPriority(final int priority) { this.priority = priority; } @Override public int compareTo(final PatternHelper o) { return Integer.compare(o.priority, this.priority); } @Override public int hashCode() { return this.pattern.hashCode(); } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (this.getClass() != obj.getClass()) { return false; } final PatternHelper other = (PatternHelper) obj; if (this.pattern != null && other.pattern != null) { return this.pattern.equals(other.pattern); } return false; } private enum TestStatus { ACCEPT, DECLINE, TEST } private static final class TestLookup { private final int slot; private final int ref; private final int hash; public TestLookup(final int slot, final ItemStack i) { this(slot, i.getItem(), i.getItemDamage()); } public TestLookup(final int slot, final Item item, final int dmg) { this.slot = slot; this.ref = (dmg << Platform.DEF_OFFSET) | (Item.getIdFromItem(item) & 0xffff); final int offset = 3 * slot; this.hash = (this.ref << offset) | (this.ref >> (offset + 32)); } @Override public int hashCode() { return this.hash; } @Override public boolean equals(final Object obj) { final boolean equality; if (obj instanceof TestLookup) { final TestLookup b = (TestLookup) obj; equality = b.slot == this.slot && b.ref == this.ref; } else { equality = false; } return equality; } } }