Search in sources :

Example 1 with QIOCraftingTransferHelper

use of mekanism.common.content.qio.QIOCraftingTransferHelper in project Mekanism by mekanism.

the class QIOCraftingTransferHandler method transferRecipe.

@Nullable
@Override
public IRecipeTransferError transferRecipe(CONTAINER container, Object rawRecipe, IRecipeLayout recipeLayout, PlayerEntity player, boolean maxTransfer, boolean doTransfer) {
    if (!(rawRecipe instanceof ICraftingRecipe)) {
        // a crafting recipe, and if it isn't the server won't know how to transfer it anyway
        return handlerHelper.createInternalError();
    }
    byte selectedCraftingGrid = container.getSelectedCraftingGrid();
    if (selectedCraftingGrid == -1) {
        // as there are no crafting grids being shown
        return handlerHelper.createInternalError();
    }
    ICraftingRecipe recipe = (ICraftingRecipe) rawRecipe;
    QIOCraftingWindow craftingWindow = container.getCraftingWindow(selectedCraftingGrid);
    // Note: This variable is only used for when doTransfer is false
    byte nonEmptyCraftingSlots = 0;
    if (!doTransfer) {
        CraftingInventory dummy = MekanismUtils.getDummyCraftingInv();
        for (int slot = 0; slot < 9; slot++) {
            CraftingWindowInventorySlot inputSlot = craftingWindow.getInputSlot(slot);
            if (!inputSlot.isEmpty()) {
                // Copy it in case any recipe does weird things and tries to mutate the stack
                dummy.setItem(slot, StackUtils.size(inputSlot.getStack(), 1));
                // Count how many crafting slots are not empty
                nonEmptyCraftingSlots++;
            }
        }
        if (recipe.matches(dummy, player.level)) {
            // or we may be transferring different items if different ones are shown in JEI
            return null;
        }
    }
    // TODO: It may be nice to eventually implement some sort of caching for this, it isn't drastically needed because JEI is smart
    // and only calls it once per recipe to decide if it should display the button rather than say calling it every render tick in
    // case something changed and the render state should be different. We probably could add some sort of listeners to
    // inventory, QIO, and crafting window that if one changes it invalidates the cache of what ingredients are stored, though then
    // we wouldn't be able to directly modify the map as we find inputs, and also we still would have to do a lot of this comparison
    // logic, unless we can also somehow cache the recipe layout and how it interacts with the other information
    int inputCount = 0;
    Byte2ObjectMap<Set<HashedItem>> hashedIngredients = new Byte2ObjectArrayMap<>();
    for (Map.Entry<Integer, ? extends IGuiIngredient<ItemStack>> entry : recipeLayout.getItemStacks().getGuiIngredients().entrySet()) {
        IGuiIngredient<ItemStack> ingredient = entry.getValue();
        if (ingredient.isInput()) {
            List<ItemStack> validIngredients = ingredient.getAllIngredients();
            if (!validIngredients.isEmpty()) {
                // If there are valid ingredients, increment the count
                inputCount++;
                // and convert them to HashedItems
                // Note: we use a linked hash set to preserve the order of the ingredients as done in JEI
                LinkedHashSet<HashedItem> representations = new LinkedHashSet<>();
                // Note: We shouldn't need to convert the item that is part of the recipe to a "reduced" stack form based
                // on what the server would send, as the item should already be like that from when the server sent the
                // client the recipe. If this turns out to be incorrect due to how some mod does recipes, then we may need
                // to change this
                ItemStack displayed = ingredient.getDisplayedIngredient();
                // so we may as well remove some unneeded copies
                if (displayed != null) {
                    // Start by adding the displayed ingredient if there is one to prioritize it
                    representations.add(HashedItem.raw(displayed));
                }
                // we will just end up merging with the displayed ingredient when we get to it as a valid ingredient
                for (ItemStack validIngredient : validIngredients) {
                    representations.add(HashedItem.raw(validIngredient));
                }
                // Note: We decrement the index by one because JEI uses the first index for the output
                int actualIndex = entry.getKey() - 1;
                if (actualIndex > Byte.MAX_VALUE || actualIndex < Byte.MIN_VALUE) {
                    // Note: This should not happen, but validate it doesn't just to ensure if it does, we gracefully error
                    Mekanism.logger.warn("Error evaluating recipe transfer handler for recipe: {}, had unexpected index: {}", recipe.getId(), actualIndex);
                    return handlerHelper.createInternalError();
                }
                hashedIngredients.put((byte) actualIndex, representations);
            }
        }
    }
    if (inputCount > 9) {
        // I don't believe this ever will happen with a normal crafting recipe but just in case it does, error
        // if we have more than nine inputs, we check it as an extra validation step, but we don't hold off on
        // converting input ingredients to HashedItems, until we have validated this, as there should never be
        // a case where this actually happens except potentially with some really obscure modded recipe
        Mekanism.logger.warn("Error evaluating recipe transfer handler for recipe: {}, had more than 9 inputs: {}", recipe.getId(), inputCount);
        return handlerHelper.createInternalError();
    }
    // Get all our available items in the QIO frequency, we flatten the cache to stack together items that
    // as far as the client is concerned are the same instead of keeping them UUID separated, and add all
    // the items in the currently selected crafting window and the player's inventory to our available items
    QIOCraftingTransferHelper qioTransferHelper = container.getTransferHelper(player, craftingWindow);
    if (qioTransferHelper.isInvalid()) {
        Mekanism.logger.warn("Error initializing QIO transfer handler for crafting window: {}", selectedCraftingGrid);
        return handlerHelper.createInternalError();
    }
    // Note: We do this in a reversed manner (HashedItem -> slots, vs slot -> HashedItem) so that we can more easily
    // calculate the split for how we handle maxTransfer by quickly being able to see how many of each type we have
    Map<HashedItem, ByteList> matchedItems = new HashMap<>(inputCount);
    ByteSet missingSlots = new ByteArraySet(inputCount);
    for (Byte2ObjectMap.Entry<Set<HashedItem>> entry : hashedIngredients.byte2ObjectEntrySet()) {
        // TODO: Eventually we probably will want to add in some handling for if an item is valid for more than one slot and one combination
        // has it being valid and one combination it is not valid. For example if we have a single piece of stone and it is valid in either
        // slot 1 or 2 but slot 2 only allows for stone, and slot 1 can accept granite instead and we have granite available. When coming
        // up with a solution to this, we also will need to handle the slower comparison method, and make sure that if maxTransfer is true
        // then we pick the one that has the most elements we can assign to all slots evenly so that we can craft as many things as possible.
        // We currently don't bother with any handling related to this as JEI's own transfer handler it registers for things like the crafting
        // table don't currently handle this, though it is something that would be nice to handle and is something I believe vanilla's recipe
        // book transfer handler is able to do (RecipeItemHelper/ServerRecipePlayer)
        boolean matchFound = false;
        for (HashedItem validInput : entry.getValue()) {
            HashedItemSource source = qioTransferHelper.getSource(validInput);
            if (source != null && source.hasMoreRemaining()) {
                // We found a match for this slot, reduce how much of the item we have as an input
                source.matchFound();
                // mark that we found a match
                matchFound = true;
                // and which HashedItem the slot's index corresponds to
                matchedItems.computeIfAbsent(validInput, item -> new ByteArrayList()).add(entry.getByteKey());
                // and stop checking the other possible inputs
                break;
            }
        }
        if (!matchFound) {
            // If we didn't find a match for the slot, add it as a slot we may be missing
            missingSlots.add(entry.getByteKey());
        }
    }
    if (!missingSlots.isEmpty()) {
        // After doing the quicker exact match lookup checks, go through any potentially missing slots
        // and do the slower more "accurate" check of if the stacks match. This allows us to use JEI's
        // system for letting mods declare what things match when it comes down to NBT
        Map<HashedItem, String> cachedIngredientUUIDs = new HashMap<>();
        for (Map.Entry<HashedItem, HashedItemSource> entry : qioTransferHelper.reverseLookup.entrySet()) {
            HashedItemSource source = entry.getValue();
            if (source.hasMoreRemaining()) {
                // Only look at the source if we still have more items available in it
                HashedItem storedHashedItem = entry.getKey();
                ItemStack storedItem = storedHashedItem.getStack();
                Item storedItemType = storedItem.getItem();
                String storedItemUUID = null;
                for (ByteIterator missingIterator = missingSlots.iterator(); missingIterator.hasNext(); ) {
                    byte index = missingIterator.nextByte();
                    for (HashedItem validIngredient : hashedIngredients.get(index)) {
                        // Compare the raw item types
                        if (storedItemType == validIngredient.getStack().getItem()) {
                            // If they match, compute the identifiers for both stacks as needed
                            if (storedItemUUID == null) {
                                // If we haven't retrieved a UUID for the stored stack yet because none of our previous ingredients
                                // matched the basic item type, retrieve it
                                storedItemUUID = stackHelper.getUniqueIdentifierForStack(storedItem, UidContext.Recipe);
                            }
                            // Next compute the UUID for the ingredient we are missing if we haven't already calculated it
                            // either in a previous iteration or for a different slot
                            String ingredientUUID = cachedIngredientUUIDs.computeIfAbsent(validIngredient, ingredient -> stackHelper.getUniqueIdentifierForStack(ingredient.getStack(), UidContext.Recipe));
                            if (storedItemUUID.equals(ingredientUUID)) {
                                // If the items are equivalent, reduce how much of the item we have as an input
                                source.matchFound();
                                // unmark that the slot is missing a match
                                missingIterator.remove();
                                // and mark which HashedItem the slot's index corresponds to
                                matchedItems.computeIfAbsent(storedHashedItem, item -> new ByteArrayList()).add(index);
                                // and stop checking the other possible inputs
                                break;
                            }
                        }
                    }
                    if (!source.hasMoreRemaining()) {
                        // If we have "used up" all the input we have available then continue onto the next stored stack
                        break;
                    }
                }
                if (missingSlots.isEmpty()) {
                    // If we have accounted for all the slots, stop checking for matches
                    break;
                }
            }
        }
        if (!missingSlots.isEmpty()) {
            // Note: We have to shift this back up by one as we shifted the indices earlier to make them easier to work with
            return handlerHelper.createUserErrorForSlots(MekanismLang.JEI_MISSING_ITEMS.translate(), missingSlots.stream().map(i -> i + 1).collect(Collectors.toList()));
        }
    }
    if (doTransfer || (nonEmptyCraftingSlots > 0 && nonEmptyCraftingSlots >= qioTransferHelper.getEmptyInventorySlots())) {
        // Note: If all our crafting inventory slots are not empty, and we don't "obviously" have enough room due to empty slots,
        // then we need to calculate how much we can actually transfer and where it is coming from so that we are able to calculate
        // if we actually have enough room to shuffle the items around, even though otherwise we would only need to do these
        // calculations for when we are transferring items
        int toTransfer;
        if (maxTransfer) {
            // Calculate how much we can actually transfer if we want to transfer as many full sets as possible
            long maxToTransfer = Long.MAX_VALUE;
            for (Map.Entry<HashedItem, ByteList> entry : matchedItems.entrySet()) {
                HashedItem hashedItem = entry.getKey();
                HashedItemSource source = qioTransferHelper.getSource(hashedItem);
                if (source == null) {
                    // If something went wrong, and we don't actually have the item we think we do, error
                    return invalidSource(hashedItem);
                }
                int maxStack = hashedItem.getStack().getMaxStackSize();
                // If we have something that only stacks to one, such as a bucket. Don't limit the max stack size
                // of other items to one
                long max = maxStack == 1 ? maxToTransfer : Math.min(maxToTransfer, maxStack);
                // Note: This will always be at least one as the int list should not be able to become
                // larger than the number of items we have available
                maxToTransfer = Math.min(max, source.getAvailable() / entry.getValue().size());
            }
            toTransfer = MathUtils.clampToInt(maxToTransfer);
        } else {
            toTransfer = 1;
        }
        QIOFrequency frequency = container.getFrequency();
        Byte2ObjectMap<List<SingularHashedItemSource>> sources = new Byte2ObjectArrayMap<>(inputCount);
        Map<HashedItemSource, List<List<SingularHashedItemSource>>> shuffleLookup = frequency == null ? Collections.emptyMap() : new HashMap<>(inputCount);
        for (Map.Entry<HashedItem, ByteList> entry : matchedItems.entrySet()) {
            HashedItem hashedItem = entry.getKey();
            HashedItemSource source = qioTransferHelper.getSource(hashedItem);
            if (source == null) {
                // If something went wrong, and we don't actually have the item we think we do, error
                return invalidSource(hashedItem);
            }
            // Cap the amount to transfer at the max tack size. This way we allow for transferring buckets
            // and other stuff with it. This only actually matters if the max stack size is one, due to
            // the logic done above when calculating how much to transfer, but we do this regardless here
            // as there is no reason not to and then if we decide to widen it up we only have to change one spot
            int transferAmount = Math.min(toTransfer, hashedItem.getStack().getMaxStackSize());
            for (byte slot : entry.getValue()) {
                // Try to use the item and figure out where it is coming from
                List<SingularHashedItemSource> actualSources = source.use(transferAmount);
                if (actualSources.isEmpty()) {
                    // If something went wrong, and we don't actually have enough of the item for some reason, error
                    return invalidSource(hashedItem);
                }
                sources.put(slot, actualSources);
                if (frequency != null) {
                    // The shuffle lookup only comes into play if we have a frequency so might end up having to check if there is room in it
                    int elements = entry.getValue().size();
                    if (elements == 1) {
                        shuffleLookup.put(source, Collections.singletonList(actualSources));
                    } else {
                        shuffleLookup.computeIfAbsent(source, s -> new ArrayList<>(elements)).add(actualSources);
                    }
                }
            }
        }
        if (!hasRoomToShuffle(qioTransferHelper, frequency, craftingWindow, container.getHotBarSlots(), container.getMainInventorySlots(), shuffleLookup)) {
            return handlerHelper.createUserErrorWithTooltip(MekanismLang.JEI_INVENTORY_FULL.translate());
        }
        if (doTransfer) {
            // Note: We skip doing a validation check on if the recipe matches or not, as there is a chance that for some recipes
            // things may not fully be accurate on the client side with the stacks that JEI lets us know match the recipe, as
            // they may require extra NBT that is server side only.
            // TODO: If the sources are all from the crafting window and are already in the correct spots, there is no need to send this packet
            Mekanism.packetHandler.sendToServer(new PacketQIOFillCraftingWindow(recipe.getId(), maxTransfer, sources));
        }
    }
    return null;
}
Also used : LinkedHashSet(java.util.LinkedHashSet) PacketQIOFillCraftingWindow(mekanism.common.network.to_server.PacketQIOFillCraftingWindow) Object2BooleanMap(it.unimi.dsi.fastutil.objects.Object2BooleanMap) Object2IntArrayMap(it.unimi.dsi.fastutil.objects.Object2IntArrayMap) HashedItemSource(mekanism.common.content.qio.QIOCraftingTransferHelper.HashedItemSource) SingularHashedItemSource(mekanism.common.content.qio.QIOCraftingTransferHelper.SingularHashedItemSource) IStackHelper(mezz.jei.api.helpers.IStackHelper) Item(net.minecraft.item.Item) ByteSet(it.unimi.dsi.fastutil.bytes.ByteSet) Object2BooleanArrayMap(it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap) UidContext(mezz.jei.api.ingredients.subtypes.UidContext) BaseSimulatedInventory(mekanism.common.content.qio.QIOCraftingTransferHelper.BaseSimulatedInventory) ICraftingRecipe(net.minecraft.item.crafting.ICraftingRecipe) ByteIterator(it.unimi.dsi.fastutil.bytes.ByteIterator) Map(java.util.Map) CraftingInventory(net.minecraft.inventory.CraftingInventory) ByteArraySet(it.unimi.dsi.fastutil.bytes.ByteArraySet) IRecipeLayout(mezz.jei.api.gui.IRecipeLayout) IRecipeTransferError(mezz.jei.api.recipe.transfer.IRecipeTransferError) HashedItem(mekanism.common.lib.inventory.HashedItem) PlayerEntity(net.minecraft.entity.player.PlayerEntity) QIOItemViewerContainer(mekanism.common.inventory.container.QIOItemViewerContainer) StackUtils(mekanism.common.util.StackUtils) Set(java.util.Set) UUID(java.util.UUID) Collectors(java.util.stream.Collectors) MekanismUtils(mekanism.common.util.MekanismUtils) List(java.util.List) Byte2ObjectArrayMap(it.unimi.dsi.fastutil.bytes.Byte2ObjectArrayMap) MekanismLang(mekanism.common.MekanismLang) IRecipeTransferHandler(mezz.jei.api.recipe.transfer.IRecipeTransferHandler) MainInventorySlot(mekanism.common.inventory.container.slot.MainInventorySlot) HotBarSlot(mekanism.common.inventory.container.slot.HotBarSlot) HashMap(java.util.HashMap) QIOCraftingWindow(mekanism.common.content.qio.QIOCraftingWindow) ArrayList(java.util.ArrayList) ItemStack(net.minecraft.item.ItemStack) ByteList(it.unimi.dsi.fastutil.bytes.ByteList) QIOFrequency(mekanism.common.content.qio.QIOFrequency) IRecipeTransferHandlerHelper(mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper) Byte2ObjectMap(it.unimi.dsi.fastutil.bytes.Byte2ObjectMap) Nonnull(javax.annotation.Nonnull) LinkedHashSet(java.util.LinkedHashSet) Nullable(javax.annotation.Nullable) Mekanism(mekanism.common.Mekanism) QIOCraftingTransferHelper(mekanism.common.content.qio.QIOCraftingTransferHelper) MathUtils(mekanism.api.math.MathUtils) CraftingWindowInventorySlot(mekanism.common.inventory.slot.CraftingWindowInventorySlot) IGuiIngredient(mezz.jei.api.gui.ingredient.IGuiIngredient) Object2IntMap(it.unimi.dsi.fastutil.objects.Object2IntMap) IInventorySlot(mekanism.api.inventory.IInventorySlot) ByteArrayList(it.unimi.dsi.fastutil.bytes.ByteArrayList) Collections(java.util.Collections) HashedItem(mekanism.common.lib.inventory.HashedItem) ByteSet(it.unimi.dsi.fastutil.bytes.ByteSet) ByteArraySet(it.unimi.dsi.fastutil.bytes.ByteArraySet) Set(java.util.Set) LinkedHashSet(java.util.LinkedHashSet) HashedItemSource(mekanism.common.content.qio.QIOCraftingTransferHelper.HashedItemSource) SingularHashedItemSource(mekanism.common.content.qio.QIOCraftingTransferHelper.SingularHashedItemSource) HashMap(java.util.HashMap) ArrayList(java.util.ArrayList) ByteArrayList(it.unimi.dsi.fastutil.bytes.ByteArrayList) Item(net.minecraft.item.Item) HashedItem(mekanism.common.lib.inventory.HashedItem) PacketQIOFillCraftingWindow(mekanism.common.network.to_server.PacketQIOFillCraftingWindow) ICraftingRecipe(net.minecraft.item.crafting.ICraftingRecipe) ByteArrayList(it.unimi.dsi.fastutil.bytes.ByteArrayList) List(java.util.List) ArrayList(java.util.ArrayList) ByteList(it.unimi.dsi.fastutil.bytes.ByteList) ByteArrayList(it.unimi.dsi.fastutil.bytes.ByteArrayList) Byte2ObjectArrayMap(it.unimi.dsi.fastutil.bytes.Byte2ObjectArrayMap) CraftingWindowInventorySlot(mekanism.common.inventory.slot.CraftingWindowInventorySlot) CraftingInventory(net.minecraft.inventory.CraftingInventory) ByteList(it.unimi.dsi.fastutil.bytes.ByteList) QIOCraftingWindow(mekanism.common.content.qio.QIOCraftingWindow) SingularHashedItemSource(mekanism.common.content.qio.QIOCraftingTransferHelper.SingularHashedItemSource) Byte2ObjectMap(it.unimi.dsi.fastutil.bytes.Byte2ObjectMap) ByteArraySet(it.unimi.dsi.fastutil.bytes.ByteArraySet) ByteIterator(it.unimi.dsi.fastutil.bytes.ByteIterator) QIOCraftingTransferHelper(mekanism.common.content.qio.QIOCraftingTransferHelper) ItemStack(net.minecraft.item.ItemStack) Object2BooleanMap(it.unimi.dsi.fastutil.objects.Object2BooleanMap) Object2IntArrayMap(it.unimi.dsi.fastutil.objects.Object2IntArrayMap) Object2BooleanArrayMap(it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap) Map(java.util.Map) Byte2ObjectArrayMap(it.unimi.dsi.fastutil.bytes.Byte2ObjectArrayMap) HashMap(java.util.HashMap) Byte2ObjectMap(it.unimi.dsi.fastutil.bytes.Byte2ObjectMap) Object2IntMap(it.unimi.dsi.fastutil.objects.Object2IntMap) ByteSet(it.unimi.dsi.fastutil.bytes.ByteSet) QIOFrequency(mekanism.common.content.qio.QIOFrequency) Nullable(javax.annotation.Nullable)

