aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/gregtech/api/recipe/check
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/gregtech/api/recipe/check')
-rw-r--r--src/main/java/gregtech/api/recipe/check/CheckRecipeResult.java54
-rw-r--r--src/main/java/gregtech/api/recipe/check/CheckRecipeResultRegistry.java150
-rw-r--r--src/main/java/gregtech/api/recipe/check/ResultInsufficientHeat.java65
-rw-r--r--src/main/java/gregtech/api/recipe/check/ResultInsufficientMachineTier.java63
-rw-r--r--src/main/java/gregtech/api/recipe/check/ResultInsufficientPower.java64
-rw-r--r--src/main/java/gregtech/api/recipe/check/ResultInsufficientStartupPower.java63
-rw-r--r--src/main/java/gregtech/api/recipe/check/SimpleCheckRecipeResult.java102
-rw-r--r--src/main/java/gregtech/api/recipe/check/SingleRecipeCheck.java405
8 files changed, 966 insertions, 0 deletions
diff --git a/src/main/java/gregtech/api/recipe/check/CheckRecipeResult.java b/src/main/java/gregtech/api/recipe/check/CheckRecipeResult.java
new file mode 100644
index 0000000000..8af5c58f5e
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/check/CheckRecipeResult.java
@@ -0,0 +1,54 @@
+package gregtech.api.recipe.check;
+
+import javax.annotation.Nonnull;
+
+import net.minecraft.network.PacketBuffer;
+
+/**
+ * Class to indicate the result of recipe check in the machine. It doesn't need to be actual result of recipemap check,
+ * but can also be status of whether to start the machine. Examples can be found at {@link CheckRecipeResultRegistry}.
+ * <p>
+ * Sample instance must be registered to {@link CheckRecipeResultRegistry}.
+ */
+public interface CheckRecipeResult {
+
+ /**
+ * @return Unique registry ID
+ */
+ @Nonnull
+ String getID();
+
+ /**
+ * @return If recipe check is successful
+ */
+ boolean wasSuccessful();
+
+ /**
+ * @return Actual text to show on client GUI
+ */
+ @Nonnull
+ String getDisplayString();
+
+ /**
+ * Create new instance to receive packet.
+ */
+ @Nonnull
+ CheckRecipeResult newInstance();
+
+ /**
+ * Encode value to sync.
+ */
+ void encode(@Nonnull PacketBuffer buffer);
+
+ /**
+ * Decode synced value.
+ */
+ void decode(PacketBuffer buffer);
+
+ /**
+ * @return If this message should stay on GUI when the machine is shut down.
+ */
+ default boolean persistsOnShutdown() {
+ return false;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/check/CheckRecipeResultRegistry.java b/src/main/java/gregtech/api/recipe/check/CheckRecipeResultRegistry.java
new file mode 100644
index 0000000000..e141c39a67
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/check/CheckRecipeResultRegistry.java
@@ -0,0 +1,150 @@
+package gregtech.api.recipe.check;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+public final class CheckRecipeResultRegistry {
+
+ private static final Map<String, CheckRecipeResult> registry = new HashMap<>();
+
+ /**
+ * Registers CheckRecipeResult. No duplicated IDs are allowed.
+ *
+ * @param sample Sample object to register
+ */
+ public static void register(CheckRecipeResult sample) {
+ if (isRegistered(sample.getID())) {
+ throw new IllegalStateException(
+ String.format(
+ "ID %s is already registered for %s",
+ sample.getID(),
+ registry.get(sample.getID())
+ .getClass()
+ .getCanonicalName()));
+ }
+ registry.put(sample.getID(), sample);
+ }
+
+ public static CheckRecipeResult getSampleFromRegistry(String id) {
+ if (!isRegistered(id)) {
+ throw new RuntimeException("Unknown id: " + id);
+ }
+ return registry.get(id);
+ }
+
+ public static boolean isRegistered(String id) {
+ return registry.containsKey(id);
+ }
+
+ /**
+ * Successfully found recipe.
+ */
+ @Nonnull
+ public static final CheckRecipeResult SUCCESSFUL = SimpleCheckRecipeResult.ofSuccess("success");
+ /**
+ * All requirements met to generator power.
+ */
+ @Nonnull
+ public static final CheckRecipeResult GENERATING = SimpleCheckRecipeResult.ofSuccess("generating");
+ /**
+ * Cannot find recipe.
+ */
+ @Nonnull
+ public static final CheckRecipeResult NO_RECIPE = SimpleCheckRecipeResult.ofFailure("no_recipe");
+ /**
+ * Cannot process recipe because item output is full.
+ */
+ public static final CheckRecipeResult ITEM_OUTPUT_FULL = SimpleCheckRecipeResult.ofFailure("item_output_full");
+ /**
+ * Cannot process recipe because fluid output is full.
+ */
+ public static final CheckRecipeResult FLUID_OUTPUT_FULL = SimpleCheckRecipeResult.ofFailure("fluid_output_full");
+ /**
+ * Default unknown state.
+ */
+ @Nonnull
+ public static final CheckRecipeResult NONE = SimpleCheckRecipeResult.ofFailure("none");
+ /**
+ * Code crashed.
+ */
+ public static final CheckRecipeResult CRASH = SimpleCheckRecipeResult.ofFailurePersistOnShutdown("crash");
+ /**
+ * Cannot find valid fuel for generator.
+ */
+ @Nonnull
+ public static final CheckRecipeResult NO_FUEL_FOUND = SimpleCheckRecipeResult.ofFailure("no_fuel");
+ /**
+ * Cannot find valid turbine.
+ */
+ @Nonnull
+ public static final CheckRecipeResult NO_TURBINE_FOUND = SimpleCheckRecipeResult.ofFailure("no_turbine");
+ /**
+ * No data sticks found for Assembly Line.
+ */
+ @Nonnull
+ public static final CheckRecipeResult NO_DATA_STICKS = SimpleCheckRecipeResult.ofFailure("no_data_sticks");
+ /**
+ * EU/t overflowed.
+ */
+ @Nonnull
+ public static final CheckRecipeResult POWER_OVERFLOW = SimpleCheckRecipeResult.ofFailure("power_overflow");
+ /**
+ * Progress time overflowed.
+ */
+ @Nonnull
+ public static final CheckRecipeResult DURATION_OVERFLOW = SimpleCheckRecipeResult.ofFailure("duration_overflow");
+ /**
+ * Machine had an internal error
+ */
+ @Nonnull
+ public static final CheckRecipeResult INTERNAL_ERROR = SimpleCheckRecipeResult.ofFailure("internal_error");
+ /** Multiblock ore drill has no drilling fluid */
+ public static final CheckRecipeResult NO_DRILLING_FLUID = SimpleCheckRecipeResult.ofFailure("no_drilling_fluid");
+ /** Multiblock drill is missing mining pipe */
+ public static final CheckRecipeResult MISSING_MINING_PIPE = SimpleCheckRecipeResult.ofFailure("no_mining_pipe");
+ /** Concrete backfiller is out of concrete */
+ public static final CheckRecipeResult BACKFILLER_NO_CONCRETE = SimpleCheckRecipeResult
+ .ofFailure("backfiller_no_concrete");
+
+ /**
+ * Cannot process recipe because the machine cannot handle required EUt.
+ */
+ @Nonnull
+ public static CheckRecipeResult insufficientPower(long required) {
+ return new ResultInsufficientPower(required);
+ }
+
+ /**
+ * Cannot process recipe because the machine cannot handle its heat.
+ */
+ @Nonnull
+ public static CheckRecipeResult insufficientHeat(int required) {
+ return new ResultInsufficientHeat(required);
+ }
+
+ /**
+ * Cannot process recipe because the machine is tiered and its tier is too low.
+ */
+ @Nonnull
+ public static CheckRecipeResult insufficientMachineTier(int required) {
+ return new ResultInsufficientMachineTier(required);
+ }
+
+ /**
+ * Cannot process recipe because the machine doesn't have enough startup power.
+ */
+ @Nonnull
+ public static CheckRecipeResult insufficientStartupPower(int required) {
+ return new ResultInsufficientStartupPower(required);
+ }
+
+ static {
+ register(new SimpleCheckRecipeResult(false, "", false));
+ register(new ResultInsufficientPower(0));
+ register(new ResultInsufficientHeat(0));
+ register(new ResultInsufficientMachineTier(0));
+ register(new ResultInsufficientStartupPower(0));
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/check/ResultInsufficientHeat.java b/src/main/java/gregtech/api/recipe/check/ResultInsufficientHeat.java
new file mode 100644
index 0000000000..26c3530ba3
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/check/ResultInsufficientHeat.java
@@ -0,0 +1,65 @@
+package gregtech.api.recipe.check;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.StatCollector;
+
+import gregtech.api.enums.HeatingCoilLevel;
+import gregtech.api.util.GT_Utility;
+
+public class ResultInsufficientHeat implements CheckRecipeResult {
+
+ private int required;
+
+ ResultInsufficientHeat(int required) {
+ this.required = required;
+ }
+
+ @Override
+ @Nonnull
+ public String getID() {
+ return "insufficient_heat";
+ }
+
+ @Override
+ public boolean wasSuccessful() {
+ return false;
+ }
+
+ @Override
+ @Nonnull
+ public String getDisplayString() {
+ return Objects.requireNonNull(
+ StatCollector.translateToLocalFormatted(
+ "GT5U.gui.text.insufficient_heat",
+ GT_Utility.formatNumbers(required),
+ HeatingCoilLevel.getDisplayNameFromHeat(required, true)));
+ }
+
+ @Override
+ @Nonnull
+ public CheckRecipeResult newInstance() {
+ return new ResultInsufficientHeat(0);
+ }
+
+ @Override
+ public void encode(@Nonnull PacketBuffer buffer) {
+ buffer.writeVarIntToBuffer(required);
+ }
+
+ @Override
+ public void decode(@Nonnull PacketBuffer buffer) {
+ required = buffer.readVarIntFromBuffer();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResultInsufficientHeat that = (ResultInsufficientHeat) o;
+ return required == that.required;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/check/ResultInsufficientMachineTier.java b/src/main/java/gregtech/api/recipe/check/ResultInsufficientMachineTier.java
new file mode 100644
index 0000000000..742eb3ef7a
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/check/ResultInsufficientMachineTier.java
@@ -0,0 +1,63 @@
+package gregtech.api.recipe.check;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.StatCollector;
+
+import gregtech.api.util.GT_Utility;
+
+public class ResultInsufficientMachineTier implements CheckRecipeResult {
+
+ private int required;
+
+ ResultInsufficientMachineTier(int required) {
+ this.required = required;
+ }
+
+ @Override
+ @Nonnull
+ public String getID() {
+ return "insufficient_machine_tier";
+ }
+
+ @Override
+ public boolean wasSuccessful() {
+ return false;
+ }
+
+ @Override
+ @Nonnull
+ public String getDisplayString() {
+ return Objects.requireNonNull(
+ StatCollector.translateToLocalFormatted(
+ "GT5U.gui.text.insufficient_machine_tier",
+ GT_Utility.formatNumbers(required)));
+ }
+
+ @Override
+ @Nonnull
+ public CheckRecipeResult newInstance() {
+ return new ResultInsufficientMachineTier(0);
+ }
+
+ @Override
+ public void encode(@Nonnull PacketBuffer buffer) {
+ buffer.writeVarIntToBuffer(required);
+ }
+
+ @Override
+ public void decode(@Nonnull PacketBuffer buffer) {
+ required = buffer.readVarIntFromBuffer();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResultInsufficientMachineTier that = (ResultInsufficientMachineTier) o;
+ return required == that.required;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/check/ResultInsufficientPower.java b/src/main/java/gregtech/api/recipe/check/ResultInsufficientPower.java
new file mode 100644
index 0000000000..fdc06c0c07
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/check/ResultInsufficientPower.java
@@ -0,0 +1,64 @@
+package gregtech.api.recipe.check;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.StatCollector;
+
+import gregtech.api.util.GT_Utility;
+
+public class ResultInsufficientPower implements CheckRecipeResult {
+
+ private long required;
+
+ ResultInsufficientPower(long required) {
+ this.required = required;
+ }
+
+ @Override
+ @Nonnull
+ public String getID() {
+ return "insufficient_power";
+ }
+
+ @Override
+ public boolean wasSuccessful() {
+ return false;
+ }
+
+ @Override
+ @Nonnull
+ public String getDisplayString() {
+ return Objects.requireNonNull(
+ StatCollector.translateToLocalFormatted(
+ "GT5U.gui.text.insufficient_power",
+ GT_Utility.formatNumbers(required),
+ GT_Utility.getColoredTierNameFromVoltage(required)));
+ }
+
+ @Override
+ @Nonnull
+ public CheckRecipeResult newInstance() {
+ return new ResultInsufficientPower(0);
+ }
+
+ @Override
+ public void encode(@Nonnull PacketBuffer buffer) {
+ buffer.writeLong(required);
+ }
+
+ @Override
+ public void decode(@Nonnull PacketBuffer buffer) {
+ required = buffer.readLong();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResultInsufficientPower that = (ResultInsufficientPower) o;
+ return required == that.required;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/check/ResultInsufficientStartupPower.java b/src/main/java/gregtech/api/recipe/check/ResultInsufficientStartupPower.java
new file mode 100644
index 0000000000..62d2dd1fb2
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/check/ResultInsufficientStartupPower.java
@@ -0,0 +1,63 @@
+package gregtech.api.recipe.check;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.StatCollector;
+
+import gregtech.api.util.GT_Utility;
+
+public class ResultInsufficientStartupPower implements CheckRecipeResult {
+
+ private int required;
+
+ ResultInsufficientStartupPower(int required) {
+ this.required = required;
+ }
+
+ @Override
+ @Nonnull
+ public String getID() {
+ return "insufficient_startup_power";
+ }
+
+ @Override
+ public boolean wasSuccessful() {
+ return false;
+ }
+
+ @Override
+ @Nonnull
+ public String getDisplayString() {
+ return Objects.requireNonNull(
+ StatCollector.translateToLocalFormatted(
+ "GT5U.gui.text.insufficient_startup_power",
+ GT_Utility.formatNumbers(required)));
+ }
+
+ @Override
+ @Nonnull
+ public CheckRecipeResult newInstance() {
+ return new ResultInsufficientStartupPower(0);
+ }
+
+ @Override
+ public void encode(@Nonnull PacketBuffer buffer) {
+ buffer.writeVarIntToBuffer(required);
+ }
+
+ @Override
+ public void decode(@Nonnull PacketBuffer buffer) {
+ required = buffer.readVarIntFromBuffer();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResultInsufficientStartupPower that = (ResultInsufficientStartupPower) o;
+ return required == that.required;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/check/SimpleCheckRecipeResult.java b/src/main/java/gregtech/api/recipe/check/SimpleCheckRecipeResult.java
new file mode 100644
index 0000000000..58c85bbe9d
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/check/SimpleCheckRecipeResult.java
@@ -0,0 +1,102 @@
+package gregtech.api.recipe.check;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.StatCollector;
+
+import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils;
+
+/**
+ * Simple implementation of {@link CheckRecipeResult}. You can create new object without registering it.
+ */
+public class SimpleCheckRecipeResult implements CheckRecipeResult {
+
+ private boolean success;
+ private String key;
+ private boolean persistsOnShutdown;
+
+ SimpleCheckRecipeResult(boolean success, String key, boolean persistsOnShutdown) {
+ this.success = success;
+ this.key = key;
+ this.persistsOnShutdown = persistsOnShutdown;
+ }
+
+ @Override
+ public String getID() {
+ return "simple_result";
+ }
+
+ @Override
+ public boolean wasSuccessful() {
+ return success;
+ }
+
+ @Override
+ @Nonnull
+ public String getDisplayString() {
+ return Objects.requireNonNull(StatCollector.translateToLocal("GT5U.gui.text." + key));
+ }
+
+ @Override
+ @Nonnull
+ public CheckRecipeResult newInstance() {
+ return new SimpleCheckRecipeResult(false, "", false);
+ }
+
+ @Override
+ public void encode(@Nonnull PacketBuffer buffer) {
+ buffer.writeBoolean(success);
+ NetworkUtils.writeStringSafe(buffer, key);
+ buffer.writeBoolean(persistsOnShutdown);
+ }
+
+ @Override
+ public void decode(@Nonnull PacketBuffer buffer) {
+ success = buffer.readBoolean();
+ key = NetworkUtils.readStringSafe(buffer);
+ persistsOnShutdown = buffer.readBoolean();
+ }
+
+ @Override
+ public boolean persistsOnShutdown() {
+ return persistsOnShutdown;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SimpleCheckRecipeResult that = (SimpleCheckRecipeResult) o;
+ return success == that.success && Objects.equals(key, that.key)
+ && persistsOnShutdown == that.persistsOnShutdown;
+ }
+
+ /**
+ * Creates new result with successful state. Add your localized description with `GT5U.gui.text.{key}`.
+ * This is already registered to registry.
+ */
+ @Nonnull
+ public static CheckRecipeResult ofSuccess(String key) {
+ return new SimpleCheckRecipeResult(true, key, false);
+ }
+
+ /**
+ * Creates new result with failed state. Add your localized description with `GT5U.gui.text.{key}`.
+ * This is already registered to registry.
+ */
+ @Nonnull
+ public static CheckRecipeResult ofFailure(String key) {
+ return new SimpleCheckRecipeResult(false, key, false);
+ }
+
+ /**
+ * Creates new result object with failed state that does not get reset on shutdown. Add your localized description
+ * with `GT5U.gui.text.{key}`. This is already registered to registry.
+ */
+ public static CheckRecipeResult ofFailurePersistOnShutdown(String key) {
+ return new SimpleCheckRecipeResult(false, key, true);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/check/SingleRecipeCheck.java b/src/main/java/gregtech/api/recipe/check/SingleRecipeCheck.java
new file mode 100644
index 0000000000..8683812d84
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/check/SingleRecipeCheck.java
@@ -0,0 +1,405 @@
+package gregtech.api.recipe.check;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTBase;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraftforge.common.util.Constants;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidRegistry;
+import net.minecraftforge.fluids.FluidStack;
+
+import com.google.common.collect.ImmutableMap;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.GT_Utility.ItemId;
+
+/**
+ * Used by machines that are locked to a single recipe, for faster recipe check.
+ * <p>
+ * Computation time will be like these:
+ * <ul>
+ * Normal recipe check:
+ * <ul>
+ * {@link gregtech.api.recipe.FindRecipeQuery#find Find recipe from recipemap}: O(NCR)
+ * where N = number of machine inputs, C = average amount of recipe candidates found for specific input,
+ * R = computation time to {@link GT_Recipe#isRecipeInputEqual check if inputs match to recipe}
+ * </ul>
+ * <ul>
+ * {@link GT_Recipe#isRecipeInputEqual Check if inputs match to recipe}: O(NM)
+ * where N = number of machine inputs, M = number of recipe inputs
+ * </ul>
+ * </ul>
+ * <ul>
+ * {@link #checkRecipeInputs Single recipe check}: O(N + M)
+ * where N = number of machine inputs, M = number of recipe inputs
+ * </ul>
+ */
+public class SingleRecipeCheck {
+
+ @Nonnull
+ private final GT_Recipe recipe;
+ @Nonnull
+ private final RecipeMap<?> recipeMap;
+ @Nonnull
+ private final ImmutableMap<ItemId, Integer> itemCost;
+ @Nonnull
+ private final ImmutableMap<Fluid, Integer> fluidCost;
+
+ private final int totalItemCost;
+ private final int totalFluidCost;
+
+ private SingleRecipeCheck(@Nonnull GT_Recipe recipe, @Nonnull RecipeMap<?> recipeMap,
+ @Nonnull ImmutableMap<ItemId, Integer> itemCost, @Nonnull ImmutableMap<Fluid, Integer> fluidCost) {
+ this.recipe = recipe;
+ this.recipeMap = recipeMap;
+ this.itemCost = itemCost;
+ this.fluidCost = fluidCost;
+
+ this.totalItemCost = itemCost.values()
+ .stream()
+ .mapToInt(Integer::intValue)
+ .sum();
+ this.totalFluidCost = fluidCost.values()
+ .stream()
+ .mapToInt(Integer::intValue)
+ .sum();
+ }
+
+ @Nonnull
+ public GT_Recipe getRecipe() {
+ return recipe;
+ }
+
+ @Nonnull
+ public RecipeMap<?> getRecipeMap() {
+ return recipeMap;
+ }
+
+ /**
+ * Returns the number of parallel recipes, or 0 if recipe is not satisfied at all.
+ */
+ public int checkRecipeInputs(boolean consumeInputs, int maxParallel, ItemStack[] itemInputs,
+ FluidStack[] fluidInputs) {
+ int currentParallel = maxParallel;
+
+ if (totalItemCost > 0) {
+ // Create map for item -> stored amount
+ Map<ItemId, Integer> itemMap = new HashMap<>();
+ for (ItemStack itemStack : itemInputs) {
+ if (itemStack == null) continue;
+ itemMap.merge(ItemId.createNoCopy(itemStack), itemStack.stackSize, Integer::sum);
+ }
+
+ // Check how many parallels can it perform for each item
+ for (Map.Entry<ItemId, Integer> costEntry : itemCost.entrySet()) {
+ currentParallel = Math
+ .min(currentParallel, itemMap.getOrDefault(costEntry.getKey(), 0) / costEntry.getValue());
+ if (currentParallel <= 0) {
+ return 0;
+ }
+ }
+ }
+
+ if (totalFluidCost > 0) {
+ // Create map for fluid -> stored amount
+ Map<Fluid, Integer> fluidMap = new HashMap<>();
+ for (FluidStack fluidStack : fluidInputs) {
+ if (fluidStack == null) continue;
+ fluidMap.merge(fluidStack.getFluid(), fluidStack.amount, Integer::sum);
+ }
+
+ // Check how many parallels can it perform for each fluid
+ for (Map.Entry<Fluid, Integer> costEntry : fluidCost.entrySet()) {
+ currentParallel = Math
+ .min(currentParallel, fluidMap.getOrDefault(costEntry.getKey(), 0) / costEntry.getValue());
+ if (currentParallel <= 0) {
+ return 0;
+ }
+ }
+ }
+
+ final int finalParallel = currentParallel;
+ if (consumeInputs) {
+ if (totalItemCost > 0) {
+ int remainingItemCost = totalItemCost * finalParallel;
+ Map<ItemId, Integer> runningItemCost = itemCost.entrySet()
+ .stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue() * finalParallel));
+
+ for (ItemStack itemStack : itemInputs) {
+ if (itemStack == null) continue;
+ ItemId key = ItemId.createNoCopy(itemStack);
+ int runningCost = runningItemCost.getOrDefault(key, 0);
+ int paid = Math.min(itemStack.stackSize, runningCost);
+ itemStack.stackSize -= paid;
+ runningItemCost.put(key, runningCost - paid);
+
+ remainingItemCost -= paid;
+ // If all item costs are paid, we don't need to iterate inputs furthermore
+ if (remainingItemCost <= 0) {
+ break;
+ }
+ }
+ }
+
+ if (totalFluidCost > 0) {
+ int remainingFluidCost = totalFluidCost * finalParallel;
+ Map<Fluid, Integer> runningFluidCost = fluidCost.entrySet()
+ .stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue() * finalParallel));
+
+ for (FluidStack fluidStack : fluidInputs) {
+ if (fluidStack == null) continue;
+ Fluid key = fluidStack.getFluid();
+ int runningCost = runningFluidCost.getOrDefault(key, 0);
+ int paid = Math.min(fluidStack.amount, runningCost);
+ fluidStack.amount -= paid;
+ runningFluidCost.put(key, runningCost - paid);
+
+ remainingFluidCost -= paid;
+ // If all fluid costs are paid, we don't need to iterate inputs furthermore
+ if (remainingFluidCost <= 0) {
+ break;
+ }
+ }
+ }
+ }
+
+ return finalParallel;
+ }
+
+ public NBTTagCompound writeToNBT() {
+ // Here we encode recipe input, output and all other important values.
+ // At load time we do a recipe check again, so in case the recipe is gone, we can stop tracking.
+ // Of course the next step would be auto migrating to new recipe (if any), but given
+ // we don't yet have a mean to uniquely name a recipe, this will have to make do.
+ // Consider move serialization code to GT_Recipe once this has been proven to work
+ NBTTagCompound tag = new NBTTagCompound();
+ tag.setString("recipemap", recipeMap.unlocalizedName);
+ if (recipe.mInputs != null) {
+ tag.setTag("inputs", writeList(recipe.mInputs, GT_Utility::saveItem));
+ }
+ if (recipe.mOutputs != null) {
+ tag.setTag("outputs", writeList(recipe.mOutputs, GT_Utility::saveItem));
+ }
+ if (recipe.mChances != null) {
+ tag.setIntArray("chances", recipe.mChances);
+ }
+ if (recipe.mFluidInputs != null) {
+ tag.setTag(
+ "fInputs",
+ writeList(
+ recipe.mFluidInputs,
+ s -> s == null ? new NBTTagCompound() : s.writeToNBT(new NBTTagCompound())));
+ }
+ if (recipe.mFluidOutputs != null) {
+ tag.setTag(
+ "fOutputs",
+ writeList(
+ recipe.mFluidOutputs,
+ s -> s == null ? new NBTTagCompound() : s.writeToNBT(new NBTTagCompound())));
+ }
+ tag.setInteger("eut", recipe.mEUt);
+ tag.setInteger("duration", recipe.mDuration);
+ tag.setInteger("specialValue", recipe.mSpecialValue);
+ tag.setTag("itemCost", writeList(itemCost.entrySet(), e -> {
+ NBTTagCompound ret = new NBTTagCompound();
+ ret.setTag(
+ "id",
+ e.getKey()
+ .writeToNBT());
+ ret.setInteger("count", e.getValue());
+ return ret;
+ }));
+ tag.setTag("fluidCost", writeList(fluidCost.entrySet(), e -> {
+ NBTTagCompound ret = new NBTTagCompound();
+ ret.setString(
+ "id",
+ e.getKey()
+ .getName());
+ ret.setInteger("count", e.getValue());
+ return ret;
+ }));
+ return tag;
+ }
+
+ private static <T, NBT extends NBTBase> NBTTagList writeList(T[] arr, Function<T, NBT> ser) {
+ return writeList(Arrays.asList(arr), ser);
+ }
+
+ private static <T, NBT extends NBTBase> NBTTagList writeList(Collection<T> arr, Function<T, NBT> ser) {
+ NBTTagList l = new NBTTagList();
+ for (T t : arr) {
+ l.appendTag(ser.apply(t));
+ }
+ return l;
+ }
+
+ @Nullable
+ public static SingleRecipeCheck tryLoad(RecipeMap<?> recipeMap, NBTTagCompound tag) {
+ if (tag == null || tag.hasNoTags()) return null;
+
+ RecipeMap<?> mapToUse;
+ if (tag.hasKey("recipemap")) {
+ String mapName = tag.getString("recipemap");
+ RecipeMap<?> foundMap = RecipeMap.ALL_RECIPE_MAPS.get(mapName);
+ if (foundMap != null) {
+ mapToUse = foundMap;
+ } else {
+ mapToUse = recipeMap;
+ }
+ } else {
+ mapToUse = recipeMap;
+ }
+ if (mapToUse == null) {
+ return null;
+ }
+
+ GT_Recipe foundRecipe = tryFindRecipe(mapToUse, tag);
+ if (foundRecipe == null) return null;
+ return new SingleRecipeCheck(foundRecipe, mapToUse, loadItemCost(tag), loadFluidCost(tag));
+ }
+
+ private static ImmutableMap<Fluid, Integer> loadFluidCost(NBTTagCompound tag) {
+ return GT_Utility.streamCompounds(tag.getTagList("fluidCost", Constants.NBT.TAG_COMPOUND))
+ .collect(
+ GT_Utility
+ .toImmutableMapSerial(t -> FluidRegistry.getFluid(t.getString("id")), t -> t.getInteger("count")));
+ }
+
+ private static ImmutableMap<ItemId, Integer> loadItemCost(NBTTagCompound tag) {
+ return GT_Utility.streamCompounds(tag.getTagList("itemCost", Constants.NBT.TAG_COMPOUND))
+ .collect(
+ GT_Utility
+ .toImmutableMapSerial(t -> ItemId.create(t.getCompoundTag("id")), t -> t.getInteger("count")));
+ }
+
+ private static GT_Recipe tryFindRecipe(@Nonnull RecipeMap<?> recipeMap, NBTTagCompound tag) {
+ ItemStack[] inputs = GT_Utility.streamCompounds(tag.getTagList("inputs", Constants.NBT.TAG_COMPOUND))
+ .map(GT_Utility::loadItem)
+ .toArray(ItemStack[]::new);
+ ItemStack[] outputs = GT_Utility.streamCompounds(tag.getTagList("outputs", Constants.NBT.TAG_COMPOUND))
+ .map(GT_Utility::loadItem)
+ .toArray(ItemStack[]::new);
+ FluidStack[] fInputs = GT_Utility.streamCompounds(tag.getTagList("fInputs", Constants.NBT.TAG_COMPOUND))
+ .map(FluidStack::loadFluidStackFromNBT)
+ .toArray(FluidStack[]::new);
+ FluidStack[] fOutputs = GT_Utility.streamCompounds(tag.getTagList("fOutputs", Constants.NBT.TAG_COMPOUND))
+ .map(FluidStack::loadFluidStackFromNBT)
+ .toArray(FluidStack[]::new);
+ int eut = tag.getInteger("eut");
+ GT_Recipe found = recipeMap.findRecipe(null, false, GT_Values.V[GT_Utility.getTier(eut)], fInputs, inputs);
+ int[] chances = tag.getIntArray("chances");
+ if (chances.length == 0) chances = null;
+ if (found == null || !GT_Utility.equals(inputs, found.mInputs)
+ || !Arrays.equals(fInputs, found.mFluidInputs)
+ || !GT_Utility.equals(outputs, found.mOutputs)
+ || !Arrays.equals(fOutputs, found.mFluidOutputs)
+ || !Arrays.equals(chances, found.mChances)
+ || found.mDuration != tag.getInteger("duration")
+ || found.mEUt != eut
+ || found.mSpecialValue != tag.getInteger("specialValue")) return null;
+ return found;
+ }
+
+ private static ImmutableMap<ItemId, Integer> buildItemMap(ItemStack[] inputs) {
+ Map<ItemId, Integer> itemMap = new HashMap<>();
+ for (ItemStack itemStack : inputs) {
+ if (itemStack == null) continue;
+ itemMap.merge(ItemId.create(itemStack), itemStack.stackSize, Integer::sum);
+ }
+ return ImmutableMap.copyOf(itemMap);
+ }
+
+ private static ImmutableMap<Fluid, Integer> buildFluidMap(FluidStack[] fluids) {
+ Map<Fluid, Integer> fluidMap = new HashMap<>();
+ for (FluidStack fluidStack : fluids) {
+ if (fluidStack == null) continue;
+ fluidMap.merge(fluidStack.getFluid(), fluidStack.amount, Integer::sum);
+ }
+ return ImmutableMap.copyOf(fluidMap);
+ }
+
+ public static Builder builder(@Nonnull RecipeMap<?> recipeMap) {
+ return new Builder(Objects.requireNonNull(recipeMap));
+ }
+
+ public static class Builder {
+
+ private final RecipeMap<?> recipeMap;
+
+ // In order to compute which items and fluids are consumed by the recipe, we compare the
+ // multi-block's items and fluids before and after inputs are consumed by the recipe.
+ private Map<ItemId, Integer> beforeItems;
+ private Map<Fluid, Integer> beforeFluids;
+ private Map<ItemId, Integer> afterItems;
+ private Map<Fluid, Integer> afterFluids;
+
+ private GT_Recipe recipe;
+
+ private Builder(@Nonnull RecipeMap<?> recipeMap) {
+ this.recipeMap = recipeMap;
+ }
+
+ public Builder setBefore(ItemStack[] inputs, FluidStack[] fluids) {
+ beforeItems = buildItemMap(inputs);
+ beforeFluids = buildFluidMap(fluids);
+ return this;
+ }
+
+ public Builder setAfter(ItemStack[] inputs, FluidStack[] fluids) {
+ afterItems = buildItemMap(inputs);
+ afterFluids = buildFluidMap(fluids);
+ return this;
+ }
+
+ public Builder setRecipe(@Nonnull GT_Recipe recipe) {
+ this.recipe = recipe;
+ return this;
+ }
+
+ private ImmutableMap<ItemId, Integer> buildItemCost() {
+ ImmutableMap.Builder<ItemId, Integer> itemCostBuilder = ImmutableMap.builder();
+ for (Map.Entry<ItemId, Integer> entry : beforeItems.entrySet()) {
+ int cost = entry.getValue() - afterItems.getOrDefault(entry.getKey(), 0);
+ if (cost > 0) {
+ itemCostBuilder.put(entry.getKey(), cost);
+ }
+ }
+ return itemCostBuilder.build();
+ }
+
+ private ImmutableMap<Fluid, Integer> buildFluidCost() {
+ ImmutableMap.Builder<Fluid, Integer> fluidCostBuilder = ImmutableMap.builder();
+ for (Map.Entry<Fluid, Integer> entry : beforeFluids.entrySet()) {
+ int cost = entry.getValue() - afterFluids.getOrDefault(entry.getKey(), 0);
+ if (cost > 0) {
+ fluidCostBuilder.put(entry.getKey(), cost);
+ }
+ }
+ return fluidCostBuilder.build();
+ }
+
+ public SingleRecipeCheck build() {
+ if (recipe == null) {
+ throw new IllegalStateException("recipe is not set");
+ }
+ return new SingleRecipeCheck(recipe, recipeMap, buildItemCost(), buildFluidCost());
+ }
+ }
+}