diff options
Diffstat (limited to 'src/main/java/gregtech/api/recipe/check')
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()); + } + } +} |