Aggregations

Byte2ObjectArrayMap (it.unimi.dsi.fastutil.bytes.Byte2ObjectArrayMap)1 Byte2ObjectMap (it.unimi.dsi.fastutil.bytes.Byte2ObjectMap)1 ByteArrayList (it.unimi.dsi.fastutil.bytes.ByteArrayList)1 ByteArraySet (it.unimi.dsi.fastutil.bytes.ByteArraySet)1 ByteIterator (it.unimi.dsi.fastutil.bytes.ByteIterator)1 ByteList (it.unimi.dsi.fastutil.bytes.ByteList)1 ByteSet (it.unimi.dsi.fastutil.bytes.ByteSet)1 Object2BooleanArrayMap (it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap)1 Object2BooleanMap (it.unimi.dsi.fastutil.objects.Object2BooleanMap)1 Object2IntArrayMap (it.unimi.dsi.fastutil.objects.Object2IntArrayMap)1 Object2IntMap (it.unimi.dsi.fastutil.objects.Object2IntMap)1 ArrayList (java.util.ArrayList)1 Collections (java.util.Collections)1 HashMap (java.util.HashMap)1 LinkedHashSet (java.util.LinkedHashSet)1 List (java.util.List)1 Map (java.util.Map)1 Set (java.util.Set)1 UUID (java.util.UUID)1 Collectors (java.util.stream.Collectors)1