Skip to content

Commit 969a971

Browse files
Fix Multiblocks voiding partial multi item outputs when output is mostly Full (#1337)
Co-authored-by: Exaxxion <Exaxxion@users.noreply.github.com>
1 parent f58a9cd commit 969a971

3 files changed

Lines changed: 284 additions & 56 deletions

File tree

src/main/java/gregtech/api/metatileentity/MetaTileEntity.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
import java.util.List;
5757
import java.util.function.Consumer;
5858

59+
import static gregtech.api.util.InventoryUtils.simulateItemStackMerge;
60+
5961
public abstract class MetaTileEntity implements ICoverable {
6062

6163
public static final int DEFAULT_PAINTING_COLOR = 0xFFFFFF;
@@ -927,13 +929,38 @@ protected static void moveInventoryItems(IItemHandler sourceInventory, IItemHand
927929
}
928930
}
929931

930-
public static boolean addItemsToItemHandler(IItemHandler handler, boolean simulate, List<ItemStack> items) {
931-
boolean insertedAll = true;
932-
for (ItemStack stack : items) {
933-
insertedAll &= ItemHandlerHelper.insertItemStacked(handler, stack, simulate).isEmpty();
934-
if (!insertedAll && simulate) return false;
935-
}
936-
return insertedAll;
932+
/**
933+
* Simulates the insertion of items into a target inventory, then optionally performs the insertion.
934+
* <br /><br />
935+
* Simulating will not modify any of the input parameters. Insertion will either succeed completely, or fail
936+
* without modifying anything.
937+
*
938+
* @param handler the target inventory
939+
* @param simulate whether to simulate ({@code true}) or actually perform the insertion ({@code false})
940+
* @param items the items to insert into {@code handler}.
941+
* @return {@code true} if the insertion succeeded, {@code false} otherwise.
942+
* @throws IllegalStateException if {@code handler} does not accept all items as expected while performing a
943+
* real insertion. This should not be possible unless the handler is modified in
944+
* another thread, or it does not behave in a manner conforming with the contract
945+
* of {@link gregtech.api.util.InventoryUtils#simulateItemStackMerge simulateItemStackMerge}.
946+
*/
947+
public static boolean addItemsToItemHandler(final IItemHandler handler,
948+
final boolean simulate,
949+
final List<ItemStack> items)
950+
{
951+
// determine if there is sufficient room to insert all items into the target inventory
952+
final boolean canMerge = simulateItemStackMerge(items, handler);
953+
954+
// if we're not simulating and the merge should succeed, perform the merge.
955+
if(!simulate && canMerge)
956+
items.forEach(stack -> {
957+
ItemStack rest = ItemHandlerHelper.insertItemStacked(handler, stack, simulate);
958+
if(!rest.isEmpty())
959+
throw new IllegalStateException(
960+
String.format("Insertion failed, remaining stack contained %d items.", rest.getCount()));
961+
});
962+
963+
return canMerge;
937964
}
938965

939966
public static boolean addFluidsToFluidHandler(IFluidHandler handler, boolean simulate, List<FluidStack> items) {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package gregtech.api.util;
2+
3+
import net.minecraft.item.*;
4+
import net.minecraftforge.items.*;
5+
6+
import java.util.*;
7+
8+
public class InventoryUtils {
9+
10+
/**
11+
* @param inventory the target inventory
12+
* @return the number of empty slots in {@code inventory}
13+
*/
14+
public static int getNumberOfEmptySlotsInInventory(IItemHandler inventory) {
15+
int emptySlots = 0;
16+
for(int index = 0; index < inventory.getSlots(); index++) {
17+
if(inventory.getStackInSlot(index).isEmpty()) {
18+
emptySlots++;
19+
}
20+
}
21+
22+
return emptySlots;
23+
}
24+
25+
/**
26+
* Creates a deep copy of the target inventory, optionally keeping empty ItemStacks
27+
*
28+
* @param inventory the target inventory
29+
* @param keepEmpty whether to keep empty ItemStacks (if {@code true}, stacks will be kept).
30+
* @return a deep copy of the inventory.
31+
*/
32+
public static List<ItemStack> deepCopy(IItemHandler inventory, boolean keepEmpty) {
33+
int numSlots = inventory.getSlots();
34+
List<ItemStack> inventoryStacks = new ArrayList<>(numSlots);
35+
for(int slotIndex = 0; slotIndex < numSlots; slotIndex++) {
36+
ItemStack stack = inventory.getStackInSlot(slotIndex);
37+
if(keepEmpty || !stack.isEmpty())
38+
inventoryStacks.add(stack.copy());
39+
}
40+
return inventoryStacks;
41+
}
42+
43+
/**
44+
* Determines whether all specified items will fit into a target inventory by
45+
* simulating merging like items into existing stacks, then checking if there
46+
* are enough empty stacks left to accommodate the remaining items.
47+
* <br /><br />
48+
* <b>Precondition:</b> the target inventory must not virtualize ItemStacks such that
49+
* they can exceed the maximum stackable size of the item as
50+
* defined by {@link net.minecraft.item.ItemStack#getMaxStackSize()}.
51+
* <br /><br />
52+
* <b>Precondition:</b> the target inventory must actually accept the types of items
53+
* you are trying to insert.
54+
* <br /><br />
55+
* @param items the items you want to insert
56+
* @param inventory the target inventory receiving items
57+
* @return {@code true} if inventory contains sufficient slots to merge and
58+
* insert all requested items, {@code false} otherwise.
59+
*/
60+
public static boolean simulateItemStackMerge(List<ItemStack> items,
61+
IItemHandler inventory)
62+
{
63+
// If there's enough empty output slots then we don't need to compute merges.
64+
final int emptySlots = getNumberOfEmptySlotsInInventory(inventory);
65+
if(items.size() <= emptySlots)
66+
return true;
67+
68+
// Deep copy the recipe output ItemStacks
69+
final List<ItemStack> itemStacks = new ArrayList<>(items.size());
70+
items.forEach(itemStack -> itemStacks.add(itemStack.copy()));
71+
72+
// Sort by the number of items in each stack so we merge smallest stacks first.
73+
itemStacks.sort(Comparator.comparingInt(ItemStack::getCount));
74+
75+
// Deep copy the contents of the target inventory, skipping empty stacks
76+
final List<ItemStack> inventoryStacks = deepCopy(inventory, false);
77+
78+
// Perform a merge of the ItemStacks
79+
mergeItemStacks(itemStacks, inventoryStacks);
80+
81+
// Return whether there are now sufficient empty slots to fit the unmerged items.
82+
return itemStacks.size() <= emptySlots;
83+
}
84+
85+
/**
86+
* Merges stacks of identical items from a source into a destination.<br />
87+
* Successfully merged items will be removed from {@code source} and will appear in {@code destination}.<br />
88+
* Empty stacks in {@code destination} are not considered for this process.
89+
*
90+
* @param source the ItemStacks to merge into {@code destination}.
91+
* @param destination a target inventory of existing ItemStacks.
92+
*/
93+
private static void mergeItemStacks(Collection<ItemStack> source, Collection<ItemStack> destination) {
94+
// Since we're mutating the collection during iteration, use an iterator.
95+
final Iterator<ItemStack> sourceItemStacks = source.iterator();
96+
while(sourceItemStacks.hasNext()) {
97+
final ItemStack sourceItemStack = sourceItemStacks.next();
98+
99+
// Find a matching item in the output bus, if any
100+
for(ItemStack destItemStack : destination)
101+
if(ItemStack.areItemsEqual(destItemStack, sourceItemStack)) {
102+
// if it's possible to merge stacks
103+
final int availableSlots = destItemStack.getMaxStackSize() - destItemStack.getCount();
104+
if(availableSlots > 0) {
105+
final int itemCount = Math.min(availableSlots, sourceItemStack.getCount());
106+
sourceItemStack.shrink(itemCount);
107+
destItemStack.grow(itemCount);
108+
109+
// if the output stack was merged completely, remove it and stop looking
110+
if(sourceItemStack.isEmpty()) {
111+
sourceItemStacks.remove();
112+
break;
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}

src/main/java/gregtech/common/metatileentities/multi/electric/MetaTileEntityMultiFurnace.java

Lines changed: 132 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
11
package gregtech.common.metatileentities.multi.electric;
22

3-
import gregtech.api.capability.IMultipleTankHandler;
4-
import gregtech.api.capability.impl.MultiblockRecipeLogic;
5-
import gregtech.api.metatileentity.MetaTileEntity;
6-
import gregtech.api.metatileentity.MetaTileEntityHolder;
7-
import gregtech.api.metatileentity.multiblock.IMultiblockPart;
8-
import gregtech.api.metatileentity.multiblock.MultiblockAbility;
9-
import gregtech.api.metatileentity.multiblock.RecipeMapMultiblockController;
10-
import gregtech.api.multiblock.BlockPattern;
11-
import gregtech.api.multiblock.FactoryBlockPattern;
12-
import gregtech.api.multiblock.PatternMatchContext;
13-
import gregtech.api.recipes.CountableIngredient;
14-
import gregtech.api.recipes.Recipe;
15-
import gregtech.api.recipes.RecipeMaps;
16-
import gregtech.api.render.ICubeRenderer;
17-
import gregtech.api.render.Textures;
18-
import gregtech.common.blocks.BlockMetalCasing.MetalCasingType;
19-
import gregtech.common.blocks.BlockWireCoil.CoilType;
20-
import gregtech.common.blocks.MetaBlocks;
21-
import net.minecraft.block.state.IBlockState;
22-
import net.minecraft.item.ItemStack;
23-
import net.minecraft.util.ResourceLocation;
24-
import net.minecraft.util.text.ITextComponent;
25-
import net.minecraft.util.text.TextComponentTranslation;
26-
import net.minecraftforge.items.IItemHandlerModifiable;
27-
28-
import java.util.ArrayList;
29-
import java.util.Collections;
30-
import java.util.List;
3+
import gregtech.api.capability.*;
4+
import gregtech.api.capability.impl.*;
5+
import gregtech.api.metatileentity.*;
6+
import gregtech.api.metatileentity.multiblock.*;
7+
import gregtech.api.multiblock.*;
8+
import gregtech.api.recipes.*;
9+
import gregtech.api.render.*;
10+
import gregtech.api.util.*;
11+
import gregtech.common.blocks.BlockMetalCasing.*;
12+
import gregtech.common.blocks.BlockWireCoil.*;
13+
import gregtech.common.blocks.*;
14+
import net.minecraft.block.state.*;
15+
import net.minecraft.item.*;
16+
import net.minecraft.util.*;
17+
import net.minecraft.util.text.*;
18+
import net.minecraftforge.items.*;
19+
20+
import java.util.*;
3121

3222
public class MetaTileEntityMultiFurnace extends RecipeMapMultiblockController {
3323

@@ -130,40 +120,133 @@ protected void trySearchNewRecipe() {
130120
}
131121

132122
@Override
133-
protected Recipe findRecipe(long maxVoltage, IItemHandlerModifiable inputs, IMultipleTankHandler fluidInputs) {
123+
protected Recipe findRecipe(long maxVoltage,
124+
IItemHandlerModifiable inputs,
125+
IMultipleTankHandler fluidInputs)
126+
{
134127
int currentItemsEngaged = 0;
135-
int maxItemsLimit = 32 * heatingCoilLevel;
136-
ArrayList<CountableIngredient> recipeInputs = new ArrayList<>();
137-
ArrayList<ItemStack> recipeOutputs = new ArrayList<>();
138-
for (int index = 0; index < inputs.getSlots(); index++) {
139-
ItemStack stackInSlot = inputs.getStackInSlot(index);
140-
if (stackInSlot.isEmpty())
128+
final int maxItemsLimit = 32 * heatingCoilLevel;
129+
final ArrayList<CountableIngredient> recipeInputs = new ArrayList<>();
130+
final ArrayList<ItemStack> recipeOutputs = new ArrayList<>();
131+
132+
/* Iterate over the input items looking for more things to add until we run either out of input items
133+
* or we have exceeded the number of items permissible from the smelting bonus
134+
*/
135+
for(int index = 0; index < inputs.getSlots() && currentItemsEngaged < maxItemsLimit; index++) {
136+
137+
// Skip this slot if it is empty.
138+
final ItemStack currentInputItem = inputs.getStackInSlot(index);
139+
if(currentInputItem.isEmpty())
141140
continue;
141+
142+
// Determine if there is a valid recipe for this item. If not, skip it.
142143
Recipe matchingRecipe = recipeMap.findRecipe(maxVoltage,
143-
Collections.singletonList(stackInSlot), Collections.emptyList(), 0);
144-
CountableIngredient inputIngredient = matchingRecipe == null ? null : matchingRecipe.getInputs().get(0);
145-
if (inputIngredient != null && (maxItemsLimit - currentItemsEngaged) >= inputIngredient.getCount()) {
146-
ItemStack outputStack = matchingRecipe.getOutputs().get(0).copy();
147-
int overclockAmount = Math.min(stackInSlot.getCount() / inputIngredient.getCount(),
148-
(maxItemsLimit - currentItemsEngaged) / inputIngredient.getCount());
144+
Collections.singletonList(currentInputItem),
145+
Collections.emptyList(), 0);
146+
CountableIngredient inputIngredient;
147+
if(matchingRecipe != null)
148+
inputIngredient = matchingRecipe.getInputs().get(0);
149+
else
150+
continue;
151+
152+
// There's something not right with this recipe if the ingredient is null.
153+
if(inputIngredient == null)
154+
throw new IllegalStateException(
155+
String.format("Got recipe with null ingredient %s", matchingRecipe));
156+
157+
// If there are enough slots left to smelt this item stack
158+
int itemsLeftUntilMax = (maxItemsLimit - currentItemsEngaged);
159+
if(itemsLeftUntilMax >= inputIngredient.getCount()) {
160+
161+
/* Choose the lesser of the number of possible crafts in this ingredient's stack, or the number of
162+
* items remaining to reach the coil bonus's max smelted items.
163+
*/
164+
int craftsPossible = currentInputItem.getCount() / inputIngredient.getCount();
165+
int craftsUntilMax = itemsLeftUntilMax / inputIngredient.getCount();
166+
int recipeMultiplier = Math.min(craftsPossible, craftsUntilMax);
167+
168+
// copy the outputs list so we don't mutate it yet
169+
ArrayList<ItemStack> temp = new ArrayList<>(recipeOutputs);
170+
171+
// Process the stacks to see how many items this makes
172+
computeOutputItemStacks(temp, matchingRecipe.getOutputs().get(0), recipeMultiplier);
173+
174+
// determine if there is enough room in the output to fit all of this
175+
boolean canFitOutputs = InventoryUtils.simulateItemStackMerge(temp, this.getOutputInventory());
176+
177+
// if there isn't, we can't process this recipe.
178+
if(!canFitOutputs)
179+
break;
180+
181+
// otherwise, let's add the new output items and keep going
182+
temp.removeAll(recipeOutputs);
183+
recipeOutputs.addAll(temp);
184+
185+
// Add the ingredients to the list of things to smelt.
149186
recipeInputs.add(new CountableIngredient(inputIngredient.getIngredient(),
150-
inputIngredient.getCount() * overclockAmount));
151-
if (!outputStack.isEmpty()) {
152-
outputStack.setCount(outputStack.getCount() * overclockAmount);
153-
recipeOutputs.add(outputStack);
154-
}
155-
currentItemsEngaged += inputIngredient.getCount() * overclockAmount;
187+
inputIngredient.getCount() * recipeMultiplier));
188+
189+
currentItemsEngaged += inputIngredient.getCount() * recipeMultiplier;
156190
}
191+
}
157192

158-
if (currentItemsEngaged >= maxItemsLimit) break;
193+
// If there were no accepted ingredients, then there is no recipe to process.
194+
if(recipeInputs.isEmpty()) {
195+
//Set here to prevent recipe deadlock on world load with full output bus
196+
forceRecipeRecheck = true;
197+
return null;
159198
}
160-
return recipeInputs.isEmpty() ? null : recipeMap.recipeBuilder()
199+
200+
return recipeMap.recipeBuilder()
161201
.inputsIngredients(recipeInputs)
162202
.outputs(recipeOutputs)
163203
.EUt(Math.max(1, 16 / heatingCoilDiscount))
164204
.duration((int) Math.max(1.0, 256 * (currentItemsEngaged / (maxItemsLimit * 1.0))))
165205
.build().getResult();
166206
}
207+
208+
/**
209+
* Computes the minimal number of ItemStacks necessary to store a multiplied recipe output, then
210+
* generates the stacks. The result is then stored in {@code recipeOutputs}.
211+
*
212+
* @param recipeOutputs a collection of outputs to store the resulting output ItemStacks
213+
* @param outputStack an ItemStack representing the output item of a recipe
214+
* @param overclockAmount the number of times that {@code outputStack}'s quantity should
215+
* be multiplied by for the desired total
216+
*/
217+
private void computeOutputItemStacks(Collection<ItemStack> recipeOutputs,
218+
ItemStack outputStack,
219+
int overclockAmount)
220+
{
221+
if(!outputStack.isEmpty()) {
222+
// number of output items we're generating
223+
int finalAmount = outputStack.getCount() * overclockAmount;
224+
225+
// max items allowed in a stack
226+
int maxCount = outputStack.getMaxStackSize();
227+
228+
// number of whole stacks of output this will make
229+
int numStacks = finalAmount / maxCount;
230+
231+
// number of items left (partial stack)
232+
int remainder = finalAmount % maxCount;
233+
234+
// Add full stacks of the output item
235+
for(int fullStacks = numStacks; fullStacks > 0; fullStacks--) {
236+
ItemStack full = outputStack.copy();
237+
full.setCount(maxCount);
238+
recipeOutputs.add(full);
239+
}
240+
241+
// if there is a partial stack, add it too
242+
if(remainder > 0) {
243+
ItemStack partial = outputStack.copy();
244+
partial.setCount(remainder);
245+
recipeOutputs.add(partial);
246+
}
247+
}
248+
}
249+
167250
}
168251

169252
}

0 commit comments

Comments
 (0)