aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/gregtech/api/util/GTRecipe.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/gregtech/api/util/GTRecipe.java')
-rw-r--r--src/main/java/gregtech/api/util/GTRecipe.java1366
1 files changed, 1366 insertions, 0 deletions
diff --git a/src/main/java/gregtech/api/util/GTRecipe.java b/src/main/java/gregtech/api/util/GTRecipe.java
new file mode 100644
index 0000000000..7aa3dfbdfb
--- /dev/null
+++ b/src/main/java/gregtech/api/util/GTRecipe.java
@@ -0,0 +1,1366 @@
+package gregtech.api.util;
+
+import static gregtech.api.enums.GTValues.D2;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+import cpw.mods.fml.common.Loader;
+import cpw.mods.fml.common.ModContainer;
+import gregtech.GTMod;
+import gregtech.api.GregTechAPI;
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.Materials;
+import gregtech.api.logic.FluidInventoryLogic;
+import gregtech.api.logic.ItemInventoryLogic;
+import gregtech.api.metatileentity.implementations.MTEHatchInput;
+import gregtech.api.metatileentity.implementations.MTEHatchInputBus;
+import gregtech.api.metatileentity.implementations.MTEHatchMultiInput;
+import gregtech.api.objects.GTItemStack;
+import gregtech.api.objects.ItemData;
+import gregtech.api.recipe.RecipeCategory;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.RecipeMaps;
+import gregtech.api.recipe.RecipeMetadataKey;
+import gregtech.api.recipe.metadata.EmptyRecipeMetadataStorage;
+import gregtech.api.recipe.metadata.IRecipeMetadataStorage;
+import gregtech.api.util.extensions.ArrayExt;
+import gregtech.api.util.item.ItemHolder;
+import gregtech.common.tileentities.machines.MTEHatchInputBusME;
+import gregtech.common.tileentities.machines.MTEHatchInputME;
+import gregtech.nei.GTNEIDefaultHandler;
+import ic2.core.Ic2Items;
+import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import it.unimi.dsi.fastutil.objects.Reference2LongArrayMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
+
+public class GTRecipe implements Comparable<GTRecipe> {
+
+ private static ItemStack dataStick;
+ private static ItemStack dataOrb;
+ private static ItemStack ic2FluidCell;
+
+ public static void setItemStacks() {
+ ic2FluidCell = Ic2Items.FluidCell.copy();
+ dataStick = ItemList.Tool_DataStick.get(1L);
+ dataOrb = ItemList.Tool_DataOrb.get(1L);
+ }
+
+ /**
+ * If you want to change the Output, feel free to modify or even replace the whole ItemStack Array, for Inputs,
+ * please add a new Recipe, because of the HashMaps.
+ */
+ public ItemStack[] mInputs, mOutputs;
+ /**
+ * If you want to change the Output, feel free to modify or even replace the whole ItemStack Array, for Inputs,
+ * please add a new Recipe, because of the HashMaps.
+ */
+ public FluidStack[] mFluidInputs, mFluidOutputs;
+ /**
+ * If you changed the amount of Array-Items inside the Output Array then the length of this Array must be larger or
+ * equal to the Output Array. A chance of 10000 equals 100%
+ */
+ public int[] mChances;
+ /**
+ * An Item that needs to be inside the Special Slot, like for example the Copy Slot inside the Printer. This is only
+ * useful for Fake Recipes in NEI, since findRecipe() and containsInput() don't give a shit about this Field. Lists
+ * are also possible.
+ */
+ public Object mSpecialItems;
+
+ public int mDuration, mEUt, mSpecialValue;
+ /**
+ * Use this to just disable a specific Recipe, but the Configuration enables that already for every single Recipe.
+ */
+ public boolean mEnabled = true;
+ /**
+ * If this Recipe is hidden from NEI
+ */
+ public boolean mHidden = false;
+ /**
+ * If this Recipe is Fake and therefore doesn't get found by the findRecipe Function (It is still in the HashMaps,
+ * so that containsInput does return T on those fake Inputs)
+ */
+ public boolean mFakeRecipe = false;
+ /**
+ * If this Recipe can be stored inside a Machine in order to make Recipe searching more Efficient by trying the
+ * previously used Recipe first. In case you have a Recipe Map overriding things and returning one time use Recipes,
+ * you have to set this to F.
+ */
+ public boolean mCanBeBuffered = true;
+ /**
+ * If this Recipe needs the Output Slots to be completely empty. Needed in case you have randomised Outputs
+ */
+ public boolean mNeedsEmptyOutput = false;
+ /**
+ * If this is set to true, NBT equality is required for recipe check.
+ */
+ public boolean isNBTSensitive = false;
+ /**
+ * Used for describing recipes that do not fit the default recipe pattern (for example Large Boiler Fuels)
+ */
+ private String[] neiDesc = null;
+ /**
+ * Holds a set of metadata for this recipe.
+ */
+ @Nonnull
+ private final IRecipeMetadataStorage metadataStorage;
+ /**
+ * Category this recipe belongs to. Recipes belonging to recipemap are forced to have non-null category when added,
+ * otherwise it can be null.
+ */
+ private RecipeCategory recipeCategory;
+ /**
+ * Stores which mod added this recipe
+ */
+ public List<ModContainer> owners = new ArrayList<>();
+ /**
+ * Stores stack traces where this recipe was added
+ */
+ // BW wants to overwrite it, so no final
+ public List<List<String>> stackTraces = new ArrayList<>();
+
+ /** Used for simple cache validation */
+ private ItemStack[] inputsAtCacheTime = null;
+ /** Unified and type-merged stacks of mInputs, each item is guaranteed to be unique */
+ private RecipeItemInput[] mergedInputCache = null;
+ private static final RecipeItemInput[] EMPTY_INPUT_CACHE = new RecipeItemInput[0];
+
+ /** A single recipe input, used for an internal cache to speed up recipe matching */
+ public static final class RecipeItemInput {
+
+ /** Item count is ignored on this stack, do not mutate it either */
+ public final ItemStack unifiedStack;
+ /** Number of input items required */
+ public long inputAmount;
+ /** True if the input is NBT-sensitive */
+ public final boolean usesNbtMatching;
+
+ public RecipeItemInput(ItemStack stack, boolean recipeIsNBTSensitive) {
+ Objects.requireNonNull(stack);
+ this.inputAmount = stack.stackSize;
+ final boolean stackNeedsNBT = GTRecipe.shouldCheckNBT(stack);
+ this.usesNbtMatching = recipeIsNBTSensitive | stackNeedsNBT;
+ if (stackNeedsNBT) {
+ this.unifiedStack = stack;
+ } else {
+ this.unifiedStack = GTOreDictUnificator.get_nocopy(true, stack);
+ if (!this.usesNbtMatching) {
+ this.unifiedStack.setTagCompound(null);
+ }
+ }
+ }
+
+ /**
+ * @return True if the passed in stack is of the same item type as this input (respecting
+ * {@link RecipeItemInput#usesNbtMatching}).
+ */
+ public boolean matchesType(final ItemStack other) {
+ return GTUtility.areStacksEqual(this.unifiedStack, other, !usesNbtMatching);
+ }
+
+ /**
+ * @return True if the given input+oredict data for that input can be used as a valid recipe ingredient.
+ */
+ public boolean matchesRecipe(final ItemData oredictOther, final ItemStack other) {
+ if (usesNbtMatching) {
+ return GTUtility.areStacksEqual(this.unifiedStack, other, false);
+ } else {
+ return GTOreDictUnificator.isInputStackEqual(other, oredictOther, unifiedStack);
+ }
+ }
+ }
+
+ private GTRecipe(GTRecipe aRecipe, boolean shallow) {
+ mInputs = shallow ? aRecipe.mInputs : GTUtility.copyItemArray(aRecipe.mInputs);
+ mOutputs = shallow ? aRecipe.mOutputs : GTUtility.copyItemArray(aRecipe.mOutputs);
+ mSpecialItems = aRecipe.mSpecialItems;
+ mChances = aRecipe.mChances;
+ mFluidInputs = shallow ? aRecipe.mFluidInputs : GTUtility.copyFluidArray(aRecipe.mFluidInputs);
+ mFluidOutputs = shallow ? aRecipe.mFluidOutputs : GTUtility.copyFluidArray(aRecipe.mFluidOutputs);
+ mDuration = aRecipe.mDuration;
+ mSpecialValue = aRecipe.mSpecialValue;
+ mEUt = aRecipe.mEUt;
+ mNeedsEmptyOutput = aRecipe.mNeedsEmptyOutput;
+ isNBTSensitive = aRecipe.isNBTSensitive;
+ mCanBeBuffered = aRecipe.mCanBeBuffered;
+ mFakeRecipe = aRecipe.mFakeRecipe;
+ mEnabled = aRecipe.mEnabled;
+ mHidden = aRecipe.mHidden;
+ metadataStorage = EmptyRecipeMetadataStorage.INSTANCE;
+ owners = new ArrayList<>(aRecipe.owners);
+ reloadOwner();
+ }
+
+ /**
+ * Only for {@link GTRecipeBuilder}.
+ */
+ GTRecipe(ItemStack[] mInputs, ItemStack[] mOutputs, FluidStack[] mFluidInputs, FluidStack[] mFluidOutputs,
+ int[] mChances, Object mSpecialItems, int mDuration, int mEUt, int mSpecialValue, boolean mEnabled,
+ boolean mHidden, boolean mFakeRecipe, boolean mCanBeBuffered, boolean mNeedsEmptyOutput, boolean nbtSensitive,
+ String[] neiDesc, @Nullable IRecipeMetadataStorage metadataStorage, RecipeCategory recipeCategory) {
+ this.mInputs = mInputs;
+ this.mOutputs = mOutputs;
+ this.mFluidInputs = mFluidInputs;
+ this.mFluidOutputs = mFluidOutputs;
+ this.mChances = mChances;
+ this.mSpecialItems = mSpecialItems;
+ this.mDuration = mDuration;
+ this.mEUt = mEUt;
+ this.mSpecialValue = mSpecialValue;
+ this.mEnabled = mEnabled;
+ this.mHidden = mHidden;
+ this.mFakeRecipe = mFakeRecipe;
+ this.mCanBeBuffered = mCanBeBuffered;
+ this.mNeedsEmptyOutput = mNeedsEmptyOutput;
+ this.isNBTSensitive = nbtSensitive;
+ this.neiDesc = neiDesc;
+ this.metadataStorage = metadataStorage == null ? EmptyRecipeMetadataStorage.INSTANCE : metadataStorage.copy();
+ this.recipeCategory = recipeCategory;
+
+ reloadOwner();
+ }
+
+ public GTRecipe(boolean aOptimize, ItemStack[] aInputs, ItemStack[] aOutputs, Object aSpecialItems, int[] aChances,
+ FluidStack[] aFluidInputs, FluidStack[] aFluidOutputs, int aDuration, int aEUt, int aSpecialValue) {
+ if (aInputs == null) aInputs = new ItemStack[0];
+ if (aOutputs == null) aOutputs = new ItemStack[0];
+ if (aFluidInputs == null) aFluidInputs = new FluidStack[0];
+ if (aFluidOutputs == null) aFluidOutputs = new FluidStack[0];
+ if (aChances == null) aChances = new int[aOutputs.length];
+ if (aChances.length < aOutputs.length) aChances = Arrays.copyOf(aChances, aOutputs.length);
+
+ aInputs = ArrayExt.withoutTrailingNulls(aInputs, ItemStack[]::new);
+ aOutputs = ArrayExt.withoutTrailingNulls(aOutputs, ItemStack[]::new);
+ aFluidInputs = ArrayExt.withoutNulls(aFluidInputs, FluidStack[]::new);
+ aFluidOutputs = ArrayExt.withoutNulls(aFluidOutputs, FluidStack[]::new);
+
+ GTOreDictUnificator.setStackArray(true, aInputs);
+ GTOreDictUnificator.setStackArray(true, aOutputs);
+
+ for (ItemStack tStack : aOutputs) GTUtility.updateItemStack(tStack);
+
+ for (int i = 0; i < aChances.length; i++) if (aChances[i] <= 0) aChances[i] = 10000;
+ for (int i = 0; i < aFluidInputs.length; i++) aFluidInputs[i] = aFluidInputs[i].copy();
+ for (int i = 0; i < aFluidOutputs.length; i++) aFluidOutputs[i] = aFluidOutputs[i].copy();
+
+ if (aOptimize && aDuration >= 32) {
+ ArrayList<ItemStack> tList = new ArrayList<>();
+ tList.addAll(Arrays.asList(aInputs));
+ tList.addAll(Arrays.asList(aOutputs));
+ for (int i = 0; i < tList.size(); i++) if (tList.get(i) == null) tList.remove(i--);
+
+ for (byte i = (byte) Math.min(64, aDuration / 16); i > 1; i--) if (aDuration / i >= 16) {
+ boolean temp = true;
+ for (ItemStack stack : tList) if (stack.stackSize % i != 0) {
+ temp = false;
+ break;
+ }
+ if (temp) for (FluidStack aFluidInput : aFluidInputs) if (aFluidInput.amount % i != 0) {
+ temp = false;
+ break;
+ }
+ if (temp) for (FluidStack aFluidOutput : aFluidOutputs) if (aFluidOutput.amount % i != 0) {
+ temp = false;
+ break;
+ }
+ if (temp) {
+ for (ItemStack itemStack : tList) itemStack.stackSize /= i;
+ for (FluidStack aFluidInput : aFluidInputs) aFluidInput.amount /= i;
+ for (FluidStack aFluidOutput : aFluidOutputs) aFluidOutput.amount /= i;
+ aDuration /= i;
+ }
+ }
+ }
+
+ mInputs = aInputs;
+ mOutputs = aOutputs;
+ mSpecialItems = aSpecialItems;
+ mChances = aChances;
+ mFluidInputs = aFluidInputs;
+ mFluidOutputs = aFluidOutputs;
+ mDuration = aDuration;
+ mSpecialValue = aSpecialValue;
+ mEUt = aEUt;
+ metadataStorage = EmptyRecipeMetadataStorage.INSTANCE;
+ // checkCellBalance();
+ reloadOwner();
+ }
+
+ // aSpecialValue = EU per Liter! If there is no Liquid for this Object, then it gets multiplied with 1000!
+ public GTRecipe(ItemStack aInput1, ItemStack aOutput1, ItemStack aOutput2, ItemStack aOutput3, ItemStack aOutput4,
+ int aSpecialValue, int aType) {
+ this(
+ true,
+ new ItemStack[] { aInput1 },
+ new ItemStack[] { aOutput1, aOutput2, aOutput3, aOutput4 },
+ null,
+ null,
+ null,
+ null,
+ 0,
+ 0,
+ Math.max(1, aSpecialValue));
+
+ if (mInputs.length > 0 && aSpecialValue > 0) {
+ switch (aType) {
+ // Diesel Generator
+ case 0 -> {
+ RecipeMaps.dieselFuels.addRecipe(this);
+ RecipeMaps.largeBoilerFakeFuels.getBackend()
+ .addDieselRecipe(this);
+ }
+ // Gas Turbine
+ case 1 -> RecipeMaps.gasTurbineFuels.addRecipe(this);
+
+ // Thermal Generator
+ case 2 -> RecipeMaps.hotFuels.addRecipe(this);
+
+ // Plasma Generator
+ case 4 -> RecipeMaps.plasmaFuels.addRecipe(this);
+
+ // Magic Generator
+ case 5 -> RecipeMaps.magicFuels.addRecipe(this);
+
+ // Fluid Generator. Usually 3. Every wrong Type ends up in the Semifluid Generator
+ default -> {
+ RecipeMaps.denseLiquidFuels.addRecipe(this);
+ RecipeMaps.largeBoilerFakeFuels.getBackend()
+ .addDenseLiquidRecipe(this);
+ }
+ }
+ }
+ }
+
+ // Dummy GTRecipe maker...
+ public GTRecipe(ItemStack[] aInputs, ItemStack[] aOutputs, Object aSpecialItems, int[] aChances,
+ FluidStack[] aFluidInputs, FluidStack[] aFluidOutputs, int aDuration, int aEUt, int aSpecialValue) {
+ this(
+ true,
+ aInputs,
+ aOutputs,
+ aSpecialItems,
+ aChances,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue);
+ }
+
+ /**
+ * Re-unificates all the items present in recipes.
+ */
+ public static void reInit() {
+ GTLog.out.println("GTMod: Re-Unificating Recipes.");
+ for (RecipeMap<?> map : RecipeMap.ALL_RECIPE_MAPS.values()) {
+ map.getBackend()
+ .reInit();
+ }
+ }
+
+ public ItemStack getRepresentativeInput(int aIndex) {
+ if (aIndex < 0 || aIndex >= mInputs.length) return null;
+ return GTUtility.copyOrNull(mInputs[aIndex]);
+ }
+
+ public ItemStack getOutput(int aIndex) {
+ if (aIndex < 0 || aIndex >= mOutputs.length) return null;
+ return GTUtility.copyOrNull(mOutputs[aIndex]);
+ }
+
+ /**
+ * Dictates the ItemStacks displayed in the output slots of any NEI page handled by the default GT NEI handler.
+ * Override to make shown items differ from a GTRecipe's item output array
+ *
+ * @see GTNEIDefaultHandler
+ * @param i Slot index
+ * @return ItemStack to be displayed in the slot
+ */
+ public ItemStack getRepresentativeOutput(int i) {
+ return getOutput(i);
+ }
+
+ public int getOutputChance(int aIndex) {
+ if (mChances == null) return 10000;
+ if (aIndex < 0 || aIndex >= mChances.length) return 10000;
+ return mChances[aIndex];
+ }
+
+ public FluidStack getRepresentativeFluidInput(int aIndex) {
+ if (aIndex < 0 || aIndex >= mFluidInputs.length || mFluidInputs[aIndex] == null) return null;
+ return mFluidInputs[aIndex].copy();
+ }
+
+ public FluidStack getFluidOutput(int aIndex) {
+ if (aIndex < 0 || aIndex >= mFluidOutputs.length || mFluidOutputs[aIndex] == null) return null;
+ return mFluidOutputs[aIndex].copy();
+ }
+
+ public void checkCellBalance() {
+ if (!D2 || mInputs.length < 1) return;
+
+ int tInputAmount = GTModHandler.getCapsuleCellContainerCountMultipliedWithStackSize(mInputs);
+ int tOutputAmount = GTModHandler.getCapsuleCellContainerCountMultipliedWithStackSize(mOutputs);
+
+ if (tInputAmount < tOutputAmount) {
+ if (!Materials.Tin.contains(mInputs)) {
+ GTLog.err.println("You get more Cells, than you put in? There must be something wrong.");
+ new Exception().printStackTrace(GTLog.err);
+ }
+ } else if (tInputAmount > tOutputAmount) {
+ if (!Materials.Tin.contains(mOutputs)) {
+ GTLog.err.println("You get less Cells, than you put in? GT Machines usually don't destroy Cells.");
+ new Exception().printStackTrace(GTLog.err);
+ }
+ }
+ }
+
+ public GTRecipe copy() {
+ return new GTRecipe(this, false);
+ }
+
+ public GTRecipe copyShallow() {
+ return new GTRecipe(this, true);
+ }
+
+ public boolean isRecipeInputEqual(boolean aDecreaseStacksizeBySuccess, FluidStack[] aFluidInputs,
+ ItemStack... aInputs) {
+ return isRecipeInputEqual(aDecreaseStacksizeBySuccess, false, 1, aFluidInputs, aInputs);
+ }
+
+ // For non-multiplied recipe amount values
+ public boolean isRecipeInputEqual(boolean aDecreaseStacksizeBySuccess, boolean aDontCheckStackSizes,
+ FluidStack[] aFluidInputs, ItemStack... aInputs) {
+ return isRecipeInputEqual(aDecreaseStacksizeBySuccess, aDontCheckStackSizes, 1, aFluidInputs, aInputs);
+ }
+
+ /**
+ * Okay, did some code archeology to figure out what's going on here.
+ *
+ * <p>
+ * This variable was added in <a
+ * href=https://github.com/GTNewHorizons/GT5-Unofficial/commit/9959ab7443982a19ad329bca424ab515493432e9>this
+ * commit,</a> in order to fix the issues mentioned in <a
+ * href=https://github.com/GTNewHorizons/GT5-Unofficial/pull/183>the PR</a>.
+ *
+ * <p>
+ * It looks like it controls checking NBT. At this point, since we are still using universal fluid cells which store
+ * their fluids in NBT, it probably will not be safe to disable the NBT checks in the near future. Data sticks may
+ * be another case. Anyway, we probably can't get rid of this without some significant changes to clean up recipe
+ * inputs.
+ */
+ public static boolean GTppRecipeHelper;
+
+ /**
+ * @return Computes a (cached) array of all input items, combined by type into stacks. Do not mutate.
+ */
+ private @NotNull RecipeItemInput @NotNull [] getCachedCombinedItemInputs() {
+ if (mergedInputCache != null) {
+ if (mInputs != inputsAtCacheTime) {
+ throw new IllegalStateException(
+ "Inputs to this recipe have been modified since first recipe match: " + this);
+ }
+ return mergedInputCache;
+ }
+
+ synchronized (this) {
+ // In case another thread initialized it while this synchronized block was locked:
+ if (mergedInputCache != null) {
+ if (mInputs != inputsAtCacheTime) {
+ throw new IllegalStateException(
+ "Inputs to this recipe have been modified since first recipe match: " + this);
+ }
+ return mergedInputCache;
+ }
+
+ final ItemStack[] inputs = mInputs;
+ inputsAtCacheTime = inputs;
+ if (inputs == null || inputs.length == 0) {
+ mergedInputCache = EMPTY_INPUT_CACHE;
+ return mergedInputCache;
+ }
+ final ObjectArrayList<@NotNull RecipeItemInput> newCache = ObjectArrayList
+ .wrap(new RecipeItemInput[inputs.length], 0);
+ for (final ItemStack itemStack : inputs) {
+ if (itemStack == null) continue;
+ final RecipeItemInput existingInput = newCache.stream()
+ .filter(existing -> existing.matchesType(itemStack))
+ .findAny()
+ .orElse(null);
+ if (existingInput == null) {
+ newCache.add(new RecipeItemInput(itemStack, isNBTSensitive));
+ } else {
+ existingInput.inputAmount = Math.addExact(existingInput.inputAmount, itemStack.stackSize);
+ }
+ }
+ final RecipeItemInput[] frozenCache = newCache.toArray(new RecipeItemInput[0]);
+ if (GregTechAPI.sFullLoadFinished) {
+ mergedInputCache = frozenCache;
+ }
+ return frozenCache;
+ }
+ }
+
+ /**
+ * WARNING: Do not call this method with both {@code aDecreaseStacksizeBySuccess} and {@code aDontCheckStackSizes}
+ * set to {@code true}! You'll get weird behavior.
+ */
+ public boolean isRecipeInputEqual(boolean aDecreaseStacksizeBySuccess, boolean aDontCheckStackSizes,
+ int amountMultiplier, FluidStack[] aFluidInputs, ItemStack... aInputs) {
+ double maxParallel = maxParallelCalculatedByInputs(amountMultiplier, aFluidInputs, aInputs);
+ if (aDontCheckStackSizes) {
+ return maxParallel > 0;
+ } else if (maxParallel >= amountMultiplier) {
+ if (aDecreaseStacksizeBySuccess) {
+ consumeInput(amountMultiplier, aFluidInputs, aInputs);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * WARNING: Ensure that item inputs and fluid inputs are enough to be consumed with
+ * {@link #maxParallelCalculatedByInputs} before calling this method!
+ */
+ public void consumeInput(int amountMultiplier, FluidStack[] aFluidInputs, ItemStack... aInputs) {
+ if (amountMultiplier <= 0) return;
+
+ if (aFluidInputs != null) {
+ for (FluidStack recipeFluidCost : mFluidInputs) {
+ if (recipeFluidCost != null) {
+ long remainingCost = (long) recipeFluidCost.amount * amountMultiplier;
+
+ for (FluidStack providedFluid : aFluidInputs) {
+ if (providedFluid != null && providedFluid.isFluidEqual(recipeFluidCost)) {
+ if (providedFluid.amount >= remainingCost) {
+ providedFluid.amount -= remainingCost;
+ break;
+ } else {
+ remainingCost -= providedFluid.amount;
+ providedFluid.amount = 0;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (aInputs == null || aInputs.length == 0) {
+ return;
+ }
+
+ final ItemData[] unifiedProvidedInputs = new ItemData[aInputs.length];
+ for (int i = 0; i < aInputs.length; i++) {
+ unifiedProvidedInputs[i] = GTOreDictUnificator.getAssociation(aInputs[i]);
+ }
+ final @NotNull RecipeItemInput @NotNull [] combinedInputs = getCachedCombinedItemInputs();
+
+ for (final RecipeItemInput recipeItemCost : combinedInputs) {
+ long remainingCost = recipeItemCost.inputAmount * amountMultiplier;
+
+ for (int iProvided = 0; iProvided < aInputs.length && remainingCost > 0; iProvided++) {
+ final ItemStack providedItem = aInputs[iProvided];
+ if (providedItem == null || providedItem.stackSize == 0) {
+ continue;
+ }
+
+ final ItemData providedUnifiedItem = unifiedProvidedInputs[iProvided];
+ if (!recipeItemCost.matchesRecipe(providedUnifiedItem, providedItem)) {
+ continue;
+ }
+
+ if (providedItem.stackSize >= remainingCost) {
+ providedItem.stackSize -= (int) remainingCost;
+ break;
+ } else {
+ remainingCost -= providedItem.stackSize;
+ providedItem.stackSize = 0;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the number of parallel recipes, or 0 if recipe is not satisfied at all. 0 < number < 1 means that inputs
+ * are found but not enough.
+ */
+ public double maxParallelCalculatedByInputs(int maxParallel, FluidStack[] aFluidInputs, ItemStack... aInputs) {
+ if (mInputs.length > 0 && aInputs == null) return 0;
+ if (mFluidInputs.length > 0 && aFluidInputs == null) return 0;
+
+ double currentParallel = maxParallel;
+
+ // We need to have any fluids inputs, otherwise the code below does nothing. The second check is always true
+ // because of early exit condition above.
+ if (mFluidInputs.length > 0 /* && aFluidInputs != null */) {
+ // Create map for fluid -> stored amount
+ Reference2LongMap<Fluid> fluidMap = new Reference2LongArrayMap<>(2);
+ Reference2LongMap<Fluid> fluidCost = new Reference2LongArrayMap<>(2);
+ for (FluidStack fluidStack : aFluidInputs) {
+ if (fluidStack == null) continue;
+ fluidMap.mergeLong(fluidStack.getFluid(), fluidStack.amount, Long::sum);
+ }
+ for (FluidStack fluidStack : mFluidInputs) {
+ if (fluidStack == null) continue;
+ fluidCost.mergeLong(fluidStack.getFluid(), fluidStack.amount, Long::sum);
+ }
+
+ // Check how many parallels can it perform for each fluid
+ for (Reference2LongMap.Entry<Fluid> costEntry : fluidCost.reference2LongEntrySet()) {
+ if (costEntry.getLongValue() > 0) {
+ currentParallel = Math.min(
+ currentParallel,
+ (double) fluidMap.getOrDefault(costEntry.getKey(), 0L) / costEntry.getLongValue());
+ }
+ if (currentParallel <= 0) {
+ return 0;
+ }
+ }
+ }
+
+ if (mInputs.length > 0) {
+ final @NotNull RecipeItemInput @NotNull [] combinedInputs = getCachedCombinedItemInputs();
+
+ if (aInputs.length < combinedInputs.length) {
+ // Fewer item types provided than required by the recipe, making it impossible to satisfy.
+ return 0;
+ }
+ final ItemData[] unifiedProvidedInputs = new ItemData[aInputs.length];
+ for (int i = 0; i < aInputs.length; i++) {
+ unifiedProvidedInputs[i] = GTOreDictUnificator.getAssociation(aInputs[i]);
+ }
+
+ recipeItemLoop: for (final RecipeItemInput combinedInput : combinedInputs) {
+ double remainingCost = combinedInput.inputAmount * currentParallel;
+ long providedAmount = 0;
+
+ for (int i = 0; i < unifiedProvidedInputs.length; i++) {
+ final ItemData providedUnifiedItem = unifiedProvidedInputs[i];
+ final ItemStack providedItem = aInputs[i];
+ if (!combinedInput.matchesRecipe(providedUnifiedItem, providedItem)) {
+ continue;
+ }
+
+ providedAmount += providedItem.stackSize;
+
+ if (providedAmount >= remainingCost) {
+ continue recipeItemLoop;
+ }
+ }
+ if (providedAmount == 0) {
+ return 0;
+ }
+ currentParallel = Math.min(currentParallel, (double) providedAmount / combinedInput.inputAmount);
+ }
+ }
+ return currentParallel;
+ }
+
+ /**
+ * Please see JavaDoc on {@link #GTppRecipeHelper} for why this is here.
+ */
+ private static boolean shouldCheckNBT(ItemStack item) {
+ if (GTppRecipeHelper) {
+ return GTUtility.areStacksEqual(item, ic2FluidCell, true) || GTUtility.areStacksEqual(item, dataStick, true)
+ || GTUtility.areStacksEqual(item, dataOrb, true);
+ }
+ return false;
+ }
+
+ public boolean isRecipePossible(@Nullable ItemInventoryLogic itemInput, @Nullable FluidInventoryLogic fluidInput) {
+ return getAmountOfRecipesDone(itemInput, fluidInput, 1, true) > 0;
+ }
+
+ public long getAmountOfRecipesDone(@Nullable ItemInventoryLogic itemInput, @Nullable FluidInventoryLogic fluidInput,
+ long maxParallel, boolean simulate) {
+ if (itemInput == null) {
+ itemInput = new ItemInventoryLogic(0);
+ }
+
+ if (fluidInput == null) {
+ fluidInput = new FluidInventoryLogic(0, 0);
+ }
+
+ itemInput.startRecipeCheck();
+ Map<ItemHolder, Long> recipeItems = getItemInputsAsItemMap();
+ for (Entry<ItemHolder, Long> entry : recipeItems.entrySet()) {
+ maxParallel = Math
+ .min(maxParallel, itemInput.calculateAmountOfTimesItemCanBeTaken(entry.getKey(), entry.getValue()));
+ }
+
+ for (FluidStack fluid : mFluidInputs) {
+ if (fluid == null) continue;
+ maxParallel = Math
+ .min(maxParallel, fluidInput.calculateAmountOfTimesFluidCanBeTaken(fluid.getFluid(), fluid.amount));
+ }
+
+ if (simulate) {
+ itemInput.stopRecipeCheck();
+ return maxParallel;
+ }
+
+ for (Entry<ItemHolder, Long> entry : recipeItems.entrySet()) {
+ itemInput.subtractItemAmount(entry.getKey(), entry.getValue() * maxParallel, false);
+ }
+
+ for (FluidStack fluid : mFluidInputs) {
+ if (fluid == null) continue;
+ fluidInput.drain(fluid.getFluid(), fluid.amount * maxParallel, false);
+ }
+ itemInput.stopRecipeCheck();
+ return maxParallel;
+ }
+
+ private Map<ItemHolder, Long> getItemInputsAsItemMap() {
+ Map<ItemHolder, Long> items = new HashMap<>();
+ for (ItemStack item : mInputs) {
+ if (item == null) continue;
+ ItemHolder itemHolder = new ItemHolder(item);
+ items.put(itemHolder, items.getOrDefault(itemHolder, 0L) + item.stackSize);
+ }
+ return items;
+ }
+
+ @Override
+ public int compareTo(GTRecipe recipe) {
+ // first lowest tier recipes
+ // then fastest
+ // then with lowest special value
+ // then dry recipes
+ // then with fewer inputs
+ if (this.mEUt != recipe.mEUt) {
+ return Integer.compare(this.mEUt, recipe.mEUt);
+ } else if (this.mDuration != recipe.mDuration) {
+ return Integer.compare(this.mDuration, recipe.mDuration);
+ } else if (this.mSpecialValue != recipe.mSpecialValue) {
+ return Integer.compare(this.mSpecialValue, recipe.mSpecialValue);
+ } else if (this.mFluidInputs.length != recipe.mFluidInputs.length) {
+ return Integer.compare(this.mFluidInputs.length, recipe.mFluidInputs.length);
+ } else if (this.mInputs.length != recipe.mInputs.length) {
+ return Integer.compare(this.mInputs.length, recipe.mInputs.length);
+ }
+ return 0;
+ }
+
+ public String[] getNeiDesc() {
+ return neiDesc;
+ }
+
+ /**
+ * Sets description shown on NEI. <br>
+ * If you have a large number of recipes for the recipemap, this is not efficient memory wise, so use
+ * {@link gregtech.api.recipe.RecipeMapBuilder#neiSpecialInfoFormatter} instead.
+ */
+ public void setNeiDesc(String... neiDesc) {
+ this.neiDesc = neiDesc;
+ }
+
+ // region metadata
+
+ // Don't try implementing setMetadata, as metadataStorage can be EmptyRecipeMetadataStorage
+
+ /**
+ * Gets metadata associated with this recipe. Can return null. Use
+ * {@link #getMetadataOrDefault(RecipeMetadataKey, Object)}
+ * if you want to specify default value.
+ */
+ @Nullable
+ public <T> T getMetadata(RecipeMetadataKey<T> key) {
+ return key.cast(metadataStorage.getMetadata(key));
+ }
+
+ /**
+ * Gets metadata associated with this recipe with default value. Does not return null unless default value is null.
+ */
+ @Contract("_, !null -> !null")
+ @Nullable
+ public <T> T getMetadataOrDefault(RecipeMetadataKey<T> key, @Nullable T defaultValue) {
+ return key.cast(metadataStorage.getMetadataOrDefault(key, defaultValue));
+ }
+
+ @Nonnull
+ public IRecipeMetadataStorage getMetadataStorage() {
+ return metadataStorage;
+ }
+
+ // endregion
+
+ public RecipeCategory getRecipeCategory() {
+ return recipeCategory;
+ }
+
+ /**
+ * Exists only for recipe copying from external. For ordinal use case, use {@link GTRecipeBuilder#recipeCategory}.
+ */
+ public void setRecipeCategory(RecipeCategory recipeCategory) {
+ this.recipeCategory = recipeCategory;
+ }
+
+ private static final List<String> excludedStacktraces = Arrays.asList(
+ "java.lang.Thread",
+ "gregtech.api.interfaces.IRecipeMap",
+ "gregtech.api.interfaces.IRecipeMap$1",
+ "gregtech.api.recipe.RecipeMap",
+ "gregtech.api.recipe.RecipeMapBackend",
+ "gregtech.api.recipe.RecipeMapBackendPropertiesBuilder",
+ "gregtech.api.util.GTRecipe",
+ "gregtech.api.util.GTRecipeBuilder",
+ "gregtech.api.util.GTRecipeConstants",
+ "gregtech.api.util.GTRecipeMapUtil",
+ "gregtech.common.GTRecipeAdder");
+
+ public void reloadOwner() {
+ setOwner(
+ Loader.instance()
+ .activeModContainer());
+
+ if (GTMod.gregtechproxy.mNEIRecipeOwnerStackTrace) {
+ List<String> toAdd = new ArrayList<>();
+ for (StackTraceElement stackTrace : Thread.currentThread()
+ .getStackTrace()) {
+ if (excludedStacktraces.stream()
+ .noneMatch(
+ c -> stackTrace.getClassName()
+ .equals(c))) {
+ toAdd.add(formatStackTrace(stackTrace));
+ }
+ }
+ stackTraces.add(toAdd);
+ }
+ }
+
+ private static String formatStackTrace(StackTraceElement stackTraceElement) {
+ String raw = stackTraceElement.toString();
+ int startParen = raw.lastIndexOf('(');
+ int colon = raw.lastIndexOf(':');
+ if (colon == -1) {
+ // native or unknown source
+ return raw;
+ }
+ // strip class name and leave line number, as class name is already shown
+ return raw.substring(0, startParen + 1) + raw.substring(colon);
+ }
+
+ public void setOwner(ModContainer newOwner) {
+ ModContainer oldOwner = !owners.isEmpty() ? this.owners.get(owners.size() - 1) : null;
+ if (newOwner != null && newOwner != oldOwner) {
+ owners.add(newOwner);
+ }
+ }
+
+ /**
+ * Use in case {@link Loader#activeModContainer()} isn't helpful
+ */
+ public void setOwner(String modId) {
+ for (ModContainer mod : Loader.instance()
+ .getModList()) {
+ if (mod.getModId()
+ .equals(modId)) {
+ setOwner(mod);
+ return;
+ }
+ }
+ }
+
+ public GTRecipe setInputs(ItemStack... aInputs) {
+ // TODO determine if we need this without trailing nulls call
+ this.mInputs = ArrayExt.withoutTrailingNulls(aInputs, ItemStack[]::new);
+ return this;
+ }
+
+ public GTRecipe setOutputs(ItemStack... aOutputs) {
+ this.mOutputs = ArrayExt.withoutTrailingNulls(aOutputs, ItemStack[]::new);
+ return this;
+ }
+
+ public GTRecipe setFluidInputs(FluidStack... aInputs) {
+ this.mFluidInputs = ArrayExt.withoutTrailingNulls(aInputs, FluidStack[]::new);
+ return this;
+ }
+
+ public GTRecipe setFluidOutputs(FluidStack... aOutputs) {
+ this.mFluidOutputs = ArrayExt.withoutTrailingNulls(aOutputs, FluidStack[]::new);
+ return this;
+ }
+
+ public GTRecipe setDuration(int aDuration) {
+ this.mDuration = aDuration;
+ return this;
+ }
+
+ public GTRecipe setEUt(int aEUt) {
+ this.mEUt = aEUt;
+ return this;
+ }
+
+ public static class RecipeAssemblyLine {
+
+ public static final ArrayList<RecipeAssemblyLine> sAssemblylineRecipes = new ArrayList<>();
+
+ static {
+ if (!Boolean.getBoolean("com.gtnh.gt5u.ignore-invalid-assline-recipe"))
+ GregTechAPI.sFirstWorldTick.add(RecipeAssemblyLine::checkInvalidRecipes);
+ else GTLog.out.println("NOT CHECKING INVALID ASSLINE RECIPE.");
+ }
+
+ private static void checkInvalidRecipes() {
+ int invalidCount = 0;
+ GTLog.out.println("Started assline validation");
+ for (RecipeAssemblyLine recipe : sAssemblylineRecipes) {
+ if (recipe.getPersistentHash() == 0) {
+ invalidCount++;
+ GTLog.err.printf("Invalid recipe: %s%n", recipe);
+ }
+ }
+ if (invalidCount > 0) throw new RuntimeException(
+ "There are " + invalidCount + " invalid assembly line recipe(s)! Check GregTech.log for details!");
+ }
+
+ public ItemStack mResearchItem;
+ public int mResearchTime;
+ public ItemStack[] mInputs;
+ public FluidStack[] mFluidInputs;
+ public ItemStack mOutput;
+ public int mDuration;
+ public int mEUt;
+ public ItemStack[][] mOreDictAlt;
+ private int mPersistentHash;
+
+ /**
+ * THIS CONSTRUCTOR DOES SET THE PERSISTENT HASH.
+ * <p>
+ * if you set one yourself, it will give you one of the RunetimeExceptions!
+ */
+ public RecipeAssemblyLine(ItemStack aResearchItem, int aResearchTime, ItemStack[] aInputs,
+ FluidStack[] aFluidInputs, ItemStack aOutput, int aDuration, int aEUt) {
+ this(
+ aResearchItem,
+ aResearchTime,
+ aInputs,
+ aFluidInputs,
+ aOutput,
+ aDuration,
+ aEUt,
+ new ItemStack[aInputs.length][]);
+ int tPersistentHash = 1;
+ for (ItemStack tInput : aInputs)
+ tPersistentHash = tPersistentHash * 31 + GTUtility.persistentHash(tInput, true, false);
+ tPersistentHash = tPersistentHash * 31 + GTUtility.persistentHash(aResearchItem, true, false);
+ tPersistentHash = tPersistentHash * 31 + GTUtility.persistentHash(aOutput, true, false);
+ for (FluidStack tFluidInput : aFluidInputs)
+ tPersistentHash = tPersistentHash * 31 + GTUtility.persistentHash(tFluidInput, true, false);
+ tPersistentHash = tPersistentHash * 31 + aResearchTime;
+ tPersistentHash = tPersistentHash * 31 + aDuration;
+ tPersistentHash = tPersistentHash * 31 + aEUt;
+ setPersistentHash(tPersistentHash);
+ }
+
+ /**
+ * THIS CONSTRUCTOR DOES <b>NOT</b> SET THE PERSISTENT HASH.
+ * <p>
+ * if you don't set one yourself, it will break a lot of stuff!
+ */
+ public RecipeAssemblyLine(ItemStack aResearchItem, int aResearchTime, ItemStack[] aInputs,
+ FluidStack[] aFluidInputs, ItemStack aOutput, int aDuration, int aEUt, ItemStack[][] aAlt) {
+ mResearchItem = aResearchItem;
+ mResearchTime = aResearchTime;
+ mInputs = aInputs;
+ mFluidInputs = aFluidInputs;
+ mOutput = aOutput;
+ mDuration = aDuration;
+ mEUt = aEUt;
+ mOreDictAlt = aAlt;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ GTItemStack[] thisInputs = new GTItemStack[this.mInputs.length];
+ int totalInputStackSize = 0;
+ for (int i = 0; i < this.mInputs.length; i++) {
+ thisInputs[i] = new GTItemStack(this.mInputs[i]);
+ totalInputStackSize += thisInputs[i].mStackSize;
+ }
+ int inputHash = Arrays.deepHashCode(thisInputs);
+ int inputFluidHash = Arrays.deepHashCode(this.mFluidInputs);
+ GTItemStack thisOutput = new GTItemStack(mOutput);
+ GTItemStack thisResearch = new GTItemStack(mResearchItem);
+ int miscRecipeDataHash = Arrays.deepHashCode(
+ new Object[] { totalInputStackSize, mDuration, mEUt, thisOutput, thisResearch, mResearchTime });
+ result = prime * result + inputFluidHash;
+ result = prime * result + inputHash;
+ result = prime * result + miscRecipeDataHash;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RecipeAssemblyLine other)) {
+ return false;
+ }
+ if (this.mInputs.length != other.mInputs.length) {
+ return false;
+ }
+ if (this.mFluidInputs.length != other.mFluidInputs.length) {
+ return false;
+ }
+ // Check Outputs Match
+ GTItemStack output1 = new GTItemStack(this.mOutput);
+ GTItemStack output2 = new GTItemStack(other.mOutput);
+ if (!output1.equals(output2)) {
+ return false;
+ }
+ // Check Scanned Item Match
+ GTItemStack scan1 = new GTItemStack(this.mResearchItem);
+ GTItemStack scan2 = new GTItemStack(other.mResearchItem);
+ if (!scan1.equals(scan2)) {
+ return false;
+ }
+ // Check Items Match
+ GTItemStack[] thisInputs = new GTItemStack[this.mInputs.length];
+ GTItemStack[] otherInputs = new GTItemStack[other.mInputs.length];
+ for (int i = 0; i < thisInputs.length; i++) {
+ thisInputs[i] = new GTItemStack(this.mInputs[i]);
+ otherInputs[i] = new GTItemStack(other.mInputs[i]);
+ }
+ for (int i = 0; i < thisInputs.length; i++) {
+ if (!thisInputs[i].equals(otherInputs[i]) || thisInputs[i].mStackSize != otherInputs[i].mStackSize) {
+ return false;
+ }
+ }
+ // Check Fluids Match
+ for (int i = 0; i < this.mFluidInputs.length; i++) {
+ if (!this.mFluidInputs[i].isFluidStackIdentical(other.mFluidInputs[i])) {
+ return false;
+ }
+ }
+
+ return this.mDuration == other.mDuration && this.mEUt == other.mEUt
+ && this.mResearchTime == other.mResearchTime;
+ }
+
+ public int getPersistentHash() {
+ if (mPersistentHash == 0)
+ GTLog.err.println("Assline recipe persistent hash has not been set! Recipe: " + mOutput);
+ return mPersistentHash;
+ }
+
+ @Override
+ public String toString() {
+ return "GTRecipe_AssemblyLine{" + "mResearchItem="
+ + mResearchItem
+ + ", mResearchTime="
+ + mResearchTime
+ + ", mInputs="
+ + Arrays.toString(mInputs)
+ + ", mFluidInputs="
+ + Arrays.toString(mFluidInputs)
+ + ", mOutput="
+ + mOutput
+ + ", mDuration="
+ + mDuration
+ + ", mEUt="
+ + mEUt
+ + ", mOreDictAlt="
+ + Arrays.toString(mOreDictAlt)
+ + '}';
+ }
+
+ /**
+ * @param aPersistentHash the persistent hash. it should reflect the exact input used to generate this recipe If
+ * 0 is passed in, the actual persistent hash will be automatically remapped to 1
+ * instead.
+ * @throws IllegalStateException if the persistent hash has been set already
+ */
+ public void setPersistentHash(int aPersistentHash) {
+ if (this.mPersistentHash != 0) throw new IllegalStateException("Cannot set persistent hash twice!");
+ if (aPersistentHash == 0) this.mPersistentHash = 1;
+ else this.mPersistentHash = aPersistentHash;
+ }
+
+ /**
+ * @param inputBusses List of input busses to check.
+ * @return An array containing the amount of item to consume from the first slot of every input bus.
+ * {@code null} if at least one item fails to match the recipe ingredient.
+ */
+ public static int[] getItemConsumptionAmountArray(ArrayList<MTEHatchInputBus> inputBusses,
+ RecipeAssemblyLine recipe) {
+ int itemCount = recipe.mInputs.length;
+ if (itemCount == 0) return null;
+ int[] tStacks = new int[itemCount];
+ for (int i = 0; i < itemCount; i++) {
+ MTEHatchInputBus inputBus = inputBusses.get(i);
+ if (!inputBus.isValid()) return null;
+ ItemStack slotStack;
+ if (inputBus instanceof MTEHatchInputBusME meBus) {
+ slotStack = meBus.getShadowItemStack(0);
+ } else {
+ slotStack = inputBus.getStackInSlot(0);
+ }
+ if (slotStack == null) return null;
+
+ int amount = getMatchedIngredientAmount(slotStack, recipe.mInputs[i], recipe.mOreDictAlt[i]);
+ if (amount < 0) return null;
+
+ tStacks[i] = amount;
+ }
+ return tStacks;
+ }
+
+ public static int getMatchedIngredientAmount(ItemStack aSlotStack, ItemStack aIngredient, ItemStack[] alts) {
+ if (alts == null || alts.length == 0) {
+ if (GTUtility.areStacksEqual(aSlotStack, aIngredient, true)) {
+ return aIngredient.stackSize;
+ }
+ return -1;
+ }
+ for (ItemStack tAltStack : alts) {
+ if (GTUtility.areStacksEqual(aSlotStack, tAltStack, true)) {
+ return tAltStack.stackSize;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @param inputBusses Input bus list to check. Usually the input bus list of multi.
+ * @param itemConsumptions Should be generated by {@link RecipeAssemblyLine#getItemConsumptionAmountArray}.
+ * @Return The number of parallel recipes, or 0 if recipe is not satisfied at all. 0 < number < 1 means that
+ * inputs are found but not enough.
+ */
+ public static double maxParallelCalculatedByInputItems(ArrayList<MTEHatchInputBus> inputBusses, int maxParallel,
+ int[] itemConsumptions, Map<GTUtility.ItemId, ItemStack> inputsFromME) {
+ // Recipe item matching is done in the generation of itemConsumptions.
+
+ Map<GTUtility.ItemId, Long> itemConsumptionsFromME = new Object2LongOpenHashMap<>();
+ double currentParallel = maxParallel;
+
+ // Calculate the amount of each item to consume from ME
+ for (int i = 0; i < itemConsumptions.length; i++) {
+ MTEHatchInputBus inputBus = inputBusses.get(i);
+ if (!inputBus.isValid()) return 0;
+ if (inputBus instanceof MTEHatchInputBusME meBus) {
+ ItemStack item = meBus.getShadowItemStack(0);
+ if (item == null) return 0;
+ GTUtility.ItemId id = GTUtility.ItemId.createNoCopy(item);
+ itemConsumptionsFromME.merge(id, (long) itemConsumptions[i], Long::sum);
+ }
+ }
+ // Calculate parallel from ME input busses
+ for (Entry<GTUtility.ItemId, Long> entry : itemConsumptionsFromME.entrySet()) {
+ if (!inputsFromME.containsKey(entry.getKey())) return 0;
+ long consume = entry.getValue();
+ // For non-consumed inputs
+ if (consume == 0) continue;
+ currentParallel = Math
+ .min(currentParallel, (double) inputsFromME.get(entry.getKey()).stackSize / consume);
+ if (currentParallel <= 0) return 0;
+ }
+
+ // Calculate parallel from regular input busses
+ for (int i = 0; i < itemConsumptions.length; i++) {
+ MTEHatchInputBus inputBus = inputBusses.get(i);
+ if (!inputBus.isValid()) return 0;
+ if (inputBus instanceof MTEHatchInputBusME) continue;
+
+ ItemStack item = inputBus.getStackInSlot(0);
+ if (item == null) return 0;
+ // For non-consumed inputs
+ if (itemConsumptions[i] == 0) continue;
+ currentParallel = Math.min(currentParallel, (double) item.stackSize / itemConsumptions[i]);
+ if (currentParallel <= 0) return 0;
+ }
+ return currentParallel;
+ }
+
+ /**
+ * @param inputHatches Input hatch list to check. Usually the input hatch list of multi.
+ * @param fluidConsumptions Fluid inputs of the recipe.
+ * @return The number of parallel recipes, or 0 if recipe is not satisfied at all. 0 < number < 1 means that
+ * fluids are found but not enough.
+ */
+ public static double maxParallelCalculatedByInputFluids(ArrayList<MTEHatchInput> inputHatches, int maxParallel,
+ FluidStack[] fluidConsumptions, Map<Fluid, FluidStack> fluidsFromME) {
+ Map<Fluid, Long> fluidConsumptionsFromME = new Reference2LongOpenHashMap<>();
+ double currentParallel = maxParallel;
+
+ // Calculate the amount of each fluid to consume from ME
+ for (int i = 0; i < fluidConsumptions.length; i++) {
+ MTEHatchInput inputHatch = inputHatches.get(i);
+ if (!inputHatch.isValid()) return 0;
+ if (inputHatch instanceof MTEHatchInputME meHatch) {
+ FluidStack fluid = meHatch.getShadowFluidStack(0);
+ if (fluid == null) return 0;
+ if (!GTUtility.areFluidsEqual(fluid, fluidConsumptions[i])) return 0;
+ fluidConsumptionsFromME.merge(fluid.getFluid(), (long) fluidConsumptions[i].amount, Long::sum);
+ }
+ }
+ // Calculate parallel from ME input hatches
+ for (Entry<Fluid, Long> entry : fluidConsumptionsFromME.entrySet()) {
+ Fluid fluid = entry.getKey();
+ if (!fluidsFromME.containsKey(fluid)) return 0;
+ long consume = entry.getValue();
+ currentParallel = Math.min(currentParallel, (double) fluidsFromME.get(fluid).amount / consume);
+ if (currentParallel <= 0) return 0;
+ }
+
+ // Calculate parallel from regular input hatches
+ for (int i = 0; i < fluidConsumptions.length; i++) {
+ MTEHatchInput inputHatch = inputHatches.get(i);
+ if (!inputHatch.isValid()) return 0;
+ if (inputHatch instanceof MTEHatchInputME) continue;
+
+ FluidStack fluid;
+ if (inputHatch instanceof MTEHatchMultiInput multiInput) {
+ fluid = multiInput.getFluid(0);
+ } else {
+ fluid = inputHatch.getFillableStack();
+ }
+ if (fluid == null) return 0;
+ if (!GTUtility.areFluidsEqual(fluid, fluidConsumptions[i])) return 0;
+ currentParallel = Math.min(currentParallel, (double) fluid.amount / fluidConsumptions[i].amount);
+ if (currentParallel <= 0) return 0;
+ }
+ return currentParallel;
+ }
+
+ /**
+ * WARNING: Ensure that item inputs are enough to be consumed with
+ * {@link RecipeAssemblyLine#maxParallelCalculatedByInputItems} before calling this method!
+ *
+ * @param inputBusses Input bus list to check. Usually the input bus list of multi.
+ * @param itemConsumptions Should be generated by {@link RecipeAssemblyLine#getItemConsumptionAmountArray}.
+ */
+ public static void consumeInputItems(ArrayList<MTEHatchInputBus> inputBusses, int amountMultiplier,
+ int[] itemConsumptions, Map<GTUtility.ItemId, ItemStack> inputsFromME) {
+ for (int i = 0; i < itemConsumptions.length; i++) {
+ MTEHatchInputBus inputBus = inputBusses.get(i);
+ if (!inputBus.isValid()) continue;
+ ItemStack item;
+ if (inputBus instanceof MTEHatchInputBusME meBus) {
+ item = inputsFromME.get(GTUtility.ItemId.createNoCopy(meBus.getShadowItemStack(0)));
+ } else {
+ item = inputBus.getStackInSlot(0);
+ }
+ item.stackSize -= itemConsumptions[i] * amountMultiplier;
+ }
+ }
+
+ /**
+ * WARNING: Ensure that fluid inputs are enough to be consumed with
+ * {@link RecipeAssemblyLine#maxParallelCalculatedByInputFluids} before calling this method!
+ *
+ * @param inputHatches Input hatch list to check. Usually the input hatch list of multi.
+ * @param fluidConsumptions Fluid inputs of the recipe.
+ */
+ public static void consumeInputFluids(ArrayList<MTEHatchInput> inputHatches, int amountMultiplier,
+ FluidStack[] fluidConsumptions, Map<Fluid, FluidStack> fluidsFromME) {
+ for (int i = 0; i < fluidConsumptions.length; i++) {
+ MTEHatchInput inputHatch = inputHatches.get(i);
+ if (!inputHatch.isValid()) continue;
+ FluidStack fluid;
+ if (inputHatch instanceof MTEHatchInputME meHatch) {
+ fluid = fluidsFromME.get(
+ meHatch.getShadowFluidStack(0)
+ .getFluid());
+ } else if (inputHatch instanceof MTEHatchMultiInput multiInput) {
+ fluid = multiInput.getFluid(0);
+ } else {
+ fluid = inputHatch.getFillableStack();
+ }
+ fluid.amount -= fluidConsumptions[i].amount * amountMultiplier;
+ }
+ }
+ }
+
+ public static class GTRecipe_WithAlt extends GTRecipe {
+
+ public ItemStack[][] mOreDictAlt;
+
+ /**
+ * Only for {@link GTRecipeBuilder}.
+ */
+ GTRecipe_WithAlt(ItemStack[] mInputs, ItemStack[] mOutputs, FluidStack[] mFluidInputs,
+ FluidStack[] mFluidOutputs, int[] mChances, Object mSpecialItems, int mDuration, int mEUt,
+ int mSpecialValue, boolean mEnabled, boolean mHidden, boolean mFakeRecipe, boolean mCanBeBuffered,
+ boolean mNeedsEmptyOutput, boolean nbtSensitive, String[] neiDesc,
+ @Nullable IRecipeMetadataStorage metadataStorage, RecipeCategory recipeCategory,
+ ItemStack[][] mOreDictAlt) {
+ super(
+ mInputs,
+ mOutputs,
+ mFluidInputs,
+ mFluidOutputs,
+ mChances,
+ mSpecialItems,
+ mDuration,
+ mEUt,
+ mSpecialValue,
+ mEnabled,
+ mHidden,
+ mFakeRecipe,
+ mCanBeBuffered,
+ mNeedsEmptyOutput,
+ nbtSensitive,
+ neiDesc,
+ metadataStorage,
+ recipeCategory);
+ this.mOreDictAlt = mOreDictAlt;
+ }
+
+ public GTRecipe_WithAlt(boolean aOptimize, ItemStack[] aInputs, ItemStack[] aOutputs, Object aSpecialItems,
+ int[] aChances, FluidStack[] aFluidInputs, FluidStack[] aFluidOutputs, int aDuration, int aEUt,
+ int aSpecialValue, ItemStack[][] aAlt) {
+ super(
+ aOptimize,
+ aInputs,
+ aOutputs,
+ aSpecialItems,
+ aChances,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue);
+ mOreDictAlt = aAlt;
+ }
+
+ public Object getAltRepresentativeInput(int aIndex) {
+ if (aIndex < 0) return null;
+ if (aIndex < mOreDictAlt.length) {
+ if (mOreDictAlt[aIndex] != null && mOreDictAlt[aIndex].length > 0) {
+ ItemStack[] rStacks = new ItemStack[mOreDictAlt[aIndex].length];
+ for (int i = 0; i < mOreDictAlt[aIndex].length; i++) {
+ rStacks[i] = GTUtility.copyOrNull(mOreDictAlt[aIndex][i]);
+ }
+ return rStacks;
+ }
+ }
+ if (aIndex >= mInputs.length) return null;
+ return GTUtility.copyOrNull(mInputs[aIndex]);
+ }
+ }
+}