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;
}
Aggregations