aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/gregtech/api/recipe
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/gregtech/api/recipe')
-rw-r--r--src/main/java/gregtech/api/recipe/BasicUIProperties.java251
-rw-r--r--src/main/java/gregtech/api/recipe/BasicUIPropertiesBuilder.java264
-rw-r--r--src/main/java/gregtech/api/recipe/FindRecipeQuery.java178
-rw-r--r--src/main/java/gregtech/api/recipe/NEIRecipeProperties.java89
-rw-r--r--src/main/java/gregtech/api/recipe/NEIRecipePropertiesBuilder.java100
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeCategories.java71
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeCategory.java81
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeCategoryHolder.java13
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeCategorySetting.java52
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeMap.java395
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeMapBackend.java469
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeMapBackendProperties.java77
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeMapBackendPropertiesBuilder.java119
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeMapBuilder.java522
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeMapFrontend.java395
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeMaps.java1194
-rw-r--r--src/main/java/gregtech/api/recipe/RecipeMetadataKey.java84
-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
-rw-r--r--src/main/java/gregtech/api/recipe/maps/AssemblerBackend.java35
-rw-r--r--src/main/java/gregtech/api/recipe/maps/AssemblyLineFrontend.java76
-rw-r--r--src/main/java/gregtech/api/recipe/maps/DistillationTowerFrontend.java38
-rw-r--r--src/main/java/gregtech/api/recipe/maps/FluidCannerBackend.java73
-rw-r--r--src/main/java/gregtech/api/recipe/maps/FluidOnlyFrontend.java33
-rw-r--r--src/main/java/gregtech/api/recipe/maps/FormingPressBackend.java92
-rw-r--r--src/main/java/gregtech/api/recipe/maps/FuelBackend.java75
-rw-r--r--src/main/java/gregtech/api/recipe/maps/FurnaceBackend.java52
-rw-r--r--src/main/java/gregtech/api/recipe/maps/LargeBoilerFuelBackend.java132
-rw-r--r--src/main/java/gregtech/api/recipe/maps/LargeBoilerFuelFrontend.java25
-rw-r--r--src/main/java/gregtech/api/recipe/maps/LargeNEIFrontend.java65
-rw-r--r--src/main/java/gregtech/api/recipe/maps/MicrowaveBackend.java145
-rw-r--r--src/main/java/gregtech/api/recipe/maps/NonGTBackend.java52
-rw-r--r--src/main/java/gregtech/api/recipe/maps/OilCrackerBackend.java41
-rw-r--r--src/main/java/gregtech/api/recipe/maps/PrinterBackend.java142
-rw-r--r--src/main/java/gregtech/api/recipe/maps/RecyclerBackend.java55
-rw-r--r--src/main/java/gregtech/api/recipe/maps/ReplicatorBackend.java100
-rw-r--r--src/main/java/gregtech/api/recipe/maps/SpaceProjectFrontend.java131
-rw-r--r--src/main/java/gregtech/api/recipe/maps/TranscendentPlasmaMixerFrontend.java56
-rw-r--r--src/main/java/gregtech/api/recipe/maps/UnpackagerBackend.java53
-rw-r--r--src/main/java/gregtech/api/recipe/metadata/EmptyRecipeMetadataStorage.java50
-rw-r--r--src/main/java/gregtech/api/recipe/metadata/IRecipeMetadataStorage.java34
-rw-r--r--src/main/java/gregtech/api/recipe/metadata/PCBFactoryTierKey.java30
-rw-r--r--src/main/java/gregtech/api/recipe/metadata/PCBFactoryUpgrade.java7
-rw-r--r--src/main/java/gregtech/api/recipe/metadata/PCBFactoryUpgradeKey.java32
-rw-r--r--src/main/java/gregtech/api/recipe/metadata/RecipeMetadataStorage.java56
-rw-r--r--src/main/java/gregtech/api/recipe/metadata/SimpleRecipeMetadataKey.java27
52 files changed, 7027 insertions, 0 deletions
diff --git a/src/main/java/gregtech/api/recipe/BasicUIProperties.java b/src/main/java/gregtech/api/recipe/BasicUIProperties.java
new file mode 100644
index 0000000000..fde86785b2
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/BasicUIProperties.java
@@ -0,0 +1,251 @@
+package gregtech.api.recipe;
+
+import java.awt.Rectangle;
+import java.util.List;
+import java.util.function.IntFunction;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import com.gtnewhorizons.modularui.api.drawable.FallbackableUITexture;
+import com.gtnewhorizons.modularui.api.drawable.IDrawable;
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.math.Size;
+import com.gtnewhorizons.modularui.common.widget.ProgressBar;
+
+import gregtech.api.gui.modularui.FallbackableSteamTexture;
+import gregtech.api.gui.modularui.SteamTexture;
+import gregtech.api.util.FieldsAreNonnullByDefault;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Data object to store properties, used to draw both basic machine GUI and NEI recipe GUI, mainly GUI widgets.
+ * Not all the info used to draw NEI are listed here, see also {@link NEIRecipeProperties}.
+ * <p>
+ * Use {@link #builder()} for creation.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@FieldsAreNonnullByDefault
+public final class BasicUIProperties {
+
+ /**
+ * Starts constructing BasicUIProperties.
+ */
+ public static BasicUIPropertiesBuilder builder() {
+ return new BasicUIPropertiesBuilder();
+ }
+
+ /**
+ * Creates new builder from this instance.
+ */
+ public BasicUIPropertiesBuilder toBuilder() {
+ return new BasicUIPropertiesBuilder().maxItemInputs(maxItemInputs)
+ .maxItemOutputs(maxItemOutputs)
+ .maxFluidInputs(maxFluidInputs)
+ .maxFluidOutputs(maxFluidOutputs)
+ .slotOverlays(slotOverlays)
+ .slotOverlaysSteam(slotOverlaysSteam)
+ .progressBarTexture(progressBarTexture)
+ .progressBarTextureSteam(progressBarTextureSteam)
+ .progressBarDirection(progressBarDirection)
+ .progressBarSize(progressBarSize)
+ .progressBarPos(progressBarPos)
+ .useProgressBar(useProgressBar)
+ .useSpecialSlot(useSpecialSlot)
+ .neiTransferRect(neiTransferRect)
+ .neiTransferRectId(neiTransferRectId)
+ .specialTextures(specialTextures)
+ .specialTexturesSteam(specialTexturesSteam)
+ .logo(logo)
+ .logoSize(logoSize)
+ .logoPos(logoPos)
+ .itemInputPositionsGetter(itemInputPositionsGetter)
+ .itemOutputPositionsGetter(itemOutputPositionsGetter)
+ .specialItemPositionGetter(specialItemPositionGetter)
+ .fluidInputPositionsGetter(fluidInputPositionsGetter)
+ .fluidOutputPositionsGetter(fluidOutputPositionsGetter)
+ .amperage(amperage);
+ }
+
+ /**
+ * How many item inputs does this recipemap usually has at most.
+ * It does not actually restrict number of items used in the recipe.
+ */
+ public final int maxItemInputs;
+ /**
+ * How many item outputs does this recipemap usually has at most.
+ * It does not actually restrict number of items used in the recipe.
+ */
+ public final int maxItemOutputs;
+ /**
+ * How many fluid inputs does this recipemap usually has at most.
+ * It does not actually restrict number of items used in the recipe.
+ */
+ public final int maxFluidInputs;
+ /**
+ * How many fluid outputs does this recipemap usually has at most.
+ * It does not actually restrict number of items used in the recipe.
+ */
+ public final int maxFluidOutputs;
+
+ private final SlotOverlayGetter<IDrawable> slotOverlays;
+ private final SlotOverlayGetter<SteamTexture> slotOverlaysSteam;
+
+ /**
+ * Progressbar used for BasicMachine GUI and NEI.
+ */
+ @Nullable
+ public final FallbackableUITexture progressBarTexture;
+ /**
+ * Progressbar used for steam machine GUI.
+ */
+ @Nullable
+ public final FallbackableSteamTexture progressBarTextureSteam;
+ /**
+ * Direction of progressbar animation.
+ */
+ public final ProgressBar.Direction progressBarDirection;
+ /**
+ * Size of the progressbar. (20, 36) by default.
+ */
+ public final Size progressBarSize;
+ /**
+ * Position of the progressbar. (78, 24) by default.
+ */
+ public final Pos2d progressBarPos;
+ /**
+ * Image size in the direction of progressbar. Used for non-smooth rendering.
+ */
+ public final int progressBarImageSize;
+
+ /**
+ * If progressbar should be added.
+ */
+ public final boolean useProgressBar;
+
+ /**
+ * If special slot has its usage for this GUI.
+ */
+ public final boolean useSpecialSlot;
+
+ /**
+ * GUI area where clicking shows up all the recipes available.
+ */
+ public final List<Rectangle> neiTransferRect;
+ /**
+ * ID used to open NEI recipe GUI when progressbar is clicked.
+ */
+ @Nullable
+ public final String neiTransferRectId;
+
+ /**
+ * Additional textures shown on GUI.
+ */
+ public final List<Pair<IDrawable, Pair<Size, Pos2d>>> specialTextures;
+ /**
+ * Additional textures shown on steam machine GUI.
+ */
+ public final List<Pair<SteamTexture, Pair<Size, Pos2d>>> specialTexturesSteam;
+
+ /**
+ * Logo shown on GUI. GregTech logo by default.
+ */
+ public final IDrawable logo;
+ /**
+ * Size of logo. (17, 17) by default.
+ */
+ public final Size logoSize;
+ /**
+ * Position of logo. (152, 63) by default.
+ */
+ public final Pos2d logoPos;
+
+ public final IntFunction<List<Pos2d>> itemInputPositionsGetter;
+ public final IntFunction<List<Pos2d>> itemOutputPositionsGetter;
+ public final Supplier<Pos2d> specialItemPositionGetter;
+ public final IntFunction<List<Pos2d>> fluidInputPositionsGetter;
+ public final IntFunction<List<Pos2d>> fluidOutputPositionsGetter;
+
+ /**
+ * Amperage for the recipemap. Even though this is placed at frontend because backend logic doesn't need it,
+ * some machine logic also use this variable.
+ */
+ public final int amperage;
+
+ BasicUIProperties(int maxItemInputs, int maxItemOutputs, int maxFluidInputs, int maxFluidOutputs,
+ SlotOverlayGetter<IDrawable> slotOverlays, SlotOverlayGetter<SteamTexture> slotOverlaysSteam,
+ @Nullable FallbackableUITexture progressBarTexture, @Nullable FallbackableSteamTexture progressBarTextureSteam,
+ ProgressBar.Direction progressBarDirection, Size progressBarSize, Pos2d progressBarPos, boolean useProgressBar,
+ boolean useSpecialSlot, List<Rectangle> neiTransferRect, @Nullable String neiTransferRectId,
+ List<Pair<IDrawable, Pair<Size, Pos2d>>> specialTextures,
+ List<Pair<SteamTexture, Pair<Size, Pos2d>>> specialTexturesSteam, IDrawable logo, Size logoSize, Pos2d logoPos,
+ IntFunction<List<Pos2d>> itemInputPositionsGetter, IntFunction<List<Pos2d>> itemOutputPositionsGetter,
+ Supplier<Pos2d> specialItemPositionGetter, IntFunction<List<Pos2d>> fluidInputPositionsGetter,
+ IntFunction<List<Pos2d>> fluidOutputPositionsGetter, int amperage) {
+ if (maxItemInputs < 0 || maxItemOutputs < 0 || maxFluidInputs < 0 || maxFluidOutputs < 0) {
+ throw new IllegalArgumentException(
+ "maxItemInputs, maxItemOutputs, maxFluidInputs and maxFluidOutputs cannot be negative");
+ }
+ if (amperage < 1) {
+ throw new IllegalArgumentException("Amperage cannot be lower than 1");
+ }
+ this.maxItemInputs = maxItemInputs;
+ this.maxItemOutputs = maxItemOutputs;
+ this.maxFluidInputs = maxFluidInputs;
+ this.maxFluidOutputs = maxFluidOutputs;
+ this.slotOverlays = slotOverlays;
+ this.slotOverlaysSteam = slotOverlaysSteam;
+ this.progressBarTexture = progressBarTexture;
+ this.progressBarTextureSteam = progressBarTextureSteam;
+ this.progressBarDirection = progressBarDirection;
+ this.progressBarSize = progressBarSize;
+ this.progressBarPos = progressBarPos;
+ this.useProgressBar = useProgressBar;
+ this.useSpecialSlot = useSpecialSlot;
+ this.neiTransferRect = neiTransferRect;
+ this.neiTransferRectId = neiTransferRectId;
+ this.specialTextures = specialTextures;
+ this.specialTexturesSteam = specialTexturesSteam;
+ this.logo = logo;
+ this.logoSize = logoSize;
+ this.logoPos = logoPos;
+ this.itemInputPositionsGetter = itemInputPositionsGetter;
+ this.itemOutputPositionsGetter = itemOutputPositionsGetter;
+ this.specialItemPositionGetter = specialItemPositionGetter;
+ this.fluidInputPositionsGetter = fluidInputPositionsGetter;
+ this.fluidOutputPositionsGetter = fluidOutputPositionsGetter;
+ this.amperage = amperage;
+
+ this.progressBarImageSize = switch (progressBarDirection) {
+ case UP, DOWN -> progressBarSize.height;
+ case CIRCULAR_CW -> Math.max(progressBarSize.width, progressBarSize.height);
+ default -> progressBarSize.width;
+ };
+ }
+
+ /**
+ * Retrieves overlay for slot, with given matching conditions.
+ */
+ @Nullable
+ public IDrawable getOverlayForSlot(int index, boolean isFluid, boolean isOutput, boolean isSpecial) {
+ return slotOverlays.apply(index, isFluid, isOutput, isSpecial);
+ }
+
+ /**
+ * Retrieves overlay for slot of steam machines, with given matching conditions.
+ */
+ @Nullable
+ public SteamTexture getOverlayForSlotSteam(int index, boolean isFluid, boolean isOutput, boolean isSpecial) {
+ return slotOverlaysSteam.apply(index, isFluid, isOutput, isSpecial);
+ }
+
+ public interface SlotOverlayGetter<T> {
+
+ @Nullable
+ T apply(int index, boolean isFluid, boolean isOutput, boolean isSpecial);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/BasicUIPropertiesBuilder.java b/src/main/java/gregtech/api/recipe/BasicUIPropertiesBuilder.java
new file mode 100644
index 0000000000..7be2c94b23
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/BasicUIPropertiesBuilder.java
@@ -0,0 +1,264 @@
+package gregtech.api.recipe;
+
+import java.awt.Rectangle;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.IntFunction;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.commons.lang3.tuple.Pair;
+
+import com.google.common.collect.ImmutableList;
+import com.gtnewhorizons.modularui.api.drawable.FallbackableUITexture;
+import com.gtnewhorizons.modularui.api.drawable.IDrawable;
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.math.Size;
+import com.gtnewhorizons.modularui.common.widget.ProgressBar;
+
+import gregtech.api.gui.modularui.FallbackableSteamTexture;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.gui.modularui.SteamTexture;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.common.gui.modularui.UIHelper;
+
+/**
+ * Builder class for {@link BasicUIProperties}.
+ */
+@SuppressWarnings("UnusedReturnValue")
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public final class BasicUIPropertiesBuilder {
+
+ private int maxItemInputs, maxItemOutputs, maxFluidInputs, maxFluidOutputs;
+
+ private BasicUIProperties.SlotOverlayGetter<IDrawable> slotOverlays = (index, isFluid, isOutput, isSpecial) -> null;
+ private BasicUIProperties.SlotOverlayGetter<SteamTexture> slotOverlaysSteam = (index, isFluid, isOutput,
+ isSpecial) -> null;
+
+ @Nullable
+ private FallbackableUITexture progressBarTexture;
+ @Nullable
+ private FallbackableSteamTexture progressBarTextureSteam;
+ private ProgressBar.Direction progressBarDirection = ProgressBar.Direction.RIGHT;
+ private Size progressBarSize = new Size(20, 18);
+ private Pos2d progressBarPos = new Pos2d(78, 24);
+
+ private boolean useProgressBar = true;
+
+ private boolean useSpecialSlot;
+
+ private final ImmutableList.Builder<Rectangle> neiTransferRect = ImmutableList.builder();
+ @Nullable
+ private String neiTransferRectId;
+
+ private final ImmutableList.Builder<Pair<IDrawable, Pair<Size, Pos2d>>> specialTextures = ImmutableList.builder();
+ private final ImmutableList.Builder<Pair<SteamTexture, Pair<Size, Pos2d>>> specialTexturesSteam = ImmutableList
+ .builder();
+
+ private IDrawable logo = GT_UITextures.PICTURE_GT_LOGO_17x17_TRANSPARENT;
+ private Size logoSize = new Size(17, 17);
+ private Pos2d logoPos = new Pos2d(152, 63);
+
+ private IntFunction<List<Pos2d>> itemInputPositionsGetter = UIHelper::getItemInputPositions;
+ private IntFunction<List<Pos2d>> itemOutputPositionsGetter = UIHelper::getItemOutputPositions;
+ private Supplier<Pos2d> specialItemPositionGetter = UIHelper::getSpecialItemPosition;
+ private IntFunction<List<Pos2d>> fluidInputPositionsGetter = UIHelper::getFluidInputPositions;
+ private IntFunction<List<Pos2d>> fluidOutputPositionsGetter = UIHelper::getFluidOutputPositions;
+
+ private int amperage = 1;
+
+ BasicUIPropertiesBuilder() {}
+
+ public BasicUIProperties build() {
+ if (maxItemInputs == 0 && maxItemOutputs == 0 && maxFluidInputs == 0 && maxFluidOutputs == 0) {
+ throw new IllegalArgumentException("Set either of max I/O count");
+ }
+ List<Rectangle> builtNEITransferRect = neiTransferRect.build();
+ if (builtNEITransferRect.isEmpty()) {
+ builtNEITransferRect = Collections.singletonList(
+ new Rectangle(
+ progressBarPos.x - (16 / 2),
+ progressBarPos.y,
+ progressBarSize.width + 16,
+ progressBarSize.height));
+ }
+ return new BasicUIProperties(
+ maxItemInputs,
+ maxItemOutputs,
+ maxFluidInputs,
+ maxFluidOutputs,
+ slotOverlays,
+ slotOverlaysSteam,
+ progressBarTexture,
+ progressBarTextureSteam,
+ progressBarDirection,
+ progressBarSize,
+ progressBarPos,
+ useProgressBar,
+ useSpecialSlot,
+ builtNEITransferRect,
+ neiTransferRectId,
+ specialTextures.build(),
+ specialTexturesSteam.build(),
+ logo,
+ logoSize,
+ logoPos,
+ itemInputPositionsGetter,
+ itemOutputPositionsGetter,
+ specialItemPositionGetter,
+ fluidInputPositionsGetter,
+ fluidOutputPositionsGetter,
+ amperage);
+ }
+
+ public BasicUIPropertiesBuilder maxItemInputs(int maxItemInputs) {
+ this.maxItemInputs = maxItemInputs;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder maxItemOutputs(int maxItemOutputs) {
+ this.maxItemOutputs = maxItemOutputs;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder maxFluidInputs(int maxFluidInputs) {
+ this.maxFluidInputs = maxFluidInputs;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder maxFluidOutputs(int maxFluidOutputs) {
+ this.maxFluidOutputs = maxFluidOutputs;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder slotOverlays(BasicUIProperties.SlotOverlayGetter<IDrawable> slotOverlays) {
+ this.slotOverlays = slotOverlays;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder slotOverlaysSteam(
+ BasicUIProperties.SlotOverlayGetter<SteamTexture> slotOverlaysSteam) {
+ this.slotOverlaysSteam = slotOverlaysSteam;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder progressBarTexture(@Nullable FallbackableUITexture progressBarTexture) {
+ this.progressBarTexture = progressBarTexture;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder progressBarTextureSteam(
+ @Nullable FallbackableSteamTexture progressBarTextureSteam) {
+ this.progressBarTextureSteam = progressBarTextureSteam;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder progressBarDirection(ProgressBar.Direction progressBarDirection) {
+ this.progressBarDirection = progressBarDirection;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder progressBarSize(Size progressBarSize) {
+ this.progressBarSize = progressBarSize;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder progressBarPos(Pos2d progressBarPos) {
+ this.progressBarPos = progressBarPos;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder useProgressBar(boolean useProgressBar) {
+ this.useProgressBar = useProgressBar;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder useSpecialSlot(boolean useSpecialSlot) {
+ this.useSpecialSlot = useSpecialSlot;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder addNEITransferRect(Rectangle neiTransferRect) {
+ this.neiTransferRect.add(neiTransferRect);
+ return this;
+ }
+
+ BasicUIPropertiesBuilder neiTransferRect(List<Rectangle> neiTransferRect) {
+ this.neiTransferRect.addAll(neiTransferRect);
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder neiTransferRectId(@Nullable String neiTransferRectId) {
+ this.neiTransferRectId = neiTransferRectId;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder addSpecialTexture(Size size, Pos2d pos, IDrawable texture) {
+ this.specialTextures.add(new ImmutablePair<>(texture, new ImmutablePair<>(size, pos)));
+ return this;
+ }
+
+ BasicUIPropertiesBuilder specialTextures(List<Pair<IDrawable, Pair<Size, Pos2d>>> specialTextures) {
+ this.specialTextures.addAll(specialTextures);
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder addSpecialTextureSteam(Size size, Pos2d pos, SteamTexture texture) {
+ this.specialTexturesSteam.add(new ImmutablePair<>(texture, new ImmutablePair<>(size, pos)));
+ return this;
+ }
+
+ BasicUIPropertiesBuilder specialTexturesSteam(List<Pair<SteamTexture, Pair<Size, Pos2d>>> specialTextures) {
+ this.specialTexturesSteam.addAll(specialTextures);
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder logo(IDrawable logo) {
+ this.logo = logo;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder logoSize(Size logoSize) {
+ this.logoSize = logoSize;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder logoPos(Pos2d logoPos) {
+ this.logoPos = logoPos;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder itemInputPositionsGetter(IntFunction<List<Pos2d>> itemInputPositionsGetter) {
+ this.itemInputPositionsGetter = itemInputPositionsGetter;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder itemOutputPositionsGetter(IntFunction<List<Pos2d>> itemOutputPositionsGetter) {
+ this.itemOutputPositionsGetter = itemOutputPositionsGetter;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder specialItemPositionGetter(Supplier<Pos2d> specialItemPositionGetter) {
+ this.specialItemPositionGetter = specialItemPositionGetter;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder fluidInputPositionsGetter(IntFunction<List<Pos2d>> fluidInputPositionsGetter) {
+ this.fluidInputPositionsGetter = fluidInputPositionsGetter;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder fluidOutputPositionsGetter(IntFunction<List<Pos2d>> fluidOutputPositionsGetter) {
+ this.fluidOutputPositionsGetter = fluidOutputPositionsGetter;
+ return this;
+ }
+
+ public BasicUIPropertiesBuilder amperage(int amperage) {
+ this.amperage = amperage;
+ return this;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/FindRecipeQuery.java b/src/main/java/gregtech/api/recipe/FindRecipeQuery.java
new file mode 100644
index 0000000000..77c0648688
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/FindRecipeQuery.java
@@ -0,0 +1,178 @@
+package gregtech.api.recipe;
+
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+// spotless:off spotless likes formatting @code to &#64;code
+/**
+ * Helper class for searching recipe. Retrieve instance with {@link RecipeMap#findRecipeQuery}.
+ * <p>
+ * It features fluent API, so you can find recipes like this:
+ *
+ * <pre>
+ * {@code
+ * GT_Recipe recipe = recipeMap.findRecipeQuery()
+ * .items(inputItems)
+ * .fluids(inputFluids)
+ * .find();
+ * }
+ * </pre>
+ */
+// spotless:on
+@SuppressWarnings("unused")
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public final class FindRecipeQuery {
+
+ private static final Predicate<GT_Recipe> ALWAYS = r -> true;
+
+ private final RecipeMap<?> recipeMap;
+
+ @Nullable
+ private ItemStack[] items;
+ @Nullable
+ private FluidStack[] fluids;
+ @Nullable
+ private ItemStack specialSlot;
+ private Predicate<GT_Recipe> filter = ALWAYS;
+ private long voltage = Integer.MAX_VALUE;
+ @Nullable
+ private GT_Recipe cachedRecipe;
+ private boolean notUnificated;
+ private boolean dontCheckStackSizes;
+ private boolean forCollisionCheck;
+
+ FindRecipeQuery(RecipeMap<?> recipeMap) {
+ this.recipeMap = recipeMap;
+ }
+
+ // region executors
+
+ /**
+ * @return The first matched recipe, or null if not found.
+ */
+ @Nullable
+ public GT_Recipe find() {
+ return findAll().findFirst()
+ .orElse(null);
+ }
+
+ /**
+ * @return All the matched recipes in the form of Stream.
+ */
+ public Stream<GT_Recipe> findAll() {
+ if (items == null) {
+ items = new ItemStack[0];
+ }
+ if (fluids == null) {
+ fluids = new FluidStack[0];
+ }
+
+ return recipeMap.getBackend()
+ .matchRecipeStream(
+ items,
+ fluids,
+ specialSlot,
+ cachedRecipe,
+ notUnificated,
+ dontCheckStackSizes,
+ forCollisionCheck)
+ .filter(recipe -> voltage * recipeMap.getAmperage() >= recipe.mEUt && filter.test(recipe));
+ }
+
+ /**
+ * Checks if given inputs conflict with already registered recipes.
+ *
+ * @return True if collision is found.
+ */
+ public boolean checkCollision() {
+ dontCheckStackSizes = true;
+ forCollisionCheck = true;
+ return findAll().findAny()
+ .isPresent();
+ }
+
+ // endregion
+
+ // region setters
+
+ /**
+ * @param items Item inputs.
+ */
+ public FindRecipeQuery items(@Nullable ItemStack... items) {
+ this.items = items;
+ return this;
+ }
+
+ /**
+ * @param fluids Fluid inputs.
+ */
+ public FindRecipeQuery fluids(@Nullable FluidStack... fluids) {
+ this.fluids = fluids;
+ return this;
+ }
+
+ /**
+ * @param specialSlot Content of the special slot. Normal recipemaps don't need this, but some do.
+ * Set {@link RecipeMapBuilder#specialSlotSensitive} to make it actually functional.
+ * Alternatively overriding {@link RecipeMapBackend#filterFindRecipe} will also work.
+ */
+ public FindRecipeQuery specialSlot(@Nullable ItemStack specialSlot) {
+ this.specialSlot = specialSlot;
+ return this;
+ }
+
+ /**
+ * @param filter Matched recipe will be tested by this function. If it returns false, the query will attempt to
+ * find next recipe.
+ */
+ public FindRecipeQuery filter(Predicate<GT_Recipe> filter) {
+ this.filter = filter;
+ return this;
+ }
+
+ /**
+ * @param voltage Recipes that exceed this voltage won't match. It will be automatically multiplied by amperage
+ * of the recipemap.
+ */
+ public FindRecipeQuery voltage(long voltage) {
+ this.voltage = voltage;
+ return this;
+ }
+
+ /**
+ * @param cachedRecipe If this is not null, the query tests it before all other recipes.
+ */
+ public FindRecipeQuery cachedRecipe(@Nullable GT_Recipe cachedRecipe) {
+ this.cachedRecipe = cachedRecipe;
+ return this;
+ }
+
+ /**
+ * @param notUnificated If this is set to true, item inputs will be unificated.
+ */
+ public FindRecipeQuery notUnificated(boolean notUnificated) {
+ this.notUnificated = notUnificated;
+ return this;
+ }
+
+ /**
+ * @param dontCheckStackSizes If this is set to true, the query won't check item count and fluid amount
+ * for the matched recipe.
+ */
+ public FindRecipeQuery dontCheckStackSizes(boolean dontCheckStackSizes) {
+ this.dontCheckStackSizes = dontCheckStackSizes;
+ return this;
+ }
+
+ // endregion
+}
diff --git a/src/main/java/gregtech/api/recipe/NEIRecipeProperties.java b/src/main/java/gregtech/api/recipe/NEIRecipeProperties.java
new file mode 100644
index 0000000000..2ba49f5da1
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/NEIRecipeProperties.java
@@ -0,0 +1,89 @@
+package gregtech.api.recipe;
+
+import java.util.Comparator;
+import java.util.function.UnaryOperator;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.math.Size;
+
+import codechicken.nei.recipe.HandlerInfo;
+import gregtech.api.objects.overclockdescriber.OverclockDescriber;
+import gregtech.api.util.FieldsAreNonnullByDefault;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.nei.formatter.INEISpecialInfoFormatter;
+
+/**
+ * Data object storing info exclusively used to draw NEI recipe GUI. Not all the properties used to draw NEI
+ * are present here. See {@link BasicUIProperties} for the rest.
+ * <p>
+ * Use {@link #builder} for creation.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@FieldsAreNonnullByDefault
+public final class NEIRecipeProperties {
+
+ static NEIRecipePropertiesBuilder builder() {
+ return new NEIRecipePropertiesBuilder();
+ }
+
+ /**
+ * Whether to register dedicated NEI recipe page for the recipemap.
+ */
+ public final boolean registerNEI;
+ @Nullable
+ public final UnaryOperator<HandlerInfo.Builder> handlerInfoCreator;
+
+ /**
+ * Size of background shown.
+ */
+ // todo make it final
+ public Size recipeBackgroundSize;
+ /**
+ * Offset of background shown.
+ */
+ public final Pos2d recipeBackgroundOffset;
+
+ /**
+ * Formats special description for the recipe, mainly {@link gregtech.api.util.GT_Recipe#mSpecialValue}.
+ */
+ public final INEISpecialInfoFormatter neiSpecialInfoFormatter;
+
+ /**
+ * Whether to show oredict equivalent item outputs.
+ */
+ public final boolean unificateOutput;
+ /**
+ * If a custom filter method {@link OverclockDescriber#canHandle} should be used to limit the shown recipes when
+ * searching recipes with recipe catalyst. Else, the voltage of the recipe is the only factor to filter recipes.
+ */
+ public final boolean useCustomFilter;
+ /**
+ * Whether to render the actual stack size of items or not.
+ */
+ public final boolean renderRealStackSizes;
+
+ /**
+ * Comparator for NEI recipe sort. {@link GT_Recipe#compareTo(GT_Recipe)} by default.
+ */
+ public final Comparator<GT_Recipe> comparator;
+
+ NEIRecipeProperties(boolean registerNEI, @Nullable UnaryOperator<HandlerInfo.Builder> handlerInfoCreator,
+ Size recipeBackgroundSize, Pos2d recipeBackgroundOffset, INEISpecialInfoFormatter neiSpecialInfoFormatter,
+ boolean unificateOutput, boolean useCustomFilter, boolean renderRealStackSizes,
+ Comparator<GT_Recipe> comparator) {
+ this.registerNEI = registerNEI;
+ this.handlerInfoCreator = handlerInfoCreator;
+ this.recipeBackgroundOffset = recipeBackgroundOffset;
+ this.recipeBackgroundSize = recipeBackgroundSize;
+ this.neiSpecialInfoFormatter = neiSpecialInfoFormatter;
+ this.unificateOutput = unificateOutput;
+ this.useCustomFilter = useCustomFilter;
+ this.renderRealStackSizes = renderRealStackSizes;
+ this.comparator = comparator;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/NEIRecipePropertiesBuilder.java b/src/main/java/gregtech/api/recipe/NEIRecipePropertiesBuilder.java
new file mode 100644
index 0000000000..050a1c4920
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/NEIRecipePropertiesBuilder.java
@@ -0,0 +1,100 @@
+package gregtech.api.recipe;
+
+import java.util.Comparator;
+import java.util.function.UnaryOperator;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.math.Size;
+
+import codechicken.nei.recipe.HandlerInfo;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.nei.formatter.DefaultSpecialValueFormatter;
+import gregtech.nei.formatter.INEISpecialInfoFormatter;
+
+/**
+ * Builder class for {@link NEIRecipeProperties}.
+ */
+@SuppressWarnings("UnusedReturnValue")
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public final class NEIRecipePropertiesBuilder {
+
+ private boolean registerNEI = true;
+ @Nullable
+ private UnaryOperator<HandlerInfo.Builder> handlerInfoCreator;
+
+ private Size recipeBackgroundSize = new Size(170, 82);
+ private Pos2d recipeBackgroundOffset = new Pos2d(3, 3);
+
+ private INEISpecialInfoFormatter neiSpecialInfoFormatter = DefaultSpecialValueFormatter.INSTANCE;
+
+ private boolean unificateOutput = true;
+ private boolean useCustomFilter;
+ private boolean renderRealStackSizes = true;
+
+ private Comparator<GT_Recipe> comparator = GT_Recipe::compareTo;
+
+ NEIRecipePropertiesBuilder() {}
+
+ public NEIRecipeProperties build() {
+ return new NEIRecipeProperties(
+ registerNEI,
+ handlerInfoCreator,
+ recipeBackgroundSize,
+ recipeBackgroundOffset,
+ neiSpecialInfoFormatter,
+ unificateOutput,
+ useCustomFilter,
+ renderRealStackSizes,
+ comparator);
+ }
+
+ public NEIRecipePropertiesBuilder disableRegisterNEI() {
+ this.registerNEI = false;
+ return this;
+ }
+
+ public NEIRecipePropertiesBuilder handlerInfoCreator(UnaryOperator<HandlerInfo.Builder> builderCreator) {
+ this.handlerInfoCreator = builderCreator;
+ return this;
+ }
+
+ public NEIRecipePropertiesBuilder recipeBackgroundSize(Size recipeBackgroundSize) {
+ this.recipeBackgroundSize = recipeBackgroundSize;
+ return this;
+ }
+
+ public NEIRecipePropertiesBuilder recipeBackgroundOffset(Pos2d recipeBackgroundOffset) {
+ this.recipeBackgroundOffset = recipeBackgroundOffset;
+ return this;
+ }
+
+ public NEIRecipePropertiesBuilder neiSpecialInfoFormatter(INEISpecialInfoFormatter neiSpecialInfoFormatter) {
+ this.neiSpecialInfoFormatter = neiSpecialInfoFormatter;
+ return this;
+ }
+
+ public NEIRecipePropertiesBuilder unificateOutput(boolean unificateOutput) {
+ this.unificateOutput = unificateOutput;
+ return this;
+ }
+
+ public NEIRecipePropertiesBuilder useCustomFilter() {
+ this.useCustomFilter = true;
+ return this;
+ }
+
+ public NEIRecipePropertiesBuilder disableRenderRealStackSizes() {
+ this.renderRealStackSizes = false;
+ return this;
+ }
+
+ public NEIRecipePropertiesBuilder recipeComparator(Comparator<GT_Recipe> comparator) {
+ this.comparator = comparator;
+ return this;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeCategories.java b/src/main/java/gregtech/api/recipe/RecipeCategories.java
new file mode 100644
index 0000000000..61eed8a8d7
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeCategories.java
@@ -0,0 +1,71 @@
+package gregtech.api.recipe;
+
+import static gregtech.api.enums.Mods.GregTech;
+import static gregtech.api.recipe.RecipeCategory.createIcon;
+
+@SuppressWarnings("unused")
+public final class RecipeCategories {
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory arcFurnaceRecycling = new RecipeCategory(
+ "gt.recipe.category.arc_furnace_recycling",
+ RecipeMaps.arcFurnaceRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "arc_furnace_recycling.png"))));
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory plasmaArcFurnaceRecycling = new RecipeCategory(
+ "gt.recipe.category.plasma_arc_furnace_recycling",
+ RecipeMaps.plasmaArcFurnaceRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "plasma_arc_furnace_recycling.png"))));
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory maceratorRecycling = new RecipeCategory(
+ "gt.recipe.category.macerator_recycling",
+ RecipeMaps.maceratorRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "macerator_recycling.png"))));
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory fluidExtractorRecycling = new RecipeCategory(
+ "gt.recipe.category.fluid_extractor_recycling",
+ RecipeMaps.fluidExtractionRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "fluid_extractor_recycling.png"))));
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory alloySmelterRecycling = new RecipeCategory(
+ "gt.recipe.category.alloy_smelter_recycling",
+ RecipeMaps.alloySmelterRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "alloy_smelter_recycling.png"))));
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory alloySmelterMolding = new RecipeCategory(
+ "gt.recipe.category.alloy_smelter_molding",
+ RecipeMaps.alloySmelterRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "alloy_smelter_molding.png"))));
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory forgeHammerRecycling = new RecipeCategory(
+ "gt.recipe.category.forge_hammer_recycling",
+ RecipeMaps.hammerRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "forge_hammer_recycling.png"))));
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory ticPartExtruding = new RecipeCategory(
+ "gt.recipe.category.tic_part_extruding",
+ RecipeMaps.extruderRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "tic_part_extruding.png"))));
+
+ @RecipeCategoryHolder
+ public static final RecipeCategory ticBoltMolding = new RecipeCategory(
+ "gt.recipe.category.tic_bolt_molding",
+ RecipeMaps.fluidSolidifierRecipes,
+ builder -> builder.setDisplayImage(
+ createIcon(GregTech.getResourcePath("textures", "gui", "picture", "tic_bolt_molding.png"))));
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeCategory.java b/src/main/java/gregtech/api/recipe/RecipeCategory.java
new file mode 100644
index 0000000000..9f8674e939
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeCategory.java
@@ -0,0 +1,81 @@
+package gregtech.api.recipe;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.UnaryOperator;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import codechicken.nei.drawable.DrawableBuilder;
+import codechicken.nei.drawable.DrawableResource;
+import codechicken.nei.recipe.HandlerInfo;
+import cpw.mods.fml.common.Loader;
+import cpw.mods.fml.common.ModContainer;
+import gregtech.api.util.FieldsAreNonnullByDefault;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Allows certain recipes to be displayed on different tabs on NEI.
+ * <p>
+ * Also apply {@link RecipeCategoryHolder} annotation to each instance to be picked up by GT config.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@FieldsAreNonnullByDefault
+public final class RecipeCategory {
+
+ public static final Map<String, RecipeCategory> ALL_RECIPE_CATEGORIES = new HashMap<>();
+
+ public final String unlocalizedName;
+ public final RecipeMap<?> recipeMap;
+ public final ModContainer ownerMod;
+ @Nullable
+ public final UnaryOperator<HandlerInfo.Builder> handlerInfoCreator;
+
+ /**
+ * @param unlocalizedName Unlocalized name of this category. Must be unique.
+ * @param recipeMap RecipeMap this category belongs to.
+ * @param handlerInfoCreator Supplier of handler info for the NEI handler this category belongs to.
+ */
+ public RecipeCategory(String unlocalizedName, RecipeMap<?> recipeMap,
+ @Nullable UnaryOperator<HandlerInfo.Builder> handlerInfoCreator) {
+ this.unlocalizedName = unlocalizedName;
+ this.recipeMap = recipeMap;
+ this.ownerMod = Loader.instance()
+ .activeModContainer();
+ this.handlerInfoCreator = handlerInfoCreator;
+ if (ALL_RECIPE_CATEGORIES.containsKey(unlocalizedName)) {
+ throw new IllegalArgumentException(
+ "Cannot register recipe category with duplicated unlocalized name: " + unlocalizedName);
+ }
+ ALL_RECIPE_CATEGORIES.put(unlocalizedName, this);
+ }
+
+ RecipeCategory(RecipeMap<?> recipeMap) {
+ this(recipeMap.unlocalizedName, recipeMap, recipeMap.getFrontend().neiProperties.handlerInfoCreator);
+ }
+
+ @Override
+ public String toString() {
+ return "RecipeCategory{" + "unlocalizedName='"
+ + unlocalizedName
+ + '\''
+ + ", recipeMap="
+ + recipeMap.unlocalizedName
+ + ", ownerMod="
+ + ownerMod.getModId()
+ + '}';
+ }
+
+ /**
+ * Util method for creating icon for recipe category. Size is 16px.
+ */
+ public static DrawableResource createIcon(String resourceLocation) {
+ return new DrawableBuilder(resourceLocation, 0, 0, 16, 16)
+ // GuiRecipeTab#drawForeground draws icon with 1px offset to make fuel icon (14px) prettier
+ .addPadding(-1, 0, -1, 0)
+ .setTextureSize(16, 16)
+ .build();
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeCategoryHolder.java b/src/main/java/gregtech/api/recipe/RecipeCategoryHolder.java
new file mode 100644
index 0000000000..0ad87e3f6f
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeCategoryHolder.java
@@ -0,0 +1,13 @@
+package gregtech.api.recipe;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Apply this annotation to each recipe category so that GT config will pick it up.
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RecipeCategoryHolder {}
diff --git a/src/main/java/gregtech/api/recipe/RecipeCategorySetting.java b/src/main/java/gregtech/api/recipe/RecipeCategorySetting.java
new file mode 100644
index 0000000000..4920d64212
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeCategorySetting.java
@@ -0,0 +1,52 @@
+package gregtech.api.recipe;
+
+import java.util.Locale;
+import java.util.stream.Stream;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Specifies behaviors for {@link RecipeCategory}.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public enum RecipeCategorySetting {
+
+ /**
+ * Show the category in separated NEI tab.
+ */
+ ENABLE,
+ /**
+ * The category is merged to default one for the recipemap.
+ */
+ MERGE,
+ /**
+ * Recipes belonging to the category are hidden from NEI.
+ */
+ HIDE;
+
+ public static final RecipeCategorySetting[] VALUES = values();
+ public static final String[] NAMES = Stream.of(VALUES)
+ .map(RecipeCategorySetting::toName)
+ .toArray(String[]::new);
+
+ public static RecipeCategorySetting getDefault() {
+ return ENABLE;
+ }
+
+ public String toName() {
+ return toString().toLowerCase(Locale.ENGLISH);
+ }
+
+ public static RecipeCategorySetting find(String name) {
+ for (RecipeCategorySetting setting : VALUES) {
+ if (setting.toName()
+ .equals(name)) {
+ return setting;
+ }
+ }
+ return getDefault();
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeMap.java b/src/main/java/gregtech/api/recipe/RecipeMap.java
new file mode 100644
index 0000000000..2ee2d3cb94
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeMap.java
@@ -0,0 +1,395 @@
+package gregtech.api.recipe;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import org.jetbrains.annotations.Unmodifiable;
+
+import gregtech.api.interfaces.IRecipeMap;
+import gregtech.api.interfaces.tileentity.IHasWorldObjectAndCoords;
+import gregtech.api.util.FieldsAreNonnullByDefault;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Manages list of recipes. Its functionalities are split
+ * between {@link RecipeMapBackend} and {@link RecipeMapFrontend}.
+ *
+ * @param <B> Type of {@link RecipeMapBackend}
+ */
+@SuppressWarnings({ "unused", "UnusedReturnValue" })
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@FieldsAreNonnullByDefault
+public final class RecipeMap<B extends RecipeMapBackend> implements IRecipeMap {
+
+ /**
+ * All the recipemap instances. key=unlocalized name, value=instance.
+ */
+ public static final Map<String, RecipeMap<?>> ALL_RECIPE_MAPS = new HashMap<>();
+
+ private final B backend;
+ private final RecipeMapFrontend frontend;
+
+ /**
+ * Unique unlocalized name of this recipemap. Used for identifier, localization key for NEI tab name, etc.
+ */
+ public final String unlocalizedName;
+
+ private final RecipeCategory defaultRecipeCategory;
+
+ /**
+ * Use {@link RecipeMapBuilder} to instantiate.
+ */
+ RecipeMap(String unlocalizedName, B backend, RecipeMapFrontend frontend) {
+ this.unlocalizedName = unlocalizedName;
+ this.backend = backend;
+ this.frontend = frontend;
+ this.defaultRecipeCategory = new RecipeCategory(this);
+ backend.setRecipeMap(this);
+ if (ALL_RECIPE_MAPS.containsKey(unlocalizedName)) {
+ throw new IllegalArgumentException(
+ "Cannot register recipemap with duplicated unlocalized name: " + unlocalizedName);
+ }
+ ALL_RECIPE_MAPS.put(unlocalizedName, this);
+ }
+
+ public B getBackend() {
+ return backend;
+ }
+
+ public RecipeMapFrontend getFrontend() {
+ return frontend;
+ }
+
+ /**
+ * @return All the recipes belonging to this recipemap.
+ */
+ @Unmodifiable
+ public Collection<GT_Recipe> getAllRecipes() {
+ return backend.getAllRecipes();
+ }
+
+ /**
+ * @return List of registered recipe categories associated with this recipemap.
+ */
+ public List<RecipeCategory> getAssociatedCategories() {
+ return RecipeCategory.ALL_RECIPE_CATEGORIES.values()
+ .stream()
+ .filter(category -> category.recipeMap == this)
+ .collect(Collectors.toList());
+ }
+
+ public RecipeCategory getDefaultRecipeCategory() {
+ return defaultRecipeCategory;
+ }
+
+ /**
+ * @return Amperage of this recipemap. Note that recipes store EU/t with amperage included,
+ * e.g. Arc Furnace recipe with 90 EU/t means 30 EU/t (LV) with 3 amperage.
+ */
+ public int getAmperage() {
+ return frontend.getUIProperties().amperage;
+ }
+
+ @Override
+ public void addDownstream(IRecipeMap downstream) {
+ backend.addDownstream(downstream);
+ }
+
+ // region add recipe
+
+ @Nullable
+ public GT_Recipe addRecipe(boolean aOptimize, @Nullable ItemStack[] aInputs, @Nullable ItemStack[] aOutputs,
+ @Nullable Object aSpecial, @Nullable int[] aOutputChances, @Nullable FluidStack[] aFluidInputs,
+ @Nullable FluidStack[] aFluidOutputs, int aDuration, int aEUt, int aSpecialValue) {
+ return addRecipe(
+ new GT_Recipe(
+ aOptimize,
+ aInputs,
+ aOutputs,
+ aSpecial,
+ aOutputChances,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue));
+ }
+
+ @Nullable
+ public GT_Recipe addRecipe(@Nullable int[] aOutputChances, @Nullable FluidStack[] aFluidInputs,
+ @Nullable FluidStack[] aFluidOutputs, int aDuration, int aEUt, int aSpecialValue) {
+ return addRecipe(
+ new GT_Recipe(
+ false,
+ null,
+ null,
+ null,
+ aOutputChances,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue),
+ false,
+ false,
+ false);
+ }
+
+ @Nullable
+ public GT_Recipe addRecipe(boolean aOptimize, @Nullable ItemStack[] aInputs, @Nullable ItemStack[] aOutputs,
+ @Nullable Object aSpecial, @Nullable FluidStack[] aFluidInputs, @Nullable FluidStack[] aFluidOutputs,
+ int aDuration, int aEUt, int aSpecialValue) {
+ return addRecipe(
+ new GT_Recipe(
+ aOptimize,
+ aInputs,
+ aOutputs,
+ aSpecial,
+ null,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue));
+ }
+
+ @Nullable
+ public GT_Recipe addRecipe(GT_Recipe aRecipe) {
+ return addRecipe(aRecipe, true, false, false);
+ }
+
+ @Nullable
+ public GT_Recipe addRecipe(GT_Recipe aRecipe, boolean aCheckForCollisions, boolean aFakeRecipe, boolean aHidden) {
+ aRecipe.mHidden = aHidden;
+ aRecipe.mFakeRecipe = aFakeRecipe;
+ if (aRecipe.mFluidInputs.length < backend.properties.minFluidInputs
+ && aRecipe.mInputs.length < backend.properties.minItemInputs) return null;
+ if (aCheckForCollisions && backend.checkCollision(aRecipe)) return null;
+ return backend.compileRecipe(aRecipe);
+ }
+
+ /**
+ * Only used for fake Recipe Handlers to show something in NEI, do not use this for adding actual Recipes!
+ * findRecipe won't find fake Recipes, containsInput WILL find fake Recipes
+ */
+ @Nullable
+ public GT_Recipe addFakeRecipe(boolean aCheckForCollisions, @Nullable ItemStack[] aInputs,
+ @Nullable ItemStack[] aOutputs, @Nullable Object aSpecial, @Nullable int[] aOutputChances,
+ @Nullable FluidStack[] aFluidInputs, @Nullable FluidStack[] aFluidOutputs, int aDuration, int aEUt,
+ int aSpecialValue) {
+ return addFakeRecipe(
+ aCheckForCollisions,
+ new GT_Recipe(
+ false,
+ aInputs,
+ aOutputs,
+ aSpecial,
+ aOutputChances,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue));
+ }
+
+ /**
+ * Only used for fake Recipe Handlers to show something in NEI, do not use this for adding actual Recipes!
+ * findRecipe won't find fake Recipes, containsInput WILL find fake Recipes
+ */
+ @Nullable
+ public GT_Recipe addFakeRecipe(boolean aCheckForCollisions, @Nullable ItemStack[] aInputs,
+ @Nullable ItemStack[] aOutputs, @Nullable Object aSpecial, @Nullable FluidStack[] aFluidInputs,
+ @Nullable FluidStack[] aFluidOutputs, int aDuration, int aEUt, int aSpecialValue) {
+ return addFakeRecipe(
+ aCheckForCollisions,
+ new GT_Recipe(
+ false,
+ aInputs,
+ aOutputs,
+ aSpecial,
+ null,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue));
+ }
+
+ @Nullable
+ public GT_Recipe addFakeRecipe(boolean aCheckForCollisions, @Nullable ItemStack[] aInputs,
+ @Nullable ItemStack[] aOutputs, @Nullable Object aSpecial, @Nullable FluidStack[] aFluidInputs,
+ @Nullable FluidStack[] aFluidOutputs, int aDuration, int aEUt, int aSpecialValue, boolean hidden) {
+ return addFakeRecipe(
+ aCheckForCollisions,
+ new GT_Recipe(
+ false,
+ aInputs,
+ aOutputs,
+ aSpecial,
+ null,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue),
+ hidden);
+ }
+
+ @Nullable
+ public GT_Recipe addFakeRecipe(boolean aCheckForCollisions, @Nullable ItemStack[] aInputs,
+ @Nullable ItemStack[] aOutputs, @Nullable Object aSpecial, @Nullable FluidStack[] aFluidInputs,
+ @Nullable FluidStack[] aFluidOutputs, int aDuration, int aEUt, int aSpecialValue, ItemStack[][] aAlt,
+ boolean hidden) {
+ return addFakeRecipe(
+ aCheckForCollisions,
+ new GT_Recipe.GT_Recipe_WithAlt(
+ false,
+ aInputs,
+ aOutputs,
+ aSpecial,
+ null,
+ aFluidInputs,
+ aFluidOutputs,
+ aDuration,
+ aEUt,
+ aSpecialValue,
+ aAlt),
+ hidden);
+ }
+
+ /**
+ * Only used for fake Recipe Handlers to show something in NEI, do not use this for adding actual Recipes!
+ * findRecipe won't find fake Recipes, containsInput WILL find fake Recipes
+ */
+ @Nullable
+ public GT_Recipe addFakeRecipe(boolean aCheckForCollisions, GT_Recipe aRecipe) {
+ return addRecipe(aRecipe, aCheckForCollisions, true, false);
+ }
+
+ @Nullable
+ public GT_Recipe addFakeRecipe(boolean aCheckForCollisions, GT_Recipe aRecipe, boolean hidden) {
+ return addRecipe(aRecipe, aCheckForCollisions, true, hidden);
+ }
+
+ @Nonnull
+ @Override
+ public Collection<GT_Recipe> doAdd(GT_RecipeBuilder builder) {
+ return backend.doAdd(builder);
+ }
+
+ public GT_Recipe add(GT_Recipe aRecipe) {
+ return backend.compileRecipe(aRecipe);
+ }
+
+ // endregion
+
+ /**
+ * @return if this Item is a valid Input for any for the Recipes
+ */
+ public boolean containsInput(@Nullable ItemStack aStack) {
+ return aStack != null && backend.containsInput(aStack);
+ }
+
+ /**
+ * @return if this Fluid is a valid Input for any for the Recipes
+ */
+ public boolean containsInput(@Nullable FluidStack aFluid) {
+ return aFluid != null && containsInput(aFluid.getFluid());
+ }
+
+ /**
+ * @return if this Fluid is a valid Input for any for the Recipes
+ */
+ public boolean containsInput(@Nullable Fluid aFluid) {
+ return aFluid != null && backend.containsInput(aFluid);
+ }
+
+ // region find recipe
+
+ /**
+ * @return Entrypoint for fluent API for finding recipe.
+ */
+ public FindRecipeQuery findRecipeQuery() {
+ return new FindRecipeQuery(this);
+ }
+
+ @Nullable
+ public GT_Recipe findRecipe(@Nullable IHasWorldObjectAndCoords aTileEntity, boolean aNotUnificated, long aVoltage,
+ @Nullable FluidStack[] aFluids, @Nullable ItemStack... aInputs) {
+ return findRecipe(aTileEntity, null, aNotUnificated, aVoltage, aFluids, null, aInputs);
+ }
+
+ @Nullable
+ public GT_Recipe findRecipe(@Nullable IHasWorldObjectAndCoords aTileEntity, boolean aNotUnificated,
+ boolean aDontCheckStackSizes, long aVoltage, @Nullable FluidStack[] aFluids, @Nullable ItemStack... aInputs) {
+ return findRecipe(aTileEntity, null, aNotUnificated, aDontCheckStackSizes, aVoltage, aFluids, null, aInputs);
+ }
+
+ @Nullable
+ public GT_Recipe findRecipe(@Nullable IHasWorldObjectAndCoords aTileEntity, @Nullable GT_Recipe aRecipe,
+ boolean aNotUnificated, long aVoltage, @Nullable FluidStack[] aFluids, @Nullable ItemStack aSpecialSlot,
+ @Nullable ItemStack... aInputs) {
+ return findRecipe(aTileEntity, aRecipe, aNotUnificated, false, aVoltage, aFluids, aSpecialSlot, aInputs);
+ }
+
+ @Nullable
+ public GT_Recipe findRecipe(@Nullable IHasWorldObjectAndCoords aTileEntity, @Nullable GT_Recipe aRecipe,
+ boolean aNotUnificated, boolean aDontCheckStackSizes, long aVoltage, @Nullable FluidStack[] aFluids,
+ @Nullable ItemStack aSpecialSlot, @Nullable ItemStack... aInputs) {
+ return findRecipeQuery().items(aInputs != null ? aInputs : new ItemStack[0])
+ .fluids(aFluids != null ? aFluids : new FluidStack[0])
+ .specialSlot(aSpecialSlot)
+ .voltage(aVoltage)
+ .cachedRecipe(aRecipe)
+ .notUnificated(aNotUnificated)
+ .dontCheckStackSizes(aDontCheckStackSizes)
+ .find();
+ }
+
+ // endregion
+
+ @Override
+ public String toString() {
+ return "RecipeMap{" + "unlocalizedName='"
+ + unlocalizedName
+ + '\''
+ + ", ownerMod="
+ + defaultRecipeCategory.ownerMod.getModId()
+ + '}';
+ }
+
+ private static final Pattern LEGACY_IDENTIFIER_PATTERN = Pattern.compile("(.+)_[0-9]+_[0-9]+_[0-9]+_[0-9]+_[0-9]+");
+
+ /**
+ * Gets recipemap instance from old mUniqueIdentifier format. This is only for backward compat, where tiles
+ * saved recipemap with mUniqueIdentifier.
+ *
+ * @param legacyIdentifier mUniqueIdentifier, in %s_%d_%d_%d_%d_%d format
+ * @return Found recipemap, can be null
+ */
+ @Nullable
+ public static RecipeMap<?> getFromOldIdentifier(String legacyIdentifier) {
+ Matcher matcher = LEGACY_IDENTIFIER_PATTERN.matcher(legacyIdentifier);
+ if (!matcher.find()) {
+ // It can be new format
+ return ALL_RECIPE_MAPS.get(legacyIdentifier);
+ }
+ return ALL_RECIPE_MAPS.get(matcher.group(1));
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeMapBackend.java b/src/main/java/gregtech/api/recipe/RecipeMapBackend.java
new file mode 100644
index 0000000000..a539067e93
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeMapBackend.java
@@ -0,0 +1,469 @@
+package gregtech.api.recipe;
+
+import static gregtech.api.util.GT_RecipeBuilder.handleInvalidRecipe;
+import static gregtech.api.util.GT_RecipeBuilder.handleRecipeCollision;
+import static gregtech.api.util.GT_Utility.areStacksEqualOrNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import org.jetbrains.annotations.Unmodifiable;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.interfaces.IRecipeMap;
+import gregtech.api.objects.GT_ItemStack;
+import gregtech.api.util.GT_OreDictUnificator;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.GT_StreamUtil;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Responsible for recipe addition / search for recipemap.
+ * <p>
+ * In order to bind custom backend to recipemap, use {@link RecipeMapBuilder#of(String, BackendCreator)}.
+ */
+@SuppressWarnings("unused")
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class RecipeMapBackend {
+
+ private RecipeMap<?> recipeMap;
+
+ /**
+ * Recipe index based on items.
+ */
+ private final SetMultimap<GT_ItemStack, GT_Recipe> itemIndex = HashMultimap.create();
+ /**
+ * Recipe index based on fluids.
+ */
+ private final SetMultimap<String, GT_Recipe> fluidIndex = HashMultimap.create();
+
+ /**
+ * All the recipes belonging to this backend, indexed by recipe category.
+ */
+ private final Map<RecipeCategory, Collection<GT_Recipe>> recipesByCategory = new HashMap<>();
+
+ /**
+ * List of recipemaps that also receive recipe addition from this backend.
+ */
+ private final List<IRecipeMap> downstreams = new ArrayList<>(0);
+
+ /**
+ * All the properties specific to this backend.
+ */
+ protected final RecipeMapBackendProperties properties;
+
+ public RecipeMapBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ this.properties = propertiesBuilder.build();
+ GregTech_API.itemStackMultiMaps.add(itemIndex);
+ }
+
+ void setRecipeMap(RecipeMap<?> recipeMap) {
+ this.recipeMap = recipeMap;
+ }
+
+ /**
+ * @return Properties specific to this backend.
+ */
+ public RecipeMapBackendProperties getProperties() {
+ return properties;
+ }
+
+ /**
+ * @return All the recipes belonging to this backend. Returned collection is immutable,
+ * use {@link #compileRecipe} to add / {@link #removeRecipes} to remove.
+ */
+ @Unmodifiable
+ public Collection<GT_Recipe> getAllRecipes() {
+ return Collections.unmodifiableCollection(allRecipes());
+ }
+
+ /**
+ * @return Raw recipe list
+ */
+ private Collection<GT_Recipe> allRecipes() {
+ return recipesByCategory.values()
+ .stream()
+ .flatMap(Collection::stream)
+ .collect(Collectors.toCollection(ArrayList::new));
+ }
+
+ /**
+ * @return All the recipes belonging to this backend, indexed by recipe category.
+ */
+ @Unmodifiable
+ public Collection<GT_Recipe> getRecipesByCategory(RecipeCategory recipeCategory) {
+ return Collections
+ .unmodifiableCollection(recipesByCategory.getOrDefault(recipeCategory, Collections.emptyList()));
+ }
+
+ @Unmodifiable
+ public Map<RecipeCategory, Collection<GT_Recipe>> getRecipeCategoryMap() {
+ return Collections.unmodifiableMap(recipesByCategory);
+ }
+
+ // region add recipe
+
+ /**
+ * Adds the supplied recipe to the recipe list and index, without any check.
+ *
+ * @return Supplied recipe.
+ */
+ public GT_Recipe compileRecipe(GT_Recipe recipe) {
+ if (recipe.getRecipeCategory() == null) {
+ recipe.setRecipeCategory(recipeMap.getDefaultRecipeCategory());
+ }
+ recipesByCategory.computeIfAbsent(recipe.getRecipeCategory(), v -> new ArrayList<>())
+ .add(recipe);
+ for (FluidStack fluid : recipe.mFluidInputs) {
+ if (fluid == null) continue;
+ fluidIndex.put(
+ fluid.getFluid()
+ .getName(),
+ recipe);
+ }
+ return addToItemMap(recipe);
+ }
+
+ /**
+ * Adds the supplied recipe to the item cache.
+ */
+ protected GT_Recipe addToItemMap(GT_Recipe recipe) {
+ for (ItemStack item : recipe.mInputs) {
+ if (item == null) continue;
+ itemIndex.put(new GT_ItemStack(item), recipe);
+ }
+ if (recipe instanceof GT_Recipe.GT_Recipe_WithAlt recipeWithAlt) {
+ for (ItemStack[] itemStacks : recipeWithAlt.mOreDictAlt) {
+ if (itemStacks == null) continue;
+ for (ItemStack item : itemStacks) {
+ if (item == null) continue;
+ itemIndex.put(new GT_ItemStack(item), recipe);
+ }
+ }
+ }
+ return recipe;
+ }
+
+ /**
+ * Builds recipe from supplied recipe builder and adds it.
+ */
+ protected Collection<GT_Recipe> doAdd(GT_RecipeBuilder builder) {
+ Iterable<? extends GT_Recipe> recipes = properties.recipeEmitter.apply(builder);
+ Collection<GT_Recipe> ret = new ArrayList<>();
+ for (GT_Recipe recipe : recipes) {
+ if (properties.recipeConfigCategory != null) {
+ assert properties.recipeConfigKeyConvertor != null;
+ String configKey = properties.recipeConfigKeyConvertor.apply(recipe);
+ if (configKey != null && recipe.mDuration <= 0) {
+ continue;
+ }
+ }
+ if (recipe.mFluidInputs.length < properties.minFluidInputs
+ || recipe.mInputs.length < properties.minItemInputs) {
+ return Collections.emptyList();
+ }
+ if (properties.recipeTransformer != null) {
+ recipe = properties.recipeTransformer.apply(recipe);
+ }
+ if (recipe == null) continue;
+ if (builder.isCheckForCollision() && checkCollision(recipe)) {
+ handleCollision(recipe);
+ continue;
+ }
+ if (recipe.getRecipeCategory() != null && recipe.getRecipeCategory().recipeMap != this.recipeMap) {
+ handleInvalidRecipe();
+ continue;
+ }
+ ret.add(compileRecipe(recipe));
+ }
+ if (!ret.isEmpty()) {
+ builder.clearInvalid();
+ for (IRecipeMap downstream : downstreams) {
+ downstream.doAdd(builder);
+ }
+ }
+ return ret;
+ }
+
+ private void handleCollision(GT_Recipe recipe) {
+ StringBuilder errorInfo = new StringBuilder();
+ boolean hasAnEntry = false;
+ for (FluidStack fluid : recipe.mFluidInputs) {
+ if (fluid == null) {
+ continue;
+ }
+ String s = fluid.getLocalizedName();
+ if (s == null) {
+ continue;
+ }
+ if (hasAnEntry) {
+ errorInfo.append("+")
+ .append(s);
+ } else {
+ errorInfo.append(s);
+ }
+ hasAnEntry = true;
+ }
+ for (ItemStack item : recipe.mInputs) {
+ if (item == null) {
+ continue;
+ }
+ String itemName = item.getDisplayName();
+ if (hasAnEntry) {
+ errorInfo.append("+")
+ .append(itemName);
+ } else {
+ errorInfo.append(itemName);
+ }
+ hasAnEntry = true;
+ }
+ handleRecipeCollision(errorInfo.toString());
+ }
+
+ void addDownstream(IRecipeMap downstream) {
+ downstreams.add(downstream);
+ }
+
+ /**
+ * Removes supplied recipes from recipe list. Do not use unless absolute necessity!
+ */
+ public void removeRecipes(Collection<? extends GT_Recipe> recipesToRemove) {
+ for (Collection<GT_Recipe> recipes : recipesByCategory.values()) {
+ recipes.removeAll(recipesToRemove);
+ }
+ for (GT_ItemStack key : new HashMap<>(itemIndex.asMap()).keySet()) {
+ itemIndex.get(key)
+ .removeAll(recipesToRemove);
+ }
+ for (String key : new HashMap<>(fluidIndex.asMap()).keySet()) {
+ fluidIndex.get(key)
+ .removeAll(recipesToRemove);
+ }
+ }
+
+ /**
+ * Removes supplied recipe from recipe list. Do not use unless absolute necessity!
+ */
+ public void removeRecipe(GT_Recipe recipe) {
+ removeRecipes(Collections.singleton(recipe));
+ }
+
+ /**
+ * If you want to shoot your foot...
+ */
+ public void clearRecipes() {
+ recipesByCategory.clear();
+ }
+
+ // endregion
+
+ /**
+ * Re-unificates all the items present in recipes. Also reflects recipe removals.
+ */
+ public void reInit() {
+ itemIndex.clear();
+ for (GT_Recipe recipe : allRecipes()) {
+ GT_OreDictUnificator.setStackArray(true, recipe.mInputs);
+ GT_OreDictUnificator.setStackArray(true, recipe.mOutputs);
+ addToItemMap(recipe);
+ }
+ }
+
+ /**
+ * @return If supplied item is a valid input for any of the recipes
+ */
+ public boolean containsInput(ItemStack item) {
+ return itemIndex.containsKey(new GT_ItemStack(item)) || itemIndex.containsKey(new GT_ItemStack(item, true));
+ }
+
+ /**
+ * @return If supplied fluid is a valid input for any of the recipes
+ */
+ public boolean containsInput(Fluid fluid) {
+ return fluidIndex.containsKey(fluid.getName());
+ }
+
+ // region find recipe
+
+ /**
+ * Checks if given recipe conflicts with already registered recipes.
+ *
+ * @return True if collision is found.
+ */
+ boolean checkCollision(GT_Recipe recipe) {
+ return matchRecipeStream(recipe.mInputs, recipe.mFluidInputs, null, null, false, true, true).findAny()
+ .isPresent();
+ }
+
+ /**
+ * Overwrites {@link #matchRecipeStream} method. Also override {@link #doesOverwriteFindRecipe} to make it work.
+ */
+ @Nullable
+ protected GT_Recipe overwriteFindRecipe(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot,
+ @Nullable GT_Recipe cachedRecipe) {
+ return null;
+ }
+
+ /**
+ * @return Whether to use {@link #overwriteFindRecipe} for finding recipe.
+ */
+ protected boolean doesOverwriteFindRecipe() {
+ return false;
+ }
+
+ /**
+ * Modifies successfully found recipe. Make sure not to mutate the found recipe but use copy!
+ */
+ @Nullable
+ protected GT_Recipe modifyFoundRecipe(GT_Recipe recipe, ItemStack[] items, FluidStack[] fluids,
+ @Nullable ItemStack specialSlot) {
+ return recipe;
+ }
+
+ /**
+ * Called when {@link #matchRecipeStream} cannot find recipe.
+ */
+ @Nullable
+ protected GT_Recipe findFallback(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot) {
+ return null;
+ }
+
+ /**
+ * Returns all the matched recipes in the form of Stream, without any additional check for matches.
+ *
+ * @param rawItems Item inputs.
+ * @param fluids Fluid inputs.
+ * @param specialSlot Content of the special slot. Normal recipemaps don't need this, but some do.
+ * Set {@link RecipeMapBuilder#specialSlotSensitive} to make it actually functional.
+ * Alternatively overriding {@link #filterFindRecipe} will also work.
+ * @param cachedRecipe If this is not null, this method tests it before all other recipes.
+ * @param notUnificated If this is set to true, item inputs will be unificated.
+ * @param dontCheckStackSizes If this is set to true, this method won't check item count and fluid amount
+ * for the matched recipe.
+ * @param forCollisionCheck If this method is called to check collision with already registered recipes.
+ * @return Stream of matches recipes.
+ */
+ Stream<GT_Recipe> matchRecipeStream(ItemStack[] rawItems, FluidStack[] fluids, @Nullable ItemStack specialSlot,
+ @Nullable GT_Recipe cachedRecipe, boolean notUnificated, boolean dontCheckStackSizes,
+ boolean forCollisionCheck) {
+ if (doesOverwriteFindRecipe()) {
+ return GT_StreamUtil.ofNullable(overwriteFindRecipe(rawItems, fluids, specialSlot, cachedRecipe));
+ }
+
+ if (recipesByCategory.isEmpty()) {
+ return Stream.empty();
+ }
+
+ // Some recipe classes require a certain amount of inputs of certain kinds. Like "at least 1 fluid + 1 item"
+ // or "at least 2 items" before they start searching for recipes.
+ // This improves performance massively, especially when people leave things like programmed circuits,
+ // molds or shapes in their machines.
+ // For checking collision, we assume min inputs check already has been passed as of building the recipe.
+ if (!forCollisionCheck) {
+ if (properties.minFluidInputs > 0) {
+ int count = 0;
+ for (FluidStack fluid : fluids) if (fluid != null) count++;
+ if (count < properties.minFluidInputs) {
+ return Stream.empty();
+ }
+ }
+ if (properties.minItemInputs > 0) {
+ int count = 0;
+ for (ItemStack item : rawItems) if (item != null) count++;
+ if (count < properties.minItemInputs) {
+ return Stream.empty();
+ }
+ }
+ }
+
+ ItemStack[] items;
+ // Unification happens here in case the item input isn't already unificated.
+ if (notUnificated) {
+ items = GT_OreDictUnificator.getStackArray(true, (Object[]) rawItems);
+ } else {
+ items = rawItems;
+ }
+
+ return Stream.<Stream<GT_Recipe>>of(
+ // Check the recipe which has been used last time in order to not have to search for it again, if possible.
+ GT_StreamUtil.ofNullable(cachedRecipe)
+ .filter(recipe -> recipe.mCanBeBuffered)
+ .filter(recipe -> filterFindRecipe(recipe, items, fluids, specialSlot, dontCheckStackSizes))
+ .map(recipe -> modifyFoundRecipe(recipe, items, fluids, specialSlot))
+ .filter(Objects::nonNull),
+ // Now look for the recipes inside the item index, but only when the recipes actually can have items inputs.
+ GT_StreamUtil.ofConditional(!itemIndex.isEmpty(), items)
+ .filter(Objects::nonNull)
+ .flatMap(item -> Stream.of(new GT_ItemStack(item), new GT_ItemStack(item, true)))
+ .map(itemIndex::get)
+ .flatMap(Collection::stream)
+ .filter(recipe -> filterFindRecipe(recipe, items, fluids, specialSlot, dontCheckStackSizes))
+ .map(recipe -> modifyFoundRecipe(recipe, items, fluids, specialSlot))
+ .filter(Objects::nonNull),
+ // If the minimum amount of items required for the recipes is 0, then it could match to fluid-only recipes,
+ // so check fluid index too.
+ GT_StreamUtil.ofConditional(properties.minItemInputs == 0, fluids)
+ .filter(Objects::nonNull)
+ .map(
+ fluidStack -> fluidIndex.get(
+ fluidStack.getFluid()
+ .getName()))
+ .flatMap(Collection::stream)
+ .filter(recipe -> filterFindRecipe(recipe, items, fluids, specialSlot, dontCheckStackSizes))
+ .map(recipe -> modifyFoundRecipe(recipe, items, fluids, specialSlot))
+ .filter(Objects::nonNull),
+ // Lastly, find fallback.
+ forCollisionCheck ? Stream.empty()
+ : GT_StreamUtil.ofSupplier(() -> findFallback(items, fluids, specialSlot))
+ .filter(Objects::nonNull))
+ .flatMap(Function.identity());
+ }
+
+ /**
+ * The minimum filter required for recipe match logic. You can override this to have custom validation.
+ * <p>
+ * Other checks like machine voltage will be done in another places.
+ * <p>
+ * Note that this won't be called if {@link #doesOverwriteFindRecipe} is true.
+ */
+ protected boolean filterFindRecipe(GT_Recipe recipe, ItemStack[] items, FluidStack[] fluids,
+ @Nullable ItemStack specialSlot, boolean dontCheckStackSizes) {
+ if (recipe.mEnabled && !recipe.mFakeRecipe
+ && recipe.isRecipeInputEqual(false, dontCheckStackSizes, fluids, items)) {
+ return !properties.specialSlotSensitive
+ || areStacksEqualOrNull((ItemStack) recipe.mSpecialItems, specialSlot);
+ }
+ return false;
+ }
+
+ // endregion
+
+ @FunctionalInterface
+ public interface BackendCreator<B extends RecipeMapBackend> {
+
+ /**
+ * @see RecipeMapBackend#RecipeMapBackend
+ */
+ B create(RecipeMapBackendPropertiesBuilder propertiesBuilder);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeMapBackendProperties.java b/src/main/java/gregtech/api/recipe/RecipeMapBackendProperties.java
new file mode 100644
index 0000000000..7262b794ab
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeMapBackendProperties.java
@@ -0,0 +1,77 @@
+package gregtech.api.recipe;
+
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import gregtech.api.util.FieldsAreNonnullByDefault;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Data object to store properties used for {@link RecipeMapBackend}. Use {@link #builder()} for creation.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@FieldsAreNonnullByDefault
+public final class RecipeMapBackendProperties {
+
+ static RecipeMapBackendPropertiesBuilder builder() {
+ return new RecipeMapBackendPropertiesBuilder();
+ }
+
+ /**
+ * Minimum amount of item inputs required for the recipes.
+ */
+ public final int minItemInputs;
+ /**
+ * Minimum amount of fluid inputs required for the recipes.
+ */
+ public final int minFluidInputs;
+
+ /**
+ * Whether this backend should check for equality of special slot when searching recipe.
+ */
+ public final boolean specialSlotSensitive;
+
+ /**
+ * If recipe builder should stop optimizing inputs.
+ */
+ public final boolean disableOptimize;
+
+ /**
+ * Changes how recipes are emitted by a particular recipe builder.
+ */
+ public final Function<? super GT_RecipeBuilder, ? extends Iterable<? extends GT_Recipe>> recipeEmitter;
+
+ /**
+ * Runs a custom hook on all recipes added <b>via builder</b>.
+ */
+ @Nullable
+ public final Function<? super GT_Recipe, ? extends GT_Recipe> recipeTransformer;
+
+ @Nullable
+ public final String recipeConfigCategory;
+ @Nullable
+ public final Function<? super GT_Recipe, String> recipeConfigKeyConvertor;
+
+ RecipeMapBackendProperties(int minItemInputs, int minFluidInputs, boolean specialSlotSensitive,
+ boolean disableOptimize,
+ Function<? super GT_RecipeBuilder, ? extends Iterable<? extends GT_Recipe>> recipeEmitter,
+ @Nullable Function<? super GT_Recipe, ? extends GT_Recipe> recipeTransformer,
+ @Nullable String recipeConfigCategory, @Nullable Function<? super GT_Recipe, String> recipeConfigKeyConvertor) {
+ if (minItemInputs < 0 || minFluidInputs < 0) {
+ throw new IllegalArgumentException("minItemInputs and minFluidInputs cannot be negative");
+ }
+ this.minItemInputs = minItemInputs;
+ this.minFluidInputs = minFluidInputs;
+ this.specialSlotSensitive = specialSlotSensitive;
+ this.disableOptimize = disableOptimize;
+ this.recipeEmitter = recipeEmitter;
+ this.recipeTransformer = recipeTransformer;
+ this.recipeConfigCategory = recipeConfigCategory;
+ this.recipeConfigKeyConvertor = recipeConfigKeyConvertor;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeMapBackendPropertiesBuilder.java b/src/main/java/gregtech/api/recipe/RecipeMapBackendPropertiesBuilder.java
new file mode 100644
index 0000000000..933ea1b06b
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeMapBackendPropertiesBuilder.java
@@ -0,0 +1,119 @@
+package gregtech.api.recipe;
+
+import static gregtech.api.util.GT_RecipeMapUtil.buildOrEmpty;
+
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import com.google.common.collect.Iterables;
+
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Builder class for {@link RecipeMapBackendProperties}.
+ */
+@SuppressWarnings({ "unused", "UnusedReturnValue" })
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public final class RecipeMapBackendPropertiesBuilder {
+
+ private int minItemInputs;
+ private int minFluidInputs;
+
+ private boolean specialSlotSensitive;
+
+ private boolean disableOptimize;
+
+ private Function<? super GT_RecipeBuilder, ? extends Iterable<? extends GT_Recipe>> recipeEmitter = this::defaultBuildRecipe;
+
+ @Nullable
+ private Function<? super GT_Recipe, ? extends GT_Recipe> recipeTransformer;
+
+ @Nullable
+ private String recipeConfigCategory;
+ @Nullable
+ private Function<? super GT_Recipe, String> recipeConfigKeyConvertor;
+
+ RecipeMapBackendPropertiesBuilder() {}
+
+ RecipeMapBackendProperties build() {
+ return new RecipeMapBackendProperties(
+ minItemInputs,
+ minFluidInputs,
+ specialSlotSensitive,
+ disableOptimize,
+ recipeEmitter,
+ recipeTransformer,
+ recipeConfigCategory,
+ recipeConfigKeyConvertor);
+ }
+
+ public RecipeMapBackendPropertiesBuilder minItemInputs(int minItemInputs) {
+ this.minItemInputs = minItemInputs;
+ return this;
+ }
+
+ public RecipeMapBackendPropertiesBuilder minFluidInputs(int minFluidInputs) {
+ this.minFluidInputs = minFluidInputs;
+ return this;
+ }
+
+ public RecipeMapBackendPropertiesBuilder specialSlotSensitive() {
+ this.specialSlotSensitive = true;
+ return this;
+ }
+
+ public RecipeMapBackendPropertiesBuilder disableOptimize() {
+ this.disableOptimize = true;
+ return this;
+ }
+
+ public RecipeMapBackendPropertiesBuilder recipeEmitter(
+ Function<? super GT_RecipeBuilder, ? extends Iterable<? extends GT_Recipe>> recipeEmitter) {
+ this.recipeEmitter = recipeEmitter;
+ return this;
+ }
+
+ public RecipeMapBackendPropertiesBuilder combineRecipeEmitter(
+ Function<? super GT_RecipeBuilder, ? extends Iterable<? extends GT_Recipe>> func) {
+ // move recipeEmitter to local variable, so lambda capture the function itself instead of this
+ Function<? super GT_RecipeBuilder, ? extends Iterable<? extends GT_Recipe>> cur = this.recipeEmitter;
+ return recipeEmitter(b -> Iterables.concat(cur.apply(b), func.apply(b)));
+ }
+
+ public RecipeMapBackendPropertiesBuilder recipeTransformer(
+ Function<? super GT_Recipe, ? extends GT_Recipe> recipeTransformer) {
+ this.recipeTransformer = recipeTransformer;
+ return this;
+ }
+
+ public RecipeMapBackendPropertiesBuilder chainRecipeTransformer(
+ Function<? super GT_Recipe, ? extends GT_Recipe> func) {
+ this.recipeTransformer = this.recipeTransformer == null ? func : this.recipeTransformer.andThen(func);
+ return this;
+ }
+
+ public RecipeMapBackendPropertiesBuilder recipeConfigFile(String category,
+ Function<? super GT_Recipe, String> keyConvertor) {
+ this.recipeConfigCategory = category;
+ this.recipeConfigKeyConvertor = keyConvertor;
+ return this;
+ }
+
+ private Iterable<? extends GT_Recipe> defaultBuildRecipe(GT_RecipeBuilder builder) {
+ // TODO sensible validation
+ GT_RecipeBuilder b = builder;
+ if (disableOptimize && builder.isOptimize()) {
+ b = copy(builder, b).noOptimize();
+ }
+ return buildOrEmpty(b);
+ }
+
+ private static GT_RecipeBuilder copy(GT_RecipeBuilder original, GT_RecipeBuilder b) {
+ return b == original ? b.copy() : b;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeMapBuilder.java b/src/main/java/gregtech/api/recipe/RecipeMapBuilder.java
new file mode 100644
index 0000000000..8659018934
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeMapBuilder.java
@@ -0,0 +1,522 @@
+package gregtech.api.recipe;
+
+import static gregtech.api.enums.Mods.GregTech;
+
+import java.awt.Rectangle;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.gtnewhorizons.modularui.api.drawable.FallbackableUITexture;
+import com.gtnewhorizons.modularui.api.drawable.IDrawable;
+import com.gtnewhorizons.modularui.api.drawable.UITexture;
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.math.Size;
+import com.gtnewhorizons.modularui.common.widget.ProgressBar;
+
+import codechicken.nei.recipe.HandlerInfo;
+import gregtech.api.gui.modularui.FallbackableSteamTexture;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.gui.modularui.SteamTexture;
+import gregtech.api.objects.overclockdescriber.OverclockDescriber;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.nei.formatter.INEISpecialInfoFormatter;
+
+// spotless:off spotless likes formatting @code to &#64;code
+/**
+ * Builder class for constructing {@link RecipeMap}. Instantiate this class and call {@link #build}
+ * to retrieve RecipeMap. Smallest example:
+ *
+ * <pre>
+ * {@code
+ * RecipeMap<RecipeMapBackend> exampleRecipes = RecipeMapBuilder.of("example")
+ * .maxIO(9, 4, 1, 1)
+ * .build();
+ * }
+ * </pre>
+ *
+ * Note that {@link #maxIO} is required to build.
+ */
+// spotless:on
+@SuppressWarnings("unused")
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public final class RecipeMapBuilder<B extends RecipeMapBackend> {
+
+ private final String unlocalizedName;
+ private final RecipeMapBackendPropertiesBuilder backendPropertiesBuilder = RecipeMapBackendProperties.builder();
+ private final RecipeMapBackend.BackendCreator<B> backendCreator;
+ private final BasicUIPropertiesBuilder uiPropertiesBuilder;
+ private final NEIRecipePropertiesBuilder neiPropertiesBuilder = NEIRecipeProperties.builder();
+ private RecipeMapFrontend.FrontendCreator frontendCreator = RecipeMapFrontend::new;
+
+ /**
+ * Constructs builder object for {@link RecipeMap} with given backend logic. For custom frontend,
+ * call {@link #frontend} for the created builder object.
+ *
+ * @param unlocalizedName Unique identifier for the recipemap. This is also used as translation key
+ * for NEI recipe GUI header, so add localization for it if needed.
+ * @return New builder object.
+ */
+ public static <B extends RecipeMapBackend> RecipeMapBuilder<B> of(String unlocalizedName,
+ RecipeMapBackend.BackendCreator<B> backendCreator) {
+ return new RecipeMapBuilder<>(unlocalizedName, backendCreator);
+ }
+
+ /**
+ * Constructs builder object for {@link RecipeMap}.
+ *
+ * @param unlocalizedName Unique identifier for the recipemap. This is also used as translation key
+ * for NEI recipe GUI header, so add localization for it if needed.
+ * @return New builder object.
+ */
+ public static RecipeMapBuilder<RecipeMapBackend> of(String unlocalizedName) {
+ return new RecipeMapBuilder<>(unlocalizedName, RecipeMapBackend::new);
+ }
+
+ private RecipeMapBuilder(String unlocalizedName, RecipeMapBackend.BackendCreator<B> backendCreator) {
+ this.unlocalizedName = unlocalizedName;
+ this.backendCreator = backendCreator;
+ this.uiPropertiesBuilder = BasicUIProperties.builder()
+ .progressBarTexture(GT_UITextures.fallbackableProgressbar(unlocalizedName, GT_UITextures.PROGRESSBAR_ARROW))
+ .neiTransferRectId(unlocalizedName);
+ }
+
+ // region backend
+
+ /**
+ * Sets minimum amount of inputs required for the recipes.
+ */
+ public RecipeMapBuilder<B> minInputs(int minItemInputs, int minFluidInputs) {
+ backendPropertiesBuilder.minItemInputs(minItemInputs)
+ .minFluidInputs(minFluidInputs);
+ return this;
+ }
+
+ /**
+ * Whether this recipemap should check for equality of special slot when searching recipe.
+ */
+ public RecipeMapBuilder<B> specialSlotSensitive() {
+ backendPropertiesBuilder.specialSlotSensitive();
+ return this;
+ }
+
+ /**
+ * If recipe builder should stop optimizing inputs.
+ */
+ public RecipeMapBuilder<B> disableOptimize() {
+ backendPropertiesBuilder.disableOptimize();
+ return this;
+ }
+
+ /**
+ * Changes how recipes are emitted by a particular recipe builder. Can emit multiple recipe per builder.
+ */
+ public RecipeMapBuilder<B> recipeEmitter(
+ Function<? super GT_RecipeBuilder, ? extends Iterable<? extends GT_Recipe>> recipeEmitter) {
+ backendPropertiesBuilder.recipeEmitter(recipeEmitter);
+ return this;
+ }
+
+ /**
+ * Changes how recipes are emitted by a particular recipe builder. Should not return null.
+ * <p>
+ * Recipes added via one of the overloads of addRecipe will NOT be affected by this function.
+ */
+ public RecipeMapBuilder<B> recipeEmitterSingle(
+ Function<? super GT_RecipeBuilder, ? extends GT_Recipe> recipeEmitter) {
+ return recipeEmitter(recipeEmitter.andThen(Collections::singletonList));
+ }
+
+ /**
+ * Changes how recipes are emitted by a particular recipe builder. Can emit multiple recipe per builder.
+ * <p>
+ * Recipes added via one of the overloads of addRecipe will NOT be affected by this function.
+ * <p>
+ * Unlike {@link #recipeEmitter(Function)}, this one does not clear the existing recipe being emitted, if any
+ */
+ public RecipeMapBuilder<B> combineRecipeEmitter(
+ Function<? super GT_RecipeBuilder, ? extends Iterable<? extends GT_Recipe>> recipeEmitter) {
+ backendPropertiesBuilder.combineRecipeEmitter(recipeEmitter);
+ return this;
+ }
+
+ /**
+ * Changes how recipes are emitted by a particular recipe builder. Effectively add a new recipe per recipe added.
+ * func must not return null.
+ * <p>
+ * Recipes added via one of the overloads of addRecipe will NOT be affected by this function.
+ * <p>
+ * Unlike {@link #recipeEmitter(Function)}, this one does not clear the existing recipe being emitted, if any
+ */
+ public RecipeMapBuilder<B> combineRecipeEmitterSingle(
+ Function<? super GT_RecipeBuilder, ? extends GT_Recipe> recipeEmitter) {
+ return combineRecipeEmitter(recipeEmitter.andThen(Collections::singletonList));
+ }
+
+ /**
+ * Runs a custom hook on all recipes added <b>via builder</b>. For more complicated behavior,
+ * use {@link #recipeEmitter}.
+ * <p>
+ * Recipes added via one of the overloads of addRecipe will NOT be affected by this function.
+ */
+ public RecipeMapBuilder<B> recipeTransformer(Function<? super GT_Recipe, ? extends GT_Recipe> recipeTransformer) {
+ backendPropertiesBuilder.recipeTransformer(recipeTransformer);
+ return this;
+ }
+
+ /**
+ * Runs a custom hook on all recipes added <b>via builder</b>. For more complicated behavior,
+ * use {@link #recipeEmitter}.
+ * <p>
+ * Recipes added via one of the overloads of addRecipe will NOT be affected by this function.
+ */
+ public RecipeMapBuilder<B> recipeTransformer(Consumer<GT_Recipe> recipeTransformer) {
+ return recipeTransformer(withIdentityReturn(recipeTransformer));
+ }
+
+ /**
+ * Runs a custom hook on all recipes added <b>via builder</b>. For more complicated behavior,
+ * use {@link #recipeEmitter}.
+ * <p>
+ * Recipes added via one of the overloads of addRecipe will NOT be affected by this function.
+ * <p>
+ * Unlike {@link #recipeTransformer(Function)}, this one will not replace the existing special handler.
+ * The supplied function will be given the output of existing handler when a recipe is added.
+ */
+ public RecipeMapBuilder<B> chainRecipeTransformer(
+ Function<? super GT_Recipe, ? extends GT_Recipe> recipeTransformer) {
+ backendPropertiesBuilder.chainRecipeTransformer(recipeTransformer);
+ return this;
+ }
+
+ /**
+ * Runs a custom hook on all recipes added <b>via builder</b>. For more complicated behavior,
+ * use {@link #recipeEmitter}.
+ * <p>
+ * Recipes added via one of the overloads of addRecipe will NOT be affected by this function.
+ * <p>
+ * Unlike {@link #recipeTransformer(Function)}, this one will not replace the existing special handler.
+ * The supplied function will be given the output of existing handler when a recipe is added.
+ */
+ public RecipeMapBuilder<B> chainRecipeTransformer(Consumer<GT_Recipe> recipeTransformer) {
+ return chainRecipeTransformer(withIdentityReturn(recipeTransformer));
+ }
+
+ public RecipeMapBuilder<B> recipeConfigFile(String category, Function<? super GT_Recipe, String> keyConvertor) {
+ if (StringUtils.isBlank(category)) throw new IllegalArgumentException();
+ backendPropertiesBuilder.recipeConfigFile(category, keyConvertor);
+ return this;
+ }
+
+ // endregion
+
+ // region frontend UI properties
+
+ /**
+ * Sets how many item/fluid inputs/outputs does this recipemap usually has at most.
+ * It does not actually restrict the number of items that can be used in recipes.
+ */
+ public RecipeMapBuilder<B> maxIO(int maxItemInputs, int maxItemOutputs, int maxFluidInputs, int maxFluidOutputs) {
+ uiPropertiesBuilder.maxItemInputs(maxItemInputs)
+ .maxItemOutputs(maxItemOutputs)
+ .maxFluidInputs(maxFluidInputs)
+ .maxFluidOutputs(maxFluidOutputs);
+ return this;
+ }
+
+ /**
+ * Sets function to get overlay for slots.
+ */
+ public RecipeMapBuilder<B> slotOverlays(BasicUIProperties.SlotOverlayGetter<IDrawable> slotOverlays) {
+ uiPropertiesBuilder.slotOverlays(slotOverlays);
+ return this;
+ }
+
+ /**
+ * Sets function to get overlay for slots of steam machines.
+ */
+ public RecipeMapBuilder<B> slotOverlaysSteam(BasicUIProperties.SlotOverlayGetter<SteamTexture> slotOverlaysSteam) {
+ uiPropertiesBuilder.slotOverlaysSteam(slotOverlaysSteam);
+ return this;
+ }
+
+ /**
+ * Sets texture and animation direction of the progressbar.
+ * <p>
+ * Unless specified, size should be (20, 36), consisting of two parts;
+ * First is (20, 18) size of "empty" image at the top, Second is (20, 18) size of "filled" image at the bottom.
+ * <p>
+ * By default, it's set to {@code GT_UITextures.PROGRESSBAR_ARROW, ProgressBar.Direction.RIGHT}.
+ */
+ public RecipeMapBuilder<B> progressBar(UITexture texture, ProgressBar.Direction direction) {
+ return progressBarWithFallback(GT_UITextures.fallbackableProgressbar(unlocalizedName, texture), direction);
+ }
+
+ /**
+ * Sets progressbar texture with right direction.
+ * <p>
+ * Unless specified, size should be (20, 36), consisting of two parts;
+ * First is (20, 18) size of "empty" image at the top, Second is (20, 18) size of "filled" image at the bottom.
+ */
+ public RecipeMapBuilder<B> progressBar(UITexture texture) {
+ return progressBar(texture, ProgressBar.Direction.RIGHT);
+ }
+
+ /**
+ * Some resource packs want to use custom progress bar textures even for plain arrow. This method allows them to
+ * add unique textures, yet other packs don't need to make textures for every recipemap.
+ */
+ private RecipeMapBuilder<B> progressBarWithFallback(FallbackableUITexture texture,
+ ProgressBar.Direction direction) {
+ uiPropertiesBuilder.progressBarTexture(texture)
+ .progressBarDirection(direction);
+ return this;
+ }
+
+ /**
+ * Sets progressbar texture for steam machines.
+ * <p>
+ * Unless specified, size should be (20, 36), consisting of two parts;
+ * First is (20, 18) size of "empty" image at the top, Second is (20, 18) size of "filled" image at the bottom.
+ */
+ public RecipeMapBuilder<B> progressBarSteam(SteamTexture texture) {
+ return progressBarSteamWithFallback(
+ new FallbackableSteamTexture(
+ SteamTexture.fullImage(GregTech.ID, "gui/progressbar/" + unlocalizedName + "_%s"),
+ texture));
+ }
+
+ private RecipeMapBuilder<B> progressBarSteamWithFallback(FallbackableSteamTexture texture) {
+ uiPropertiesBuilder.progressBarTextureSteam(texture);
+ return this;
+ }
+
+ /**
+ * Sets size of the progressbar. (20, 36) by default.
+ */
+ public RecipeMapBuilder<B> progressBarSize(int x, int y) {
+ uiPropertiesBuilder.progressBarSize(new Size(x, y));
+ return this;
+ }
+
+ /**
+ * Sets position of the progressbar. (78, 24) by default.
+ */
+ public RecipeMapBuilder<B> progressBarPos(int x, int y) {
+ uiPropertiesBuilder.progressBarPos(new Pos2d(x, y));
+ return this;
+ }
+
+ /**
+ * Stops adding progressbar to the UI.
+ */
+ public RecipeMapBuilder<B> dontUseProgressBar() {
+ uiPropertiesBuilder.useProgressBar(false);
+ return this;
+ }
+
+ /**
+ * Configures this recipemap to use special slot. This means special slot shows up on NEI and tooltip for
+ * special slot on basic machine GUI indicates it has actual usage.
+ */
+ public RecipeMapBuilder<B> useSpecialSlot() {
+ uiPropertiesBuilder.useSpecialSlot(true);
+ return this;
+ }
+
+ /**
+ * Adds GUI area where clicking shows up all the recipes available.
+ *
+ * @see codechicken.nei.recipe.TemplateRecipeHandler.RecipeTransferRect
+ */
+ public RecipeMapBuilder<B> neiTransferRect(int x, int y, int width, int height) {
+ uiPropertiesBuilder.addNEITransferRect(new Rectangle(x, y, width, height));
+ return this;
+ }
+
+ /**
+ * Sets ID used to open NEI recipe GUI when progressbar is clicked.
+ */
+ public RecipeMapBuilder<B> neiTransferRectId(String neiTransferRectId) {
+ uiPropertiesBuilder.neiTransferRectId(neiTransferRectId);
+ return this;
+ }
+
+ /**
+ * Adds additional textures shown on GUI.
+ */
+ public RecipeMapBuilder<B> addSpecialTexture(int x, int y, int width, int height, IDrawable texture) {
+ uiPropertiesBuilder.addSpecialTexture(new Size(width, height), new Pos2d(x, y), texture);
+ return this;
+ }
+
+ /**
+ * Adds additional textures shown on steam machine GUI.
+ */
+ public RecipeMapBuilder<B> addSpecialTextureSteam(int x, int y, int width, int height, SteamTexture texture) {
+ uiPropertiesBuilder.addSpecialTextureSteam(new Size(width, height), new Pos2d(x, y), texture);
+ return this;
+ }
+
+ /**
+ * Sets logo shown on GUI. GregTech logo by default.
+ */
+ public RecipeMapBuilder<B> logo(IDrawable logo) {
+ uiPropertiesBuilder.logo(logo);
+ return this;
+ }
+
+ /**
+ * Sets size of logo. (17, 17) by default.
+ */
+ public RecipeMapBuilder<B> logoSize(int width, int height) {
+ uiPropertiesBuilder.logoSize(new Size(width, height));
+ return this;
+ }
+
+ /**
+ * Sets position of logo. (152, 63) by default.
+ */
+ public RecipeMapBuilder<B> logoPos(int x, int y) {
+ uiPropertiesBuilder.logoPos(new Pos2d(x, y));
+ return this;
+ }
+
+ /**
+ * Sets amperage for the recipemap.
+ */
+ public RecipeMapBuilder<B> amperage(int amperage) {
+ uiPropertiesBuilder.amperage(amperage);
+ return this;
+ }
+
+ // endregion
+
+ // region frontend NEI properties
+
+ /**
+ * Stops adding dedicated NEI recipe page for this recipemap. This does not prevent adding transferrect
+ * for the machine GUI.
+ */
+ public RecipeMapBuilder<B> disableRegisterNEI() {
+ neiPropertiesBuilder.disableRegisterNEI();
+ return this;
+ }
+
+ /**
+ * Sets properties of NEI handler info this recipemap belongs to. You can specify icon shown on recipe tab,
+ * handler height, number of recipes per page, etc. Either use supplied template or return newly constructed one.
+ * <p>
+ * Invocation of the builder creator is delayed until the actual registration (FMLLoadCompleteEvent),
+ * so you can safely use itemstack that doesn't exist as of recipemap initialization.
+ * <p>
+ * If this method is not used, handler icon will be inferred from recipe catalysts associated with this recipemap.
+ * <p>
+ * Precisely, what's registered to NEI is {@link RecipeCategory}, not RecipeMap. However, handler info supplied
+ * by this method will be used for default category where most of the recipes belong to.
+ */
+ public RecipeMapBuilder<B> neiHandlerInfo(UnaryOperator<HandlerInfo.Builder> handlerInfoCreator) {
+ neiPropertiesBuilder.handlerInfoCreator(handlerInfoCreator);
+ return this;
+ }
+
+ /**
+ * Sets offset of background shown on NEI.
+ */
+ public RecipeMapBuilder<B> neiRecipeBackgroundSize(int width, int height) {
+ neiPropertiesBuilder.recipeBackgroundSize(new Size(width, height));
+ return this;
+ }
+
+ /**
+ * Sets size of background shown on NEI.
+ */
+ public RecipeMapBuilder<B> neiRecipeBackgroundOffset(int x, int y) {
+ neiPropertiesBuilder.recipeBackgroundOffset(new Pos2d(x, y));
+ return this;
+ }
+
+ /**
+ * Sets formatter for special description for the recipe, mainly {@link gregtech.api.util.GT_Recipe#mSpecialValue}.
+ */
+ public RecipeMapBuilder<B> neiSpecialInfoFormatter(INEISpecialInfoFormatter neiSpecialInfoFormatter) {
+ neiPropertiesBuilder.neiSpecialInfoFormatter(neiSpecialInfoFormatter);
+ return this;
+ }
+
+ /**
+ * Sets whether to show oredict equivalent item outputs on NEI.
+ */
+ public RecipeMapBuilder<B> unificateOutputNEI(boolean unificateOutputNEI) {
+ neiPropertiesBuilder.unificateOutput(unificateOutputNEI);
+ return this;
+ }
+
+ /**
+ * Sets NEI recipe handler to use a custom filter method {@link OverclockDescriber#canHandle} to limit the shown
+ * recipes when searching recipes with recipe catalyst. Without calling this method, the voltage of the recipe is
+ * the only factor to filter recipes by default.
+ * <p>
+ * This method on its own doesn't do anything. You need to bind custom {@link OverclockDescriber} object to machines
+ * that will be shown as recipe catalysts for this recipemap by implementing
+ * {@link gregtech.api.interfaces.tileentity.IOverclockDescriptionProvider}.
+ */
+ public RecipeMapBuilder<B> useCustomFilterForNEI() {
+ neiPropertiesBuilder.useCustomFilter();
+ return this;
+ }
+
+ /**
+ * Stops rendering the actual stack size of items on NEI.
+ */
+ public RecipeMapBuilder<B> disableRenderRealStackSizes() {
+ neiPropertiesBuilder.disableRenderRealStackSizes();
+ return this;
+ }
+
+ /**
+ * Sets custom comparator for NEI recipe sort.
+ */
+ public RecipeMapBuilder<B> neiRecipeComparator(Comparator<GT_Recipe> comparator) {
+ neiPropertiesBuilder.recipeComparator(comparator);
+ return this;
+ }
+
+ // endregion
+
+ /**
+ * Sets custom frontend logic. For custom backend, pass it to {@link #of(String, RecipeMapBackend.BackendCreator)}.
+ */
+ public RecipeMapBuilder<B> frontend(RecipeMapFrontend.FrontendCreator frontendCreator) {
+ this.frontendCreator = frontendCreator;
+ return this;
+ }
+
+ /**
+ * Builds new recipemap.
+ *
+ * @return Recipemap object with backend type parameter, which is {@code RecipeMapFrontend} unless specified.
+ */
+ public RecipeMap<B> build() {
+ return new RecipeMap<>(
+ unlocalizedName,
+ backendCreator.create(backendPropertiesBuilder),
+ frontendCreator.create(uiPropertiesBuilder, neiPropertiesBuilder));
+ }
+
+ private static <T> Function<? super T, ? extends T> withIdentityReturn(Consumer<T> func) {
+ return r -> {
+ func.accept(r);
+ return r;
+ };
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeMapFrontend.java b/src/main/java/gregtech/api/recipe/RecipeMapFrontend.java
new file mode 100644
index 0000000000..63daa00dc7
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeMapFrontend.java
@@ -0,0 +1,395 @@
+package gregtech.api.recipe;
+
+import static gregtech.api.util.GT_Utility.trans;
+import static net.minecraft.util.EnumChatFormatting.GRAY;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.EnumChatFormatting;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import com.gtnewhorizons.modularui.api.GlStateManager;
+import com.gtnewhorizons.modularui.api.ModularUITextures;
+import com.gtnewhorizons.modularui.api.drawable.IDrawable;
+import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable;
+import com.gtnewhorizons.modularui.api.math.Alignment;
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.math.Size;
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+import com.gtnewhorizons.modularui.common.widget.DrawableWidget;
+import com.gtnewhorizons.modularui.common.widget.ProgressBar;
+import com.gtnewhorizons.modularui.common.widget.SlotWidget;
+
+import codechicken.nei.PositionedStack;
+import gregtech.GT_Mod;
+import gregtech.api.enums.SteamVariant;
+import gregtech.api.gui.GT_GUIColorOverride;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.recipe.metadata.IRecipeMetadataStorage;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.common.gui.modularui.UIHelper;
+import gregtech.nei.GT_NEI_DefaultHandler;
+import gregtech.nei.RecipeDisplayInfo;
+
+/**
+ * Responsible for managing GUI tied to recipemap. It has two property objects, {@link NEIRecipeProperties} and
+ * {@link BasicUIProperties}. The former is only for NEI display, while the latter is for both NEI and basic machine.
+ * <p>
+ * In order to bind custom frontend to recipemap, use {@link RecipeMapBuilder#frontend}.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class RecipeMapFrontend {
+
+ /**
+ * Properties specific to this frontend, mainly for GUI widgets.
+ */
+ protected final BasicUIProperties uiProperties;
+ /**
+ * Properties specific to this frontend, only for NEI specific settings.
+ */
+ protected final NEIRecipeProperties neiProperties;
+
+ protected final GT_GUIColorOverride colorOverride = GT_GUIColorOverride
+ .get(GT_UITextures.BACKGROUND_NEI_SINGLE_RECIPE.location);
+
+ public RecipeMapFrontend(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder) {
+ this.uiProperties = uiPropertiesBuilder.itemInputPositionsGetter(this::getItemInputPositions)
+ .itemOutputPositionsGetter(this::getItemOutputPositions)
+ .specialItemPositionGetter(this::getSpecialItemPosition)
+ .fluidInputPositionsGetter(this::getFluidInputPositions)
+ .fluidOutputPositionsGetter(this::getFluidOutputPositions)
+ .build();
+ this.neiProperties = neiPropertiesBuilder.build();
+ }
+
+ /**
+ * @return Properties specific to this frontend, mainly for GUI widgets.
+ */
+ public BasicUIProperties getUIProperties() {
+ return uiProperties;
+ }
+
+ /**
+ * @return Properties specific to this frontend, only for NEI specific settings.
+ */
+ public NEIRecipeProperties getNEIProperties() {
+ return neiProperties;
+ }
+
+ /**
+ * Creates NEI recipe layout, except for actual items / fluids.
+ */
+ public ModularWindow.Builder createNEITemplate(IItemHandlerModifiable itemInputsInventory,
+ IItemHandlerModifiable itemOutputsInventory, IItemHandlerModifiable specialSlotInventory,
+ IItemHandlerModifiable fluidInputsInventory, IItemHandlerModifiable fluidOutputsInventory,
+ Supplier<Float> progressSupplier, Pos2d windowOffset) {
+ ModularWindow.Builder builder = ModularWindow.builder(neiProperties.recipeBackgroundSize)
+ .setBackground(GT_UITextures.BACKGROUND_NEI_SINGLE_RECIPE);
+
+ UIHelper.forEachSlots(
+ (i, backgrounds, pos) -> builder.widget(
+ SlotWidget.phantom(itemInputsInventory, i)
+ .setBackground(backgrounds)
+ .setPos(pos)
+ .setSize(18, 18)),
+ (i, backgrounds, pos) -> builder.widget(
+ SlotWidget.phantom(itemOutputsInventory, i)
+ .setBackground(backgrounds)
+ .setPos(pos)
+ .setSize(18, 18)),
+ (i, backgrounds, pos) -> {
+ if (uiProperties.useSpecialSlot) builder.widget(
+ SlotWidget.phantom(specialSlotInventory, 0)
+ .setBackground(backgrounds)
+ .setPos(pos)
+ .setSize(18, 18));
+ },
+ (i, backgrounds, pos) -> builder.widget(
+ SlotWidget.phantom(fluidInputsInventory, i)
+ .setBackground(backgrounds)
+ .setPos(pos)
+ .setSize(18, 18)),
+ (i, backgrounds, pos) -> builder.widget(
+ SlotWidget.phantom(fluidOutputsInventory, i)
+ .setBackground(backgrounds)
+ .setPos(pos)
+ .setSize(18, 18)),
+ ModularUITextures.ITEM_SLOT,
+ ModularUITextures.FLUID_SLOT,
+ uiProperties,
+ uiProperties.maxItemInputs,
+ uiProperties.maxItemOutputs,
+ uiProperties.maxFluidInputs,
+ uiProperties.maxFluidOutputs,
+ SteamVariant.NONE,
+ windowOffset);
+
+ if (uiProperties.useProgressBar) {
+ addProgressBar(builder, progressSupplier, windowOffset);
+ }
+ addGregTechLogo(builder, windowOffset);
+
+ for (Pair<IDrawable, Pair<Size, Pos2d>> specialTexture : uiProperties.specialTextures) {
+ builder.widget(
+ new DrawableWidget().setDrawable(specialTexture.getLeft())
+ .setSize(
+ specialTexture.getRight()
+ .getLeft())
+ .setPos(
+ specialTexture.getRight()
+ .getRight()
+ .add(windowOffset)));
+ }
+
+ return builder;
+ }
+
+ public void addProgressBar(ModularWindow.Builder builder, Supplier<Float> progressSupplier, Pos2d windowOffset) {
+ assert uiProperties.progressBarTexture != null;
+ builder.widget(
+ new ProgressBar().setTexture(uiProperties.progressBarTexture.get(), 20)
+ .setDirection(uiProperties.progressBarDirection)
+ .setProgress(progressSupplier)
+ .setSynced(false, false)
+ .setPos(uiProperties.progressBarPos.add(windowOffset))
+ .setSize(uiProperties.progressBarSize));
+ }
+
+ public void addGregTechLogo(ModularWindow.Builder builder, Pos2d windowOffset) {
+ builder.widget(
+ new DrawableWidget().setDrawable(uiProperties.logo)
+ .setSize(uiProperties.logoSize)
+ .setPos(uiProperties.logoPos.add(windowOffset)));
+ }
+
+ /**
+ * Overriding this method allows custom NEI stack placement
+ */
+ public List<Pos2d> getItemInputPositions(int itemInputCount) {
+ return UIHelper.getItemInputPositions(itemInputCount);
+ }
+
+ /**
+ * Overriding this method allows custom NEI stack placement
+ */
+ public List<Pos2d> getItemOutputPositions(int itemOutputCount) {
+ return UIHelper.getItemOutputPositions(itemOutputCount);
+ }
+
+ /**
+ * Overriding this method allows custom NEI stack placement
+ */
+ public Pos2d getSpecialItemPosition() {
+ return UIHelper.getSpecialItemPosition();
+ }
+
+ /**
+ * Overriding this method allows custom NEI stack placement
+ */
+ public List<Pos2d> getFluidInputPositions(int fluidInputCount) {
+ return UIHelper.getFluidInputPositions(fluidInputCount);
+ }
+
+ /**
+ * Overriding this method allows custom NEI stack placement
+ */
+ public List<Pos2d> getFluidOutputPositions(int fluidOutputCount) {
+ return UIHelper.getFluidOutputPositions(fluidOutputCount);
+ }
+
+ public void drawDescription(RecipeDisplayInfo recipeInfo) {
+ drawEnergyInfo(recipeInfo);
+ drawDurationInfo(recipeInfo);
+ drawSpecialInfo(recipeInfo);
+ drawMetadataInfo(recipeInfo);
+ drawRecipeOwnerInfo(recipeInfo);
+ }
+
+ protected void drawEnergyInfo(RecipeDisplayInfo recipeInfo) {
+ recipeInfo.overclockDescriber.drawEnergyInfo(recipeInfo);
+ }
+
+ protected void drawDurationInfo(RecipeDisplayInfo recipeInfo) {
+ recipeInfo.overclockDescriber.drawDurationInfo(recipeInfo);
+ }
+
+ protected void drawSpecialInfo(RecipeDisplayInfo recipeInfo) {
+ String[] recipeDesc = recipeInfo.recipe.getNeiDesc();
+ if (recipeDesc != null) {
+ for (String s : recipeDesc) {
+ recipeInfo.drawText(s);
+ }
+ } else {
+ recipeInfo.drawTextMultipleLines(neiProperties.neiSpecialInfoFormatter.format(recipeInfo));
+ }
+ }
+
+ protected void drawMetadataInfo(RecipeDisplayInfo recipeInfo) {
+ IRecipeMetadataStorage metadataStorage = recipeInfo.recipe.getMetadataStorage();
+ for (Map.Entry<RecipeMetadataKey<?>, Object> entry : metadataStorage.getEntries()) {
+ entry.getKey()
+ .drawInfo(recipeInfo, entry.getValue());
+ }
+ }
+
+ protected void drawRecipeOwnerInfo(RecipeDisplayInfo recipeInfo) {
+ GT_Recipe recipe = recipeInfo.recipe;
+ if (GT_Mod.gregtechproxy.mNEIRecipeOwner) {
+ if (recipe.owners.size() > 1) {
+ recipeInfo.drawText(
+ EnumChatFormatting.ITALIC + trans("273", "Original Recipe by: ")
+ + recipe.owners.get(0)
+ .getName());
+ for (int i = 1; i < recipe.owners.size(); i++) {
+ recipeInfo.drawText(
+ EnumChatFormatting.ITALIC + trans("274", "Modified by: ")
+ + recipe.owners.get(i)
+ .getName());
+ }
+ } else if (!recipe.owners.isEmpty()) {
+ recipeInfo.drawText(
+ EnumChatFormatting.ITALIC + trans("272", "Recipe by: ")
+ + recipe.owners.get(0)
+ .getName());
+ }
+ }
+ if (GT_Mod.gregtechproxy.mNEIRecipeOwnerStackTrace && recipe.stackTraces != null
+ && !recipe.stackTraces.isEmpty()) {
+ recipeInfo.drawText("stackTrace:");
+ // todo: good way to show all stacktraces
+ for (String stackTrace : recipe.stackTraces.get(0)) {
+ recipeInfo.drawText(stackTrace);
+ }
+ }
+ }
+
+ public List<String> handleNEIItemTooltip(ItemStack stack, List<String> currentTip,
+ GT_NEI_DefaultHandler.CachedDefaultRecipe neiCachedRecipe) {
+ for (PositionedStack pStack : neiCachedRecipe.mInputs) {
+ if (stack == pStack.item) {
+ if (pStack instanceof GT_NEI_DefaultHandler.FixedPositionedStack) {
+ currentTip = handleNEIItemInputTooltip(
+ currentTip,
+ (GT_NEI_DefaultHandler.FixedPositionedStack) pStack);
+ }
+ break;
+ }
+ }
+ for (PositionedStack pStack : neiCachedRecipe.mOutputs) {
+ if (stack == pStack.item) {
+ if (pStack instanceof GT_NEI_DefaultHandler.FixedPositionedStack) {
+ currentTip = handleNEIItemOutputTooltip(
+ currentTip,
+ (GT_NEI_DefaultHandler.FixedPositionedStack) pStack);
+ }
+ break;
+ }
+ }
+ return currentTip;
+ }
+
+ protected List<String> handleNEIItemInputTooltip(List<String> currentTip,
+ GT_NEI_DefaultHandler.FixedPositionedStack pStack) {
+ if (pStack.isNotConsumed()) {
+ currentTip.add(GRAY + trans("151", "Does not get consumed in the process"));
+ }
+ return currentTip;
+ }
+
+ protected List<String> handleNEIItemOutputTooltip(List<String> currentTip,
+ GT_NEI_DefaultHandler.FixedPositionedStack pStack) {
+ if (pStack.isChanceBased()) {
+ currentTip.add(GRAY + trans("150", "Chance: ") + pStack.getChanceText());
+ }
+ return currentTip;
+ }
+
+ public void drawNEIOverlays(GT_NEI_DefaultHandler.CachedDefaultRecipe neiCachedRecipe) {
+ for (PositionedStack stack : neiCachedRecipe.mInputs) {
+ if (stack instanceof GT_NEI_DefaultHandler.FixedPositionedStack) {
+ drawNEIOverlayForInput((GT_NEI_DefaultHandler.FixedPositionedStack) stack);
+ }
+ }
+ for (PositionedStack stack : neiCachedRecipe.mOutputs) {
+ if (stack instanceof GT_NEI_DefaultHandler.FixedPositionedStack) {
+ drawNEIOverlayForOutput((GT_NEI_DefaultHandler.FixedPositionedStack) stack);
+ }
+ }
+ }
+
+ protected void drawNEIOverlayForInput(GT_NEI_DefaultHandler.FixedPositionedStack stack) {
+ if (stack.isNotConsumed()) {
+ drawNEIOverlayText("NC", stack);
+ }
+ }
+
+ protected void drawNEIOverlayForOutput(GT_NEI_DefaultHandler.FixedPositionedStack stack) {
+ if (stack.isChanceBased()) {
+ drawNEIOverlayText(stack.getChanceText(), stack);
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ protected void drawNEIOverlayText(String text, PositionedStack stack, int color, float scale, boolean shadow,
+ Alignment alignment) {
+ FontRenderer fontRenderer = net.minecraft.client.Minecraft.getMinecraft().fontRenderer;
+ int width = fontRenderer.getStringWidth(text);
+ int x = (int) ((stack.relx + 8 + 8 * alignment.x) / scale) - (width / 2 * (alignment.x + 1));
+ int y = (int) ((stack.rely + 8 + 8 * alignment.y) / scale) - (fontRenderer.FONT_HEIGHT / 2 * (alignment.y + 1))
+ - (alignment.y - 1) / 2;
+
+ GlStateManager.pushMatrix();
+ GlStateManager.scale(scale, scale, 1);
+ fontRenderer.drawString(text, x, y, color, shadow);
+ GlStateManager.popMatrix();
+ }
+
+ protected void drawNEIOverlayText(String text, PositionedStack stack) {
+ drawNEIOverlayText(
+ text,
+ stack,
+ colorOverride.getTextColorOrDefault("nei_overlay_yellow", 0xFDD835),
+ 0.5f,
+ false,
+ Alignment.TopLeft);
+ }
+
+ public static List<Supplier<Float>> splitProgress(Supplier<Float> progress, int... progressbarLengthArray) {
+ float lengthSum = IntStream.of(progressbarLengthArray)
+ .sum();
+ List<Supplier<Float>> ret = new ArrayList<>();
+ float currentLengthSum = 0;
+ for (int progressbarLength : progressbarLengthArray) {
+ float speed = lengthSum / progressbarLength;
+ float offset = currentLengthSum / lengthSum;
+ ret.add(() -> {
+ float current = progress.get();
+ return (current - offset) * speed;
+ });
+ currentLengthSum += progressbarLength;
+ }
+ return ret;
+ }
+
+ @FunctionalInterface
+ public interface FrontendCreator {
+
+ /**
+ * @see RecipeMapFrontend#RecipeMapFrontend
+ */
+ RecipeMapFrontend create(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeMaps.java b/src/main/java/gregtech/api/recipe/RecipeMaps.java
new file mode 100644
index 0000000000..4494d1efba
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeMaps.java
@@ -0,0 +1,1194 @@
+package gregtech.api.recipe;
+
+import static gregtech.api.enums.Mods.GTNHIntergalactic;
+import static gregtech.api.enums.Mods.GTPlusPlus;
+import static gregtech.api.enums.Mods.NEICustomDiagrams;
+import static gregtech.api.enums.Mods.Railcraft;
+import static gregtech.api.util.GT_RecipeConstants.ADDITIVE_AMOUNT;
+import static gregtech.api.util.GT_RecipeConstants.FUEL_VALUE;
+import static gregtech.api.util.GT_RecipeMapUtil.FIRST_FLUIDSTACK_INPUT;
+import static gregtech.api.util.GT_RecipeMapUtil.FIRST_FLUIDSTACK_OUTPUT;
+import static gregtech.api.util.GT_RecipeMapUtil.FIRST_FLUID_OUTPUT;
+import static gregtech.api.util.GT_RecipeMapUtil.FIRST_ITEM_INPUT;
+import static gregtech.api.util.GT_RecipeMapUtil.FIRST_ITEM_OR_FLUID_INPUT;
+import static gregtech.api.util.GT_RecipeMapUtil.FIRST_ITEM_OR_FLUID_OUTPUT;
+import static gregtech.api.util.GT_RecipeMapUtil.FIRST_ITEM_OUTPUT;
+import static gregtech.api.util.GT_RecipeMapUtil.GT_RecipeTemplate;
+import static gregtech.api.util.GT_RecipeMapUtil.asTemplate;
+import static gregtech.api.util.GT_RecipeMapUtil.buildOrEmpty;
+import static gregtech.api.util.GT_Utility.clamp;
+import static gregtech.api.util.GT_Utility.copyAmount;
+import static gregtech.api.util.GT_Utility.getFluidForFilledItem;
+import static gregtech.api.util.GT_Utility.isArrayEmptyOrNull;
+import static gregtech.api.util.GT_Utility.isArrayOfLength;
+import static gregtech.api.util.GT_Utility.multiplyStack;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Optional;
+
+import net.minecraft.init.Blocks;
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.FluidStack;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import com.gtnewhorizons.modularui.api.drawable.UITexture;
+import com.gtnewhorizons.modularui.common.widget.ProgressBar;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.OrePrefixes;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.interfaces.IRecipeMap;
+import gregtech.api.objects.ItemData;
+import gregtech.api.recipe.maps.AssemblerBackend;
+import gregtech.api.recipe.maps.AssemblyLineFrontend;
+import gregtech.api.recipe.maps.DistillationTowerFrontend;
+import gregtech.api.recipe.maps.FluidCannerBackend;
+import gregtech.api.recipe.maps.FluidOnlyFrontend;
+import gregtech.api.recipe.maps.FormingPressBackend;
+import gregtech.api.recipe.maps.FuelBackend;
+import gregtech.api.recipe.maps.FurnaceBackend;
+import gregtech.api.recipe.maps.LargeBoilerFuelBackend;
+import gregtech.api.recipe.maps.LargeBoilerFuelFrontend;
+import gregtech.api.recipe.maps.LargeNEIFrontend;
+import gregtech.api.recipe.maps.MicrowaveBackend;
+import gregtech.api.recipe.maps.OilCrackerBackend;
+import gregtech.api.recipe.maps.PrinterBackend;
+import gregtech.api.recipe.maps.RecyclerBackend;
+import gregtech.api.recipe.maps.ReplicatorBackend;
+import gregtech.api.recipe.maps.SpaceProjectFrontend;
+import gregtech.api.recipe.maps.TranscendentPlasmaMixerFrontend;
+import gregtech.api.recipe.maps.UnpackagerBackend;
+import gregtech.api.recipe.metadata.PCBFactoryTierKey;
+import gregtech.api.util.GT_Config;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_OreDictUnificator;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeConstants;
+import gregtech.api.util.GT_RecipeMapUtil;
+import gregtech.api.util.GT_Utility;
+import gregtech.nei.formatter.FuelSpecialValueFormatter;
+import gregtech.nei.formatter.FusionSpecialValueFormatter;
+import gregtech.nei.formatter.HeatingCoilSpecialValueFormatter;
+import gregtech.nei.formatter.SimpleSpecialValueFormatter;
+import mods.railcraft.common.blocks.aesthetics.cube.EnumCube;
+import mods.railcraft.common.items.RailcraftToolItems;
+
+@SuppressWarnings("SimplifyOptionalCallChains")
+public final class RecipeMaps {
+
+ public static final RecipeMap<RecipeMapBackend> oreWasherRecipes = RecipeMapBuilder.of("gt.recipe.orewasher")
+ .maxIO(1, 3, 1, 0)
+ .minInputs(1, 1)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isFluid) {
+ return null;
+ }
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_DUST;
+ } else {
+ return GT_UITextures.OVERLAY_SLOT_CRUSHED_ORE;
+ }
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_BATH, ProgressBar.Direction.CIRCULAR_CW)
+ .recipeConfigFile("orewasher", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> thermalCentrifugeRecipes = RecipeMapBuilder
+ .of("gt.recipe.thermalcentrifuge")
+ .maxIO(1, 3, 0, 0)
+ .minInputs(1, 0)
+ .amperage(2)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isFluid) {
+ return null;
+ }
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_DUST;
+ } else {
+ return GT_UITextures.OVERLAY_SLOT_CRUSHED_ORE;
+ }
+ })
+ .recipeConfigFile("thermalcentrifuge", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> compressorRecipes = RecipeMapBuilder.of("gt.recipe.compressor")
+ .maxIO(1, 1, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_COMPRESSOR
+ : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_COMPRESS)
+ .slotOverlaysSteam(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_COMPRESSOR_STEAM
+ : null)
+ .progressBarSteam(GT_UITextures.PROGRESSBAR_COMPRESS_STEAM)
+ // Avoid steam machine being used as handler icon
+ .neiHandlerInfo(builder -> builder.setDisplayStack(ItemList.Machine_LV_Compressor.get(1)))
+ .recipeConfigFile("compressor", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> extractorRecipes = RecipeMapBuilder.of("gt.recipe.extractor")
+ .maxIO(1, 1, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_CENTRIFUGE
+ : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_EXTRACT)
+ .slotOverlaysSteam(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_CENTRIFUGE_STEAM
+ : null)
+ .progressBarSteam(GT_UITextures.PROGRESSBAR_EXTRACT_STEAM)
+ // Avoid steam machine being used as handler icon
+ .neiHandlerInfo(builder -> builder.setDisplayStack(ItemList.Machine_LV_Extractor.get(1)))
+ .recipeConfigFile("extractor", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecyclerBackend> recyclerRecipes = RecipeMapBuilder
+ .of("ic.recipe.recycler", RecyclerBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_RECYCLE : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_RECYCLE, ProgressBar.Direction.CIRCULAR_CW)
+ .neiTransferRectId("ic2.recycler")
+ .disableRegisterNEI()
+ .build();
+ public static final RecipeMap<FurnaceBackend> furnaceRecipes = RecipeMapBuilder
+ .of("mc.recipe.furnace", FurnaceBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .minInputs(1, 9)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_FURNACE : null)
+ .slotOverlaysSteam(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_FURNACE_STEAM
+ : null)
+ .progressBarSteam(GT_UITextures.PROGRESSBAR_ARROW_STEAM)
+ .neiTransferRectId("smelting")
+ .disableRegisterNEI()
+ .build();
+ public static final RecipeMap<MicrowaveBackend> microwaveRecipes = RecipeMapBuilder
+ .of("gt.recipe.microwave", MicrowaveBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_FURNACE : null)
+ .neiTransferRectId("smelting")
+ .disableRegisterNEI()
+ .build();
+ public static final RecipeMap<RecipeMapBackend> scannerFakeRecipes = RecipeMapBuilder.of("gt.recipe.scanner")
+ .maxIO(1, 1, 1, 0)
+ .minInputs(1, 0)
+ .useSpecialSlot()
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isSpecial) {
+ return GT_UITextures.OVERLAY_SLOT_DATA_ORB;
+ }
+ if (!isFluid && !isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_MICROSCOPE;
+ }
+ return null;
+ })
+ .build();
+ public static final RecipeMap<RecipeMapBackend> rockBreakerFakeRecipes = RecipeMapBuilder
+ .of("gt.recipe.rockbreaker")
+ .maxIO(2, 1, 0, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_CRUSHED_ORE;
+ } else {
+ return GT_UITextures.OVERLAY_SLOT_DUST;
+ }
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_MACERATE)
+ .build();
+ public static final RecipeMap<ReplicatorBackend> replicatorRecipes = RecipeMapBuilder
+ .of("gt.recipe.replicator", ReplicatorBackend::new)
+ .maxIO(0, 1, 1, 1)
+ .minInputs(0, 1)
+ .useSpecialSlot()
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isSpecial) {
+ return GT_UITextures.OVERLAY_SLOT_DATA_ORB;
+ }
+ if (isFluid && !isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_UUM;
+ }
+ return null;
+ })
+ .build();
+ /**
+ * Use {@link GT_RecipeConstants#AssemblyLine} for recipe addition.
+ */
+ public static final RecipeMap<RecipeMapBackend> assemblylineVisualRecipes = RecipeMapBuilder
+ .of("gt.recipe.fakeAssemblylineProcess")
+ .maxIO(16, 1, 4, 0)
+ .minInputs(1, 0)
+ .useSpecialSlot()
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> isSpecial ? GT_UITextures.OVERLAY_SLOT_DATA_ORB : null)
+ .disableOptimize()
+ .neiTransferRect(88, 8, 18, 72)
+ .neiTransferRect(124, 8, 18, 72)
+ .neiTransferRect(142, 26, 18, 18)
+ .frontend(AssemblyLineFrontend::new)
+ .build();
+ /**
+ * Usually, but not always, you should use {@link GT_RecipeConstants#UniversalArcFurnace} instead.
+ */
+ public static final RecipeMap<RecipeMapBackend> plasmaArcFurnaceRecipes = RecipeMapBuilder
+ .of("gt.recipe.plasmaarcfurnace")
+ .maxIO(1, 9, 1, 1)
+ .minInputs(1, 1)
+ .recipeConfigFile("arcfurnace", FIRST_ITEM_INPUT)
+ .build();
+ /**
+ * Usually, but not always, you should use {@link GT_RecipeConstants#UniversalArcFurnace} instead.
+ */
+ public static final RecipeMap<RecipeMapBackend> arcFurnaceRecipes = RecipeMapBuilder.of("gt.recipe.arcfurnace")
+ .maxIO(1, 9, 1, 0)
+ .minInputs(1, 1)
+ .amperage(3)
+ .recipeConfigFile("arcfurnace", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<PrinterBackend> printerRecipes = RecipeMapBuilder
+ .of("gt.recipe.printer", PrinterBackend::new)
+ .maxIO(1, 1, 1, 0)
+ .minInputs(1, 1)
+ .useSpecialSlot()
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isSpecial) {
+ return GT_UITextures.OVERLAY_SLOT_DATA_STICK;
+ }
+ if (isFluid) {
+ return null;
+ }
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_PAGE_PRINTED;
+ }
+ return GT_UITextures.OVERLAY_SLOT_PAGE_BLANK;
+ })
+ .recipeConfigFile("printer", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> sifterRecipes = RecipeMapBuilder.of("gt.recipe.sifter")
+ .maxIO(1, 9, 1, 1)
+ .progressBar(GT_UITextures.PROGRESSBAR_SIFT, ProgressBar.Direction.DOWN)
+ .recipeConfigFile("sifter", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<FormingPressBackend> formingPressRecipes = RecipeMapBuilder
+ .of("gt.recipe.press", FormingPressBackend::new)
+ .maxIO(6, 1, 0, 0)
+ .minInputs(2, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_PRESS_3;
+ }
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_PRESS_1;
+ }
+ return GT_UITextures.OVERLAY_SLOT_PRESS_2;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_COMPRESS)
+ .recipeConfigFile("press", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> laserEngraverRecipes = RecipeMapBuilder
+ .of("gt.recipe.laserengraver")
+ .maxIO(4, 4, 2, 2)
+ .slotOverlays(
+ (index, isFluid, isOutput,
+ isSpecial) -> !isFluid && !isOutput && index != 0 ? GT_UITextures.OVERLAY_SLOT_LENS : null)
+ .recipeConfigFile("laserengraving", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> mixerRecipes = RecipeMapBuilder.of("gt.recipe.mixer")
+ .maxIO(9, 4, 1, 1)
+ .minInputs(1, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> !isFluid ? GT_UITextures.OVERLAY_SLOT_DUST : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_MIXER, ProgressBar.Direction.CIRCULAR_CW)
+ .recipeConfigFile("mixer", FIRST_ITEM_OR_FLUID_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> autoclaveRecipes = RecipeMapBuilder.of("gt.recipe.autoclave")
+ .maxIO(2, 4, 1, 1)
+ .minInputs(1, 1)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isFluid) {
+ return null;
+ }
+ if (isOutput) {
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_GEM;
+ }
+ return GT_UITextures.OVERLAY_SLOT_DUST;
+ }
+ return GT_UITextures.OVERLAY_SLOT_DUST;
+ })
+
+ .recipeConfigFile("autoclave", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> electroMagneticSeparatorRecipes = RecipeMapBuilder
+ .of("gt.recipe.electromagneticseparator")
+ .maxIO(1, 3, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> isOutput ? GT_UITextures.OVERLAY_SLOT_DUST
+ : GT_UITextures.OVERLAY_SLOT_CRUSHED_ORE)
+ .progressBar(GT_UITextures.PROGRESSBAR_MAGNET)
+ .recipeConfigFile("electromagneticseparator", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> polarizerRecipes = RecipeMapBuilder.of("gt.recipe.polarizer")
+ .maxIO(1, 1, 0, 0)
+ .minInputs(1, 0)
+ .progressBar(GT_UITextures.PROGRESSBAR_MAGNET)
+ .recipeConfigFile("polarizer", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> maceratorRecipes = RecipeMapBuilder.of("gt.recipe.macerator")
+ .maxIO(1, 4, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> isOutput ? GT_UITextures.OVERLAY_SLOT_DUST
+ : GT_UITextures.OVERLAY_SLOT_CRUSHED_ORE)
+ .slotOverlaysSteam(
+ (index, isFluid, isOutput, isSpecial) -> isOutput ? GT_UITextures.OVERLAY_SLOT_DUST_STEAM
+ : GT_UITextures.OVERLAY_SLOT_CRUSHED_ORE_STEAM)
+ .progressBar(GT_UITextures.PROGRESSBAR_MACERATE)
+ .progressBarSteam(GT_UITextures.PROGRESSBAR_MACERATE_STEAM)
+ // Avoid steam machine being used as handler icon
+ .neiHandlerInfo(builder -> builder.setDisplayStack(ItemList.Machine_LV_Macerator.get(1)))
+ .recipeConfigFile("pulveriser", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> chemicalBathRecipes = RecipeMapBuilder.of("gt.recipe.chemicalbath")
+ .maxIO(1, 3, 1, 1)
+ .minInputs(1, 1)
+ .progressBar(GT_UITextures.PROGRESSBAR_BATH, ProgressBar.Direction.CIRCULAR_CW)
+ .recipeConfigFile("chemicalbath", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<FluidCannerBackend> fluidCannerRecipes = RecipeMapBuilder
+ .of("gt.recipe.fluidcanner", FluidCannerBackend::new)
+ .maxIO(1, 1, 1, 1)
+ .minInputs(1, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> !isFluid ? GT_UITextures.OVERLAY_SLOT_CANISTER : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_CANNER)
+ .recipeConfigFile("canning", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> brewingRecipes = RecipeMapBuilder.of("gt.recipe.brewer")
+ .maxIO(1, 0, 1, 1)
+ .minInputs(1, 1)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_CAULDRON : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_ARROW_MULTIPLE)
+ .recipeConfigFile("brewing", FIRST_FLUIDSTACK_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> fluidHeaterRecipes = RecipeMapBuilder.of("gt.recipe.fluidheater")
+ .maxIO(1, 0, 1, 1)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (!isFluid) {
+ return null;
+ }
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_HEATER_2;
+ }
+ return GT_UITextures.OVERLAY_SLOT_HEATER_1;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_ARROW_MULTIPLE)
+ .recipeConfigFile("fluidheater", FIRST_FLUIDSTACK_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> distilleryRecipes = RecipeMapBuilder.of("gt.recipe.distillery")
+ .maxIO(1, 1, 1, 1)
+ .minInputs(1, 1)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (!isFluid) {
+ return null;
+ }
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_BEAKER_2;
+ }
+ return GT_UITextures.OVERLAY_SLOT_BEAKER_1;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_ARROW_MULTIPLE)
+ .recipeTransformer(r -> {
+ int aInput = r.mFluidInputs[0].amount, aOutput = r.mFluidOutputs[0].amount, aDuration = r.mDuration;
+
+ // reduce the batch size if fluid amount is exceeding
+ int tScale = (Math.max(aInput, aOutput) + 999) / 1000;
+ if (tScale <= 0) tScale = 1;
+ if (tScale > 1) {
+ // trying to find whether there is a better factor
+ for (int i = tScale; i <= 5; i++) {
+ if (aInput % i == 0 && aDuration % i == 0) {
+ tScale = i;
+ break;
+ }
+ }
+ for (int i = tScale; i <= 5; i++) {
+ if (aInput % i == 0 && aDuration % i == 0 && aOutput % i == 0) {
+ tScale = i;
+ break;
+ }
+ }
+ aInput = (aInput + tScale - 1) / tScale;
+ aOutput = aOutput / tScale;
+ if (!isArrayEmptyOrNull(r.mOutputs)) {
+ ItemData tData = GT_OreDictUnificator.getItemData(r.mOutputs[0]);
+ if (tData != null && (tData.mPrefix == OrePrefixes.dust
+ || OrePrefixes.dust.mFamiliarPrefixes.contains(tData.mPrefix))) {
+ r.mOutputs[0] = GT_OreDictUnificator.getDust(
+ tData.mMaterial.mMaterial,
+ tData.mMaterial.mAmount * r.mOutputs[0].stackSize / tScale);
+ } else {
+ if (r.mOutputs[0].stackSize / tScale == 0) r.mOutputs[0] = GT_Values.NI;
+ else r.mOutputs[0] = copyAmount(r.mOutputs[0].stackSize / tScale, r.mOutputs[0]);
+ }
+ }
+ aDuration = (aDuration + tScale - 1) / tScale;
+ r.mFluidInputs[0] = copyAmount(aInput, r.mFluidInputs[0]);
+ r.mFluidOutputs[0] = copyAmount(aOutput, r.mFluidOutputs[0]);
+ r.mDuration = aDuration;
+ }
+ })
+ .recipeConfigFile("distillery", FIRST_FLUIDSTACK_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> fermentingRecipes = RecipeMapBuilder.of("gt.recipe.fermenter")
+ .maxIO(0, 0, 1, 1)
+ .minInputs(0, 1)
+ .progressBar(GT_UITextures.PROGRESSBAR_ARROW_MULTIPLE)
+ .recipeConfigFile("fermenting", FIRST_FLUIDSTACK_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> fluidSolidifierRecipes = RecipeMapBuilder
+ .of("gt.recipe.fluidsolidifier")
+ .maxIO(1, 1, 1, 0)
+ .minInputs(1, 1)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_MOLD : null)
+ .recipeTransformer(r -> {
+ if (ArrayUtils.isNotEmpty(r.mFluidInputs)) {
+ if (Materials.PhasedGold.getMolten(1)
+ .isFluidEqual(r.mFluidInputs[0]))
+ r.mFluidInputs = new FluidStack[] { Materials.VibrantAlloy.getMolten(r.mFluidInputs[0].amount) };
+ else if (Materials.PhasedIron.getMolten(1)
+ .isFluidEqual(r.mFluidInputs[0]))
+ r.mFluidInputs = new FluidStack[] { Materials.PulsatingIron.getMolten(r.mFluidInputs[0].amount) };
+ }
+ })
+ .recipeConfigFile("fluidsolidifier", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> fluidExtractionRecipes = RecipeMapBuilder
+ .of("gt.recipe.fluidextractor")
+ .maxIO(1, 1, 0, 1)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_CENTRIFUGE
+ : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_EXTRACT)
+ .recipeTransformer(r -> {
+ if (ArrayUtils.isNotEmpty(r.mFluidOutputs)) {
+ if (Materials.PhasedGold.getMolten(1)
+ .isFluidEqual(r.mFluidOutputs[0]))
+ r.mFluidOutputs = new FluidStack[] { Materials.VibrantAlloy.getMolten(r.mFluidOutputs[0].amount) };
+ else if (Materials.PhasedIron.getMolten(1)
+ .isFluidEqual(r.mFluidOutputs[0]))
+ r.mFluidOutputs = new FluidStack[] { Materials.PulsatingIron.getMolten(r.mFluidOutputs[0].amount) };
+ }
+ })
+ .recipeConfigFile("fluidextractor", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> packagerRecipes = RecipeMapBuilder.of("gt.recipe.packager")
+ .maxIO(2, 1, 0, 0)
+ .minInputs(2, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_BOXED;
+ }
+ if (index != 0) {
+ return GT_UITextures.OVERLAY_SLOT_BOX;
+ }
+ return null;
+ })
+ .recipeConfigFile("boxing", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<UnpackagerBackend> unpackagerRecipes = RecipeMapBuilder
+ .of("gt.recipe.unpackager", UnpackagerBackend::new)
+ .maxIO(1, 2, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> !isOutput ? GT_UITextures.OVERLAY_SLOT_BOXED : null)
+ .recipeConfigFile("unboxing", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> fusionRecipes = RecipeMapBuilder.of("gt.recipe.fusionreactor")
+ .maxIO(0, 0, 2, 1)
+ .minInputs(0, 2)
+ .disableOptimize()
+ .useCustomFilterForNEI()
+ .neiSpecialInfoFormatter(FusionSpecialValueFormatter.INSTANCE)
+ .neiRecipeComparator(
+ Comparator
+ .<GT_Recipe, Integer>comparing(
+ recipe -> FusionSpecialValueFormatter.getFusionTier(recipe.mSpecialValue, recipe.mEUt))
+ .thenComparing(GT_Recipe::compareTo))
+ .frontend(FluidOnlyFrontend::new)
+ .recipeConfigFile("fusion", FIRST_FLUID_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> centrifugeRecipes = RecipeMapBuilder.of("gt.recipe.centrifuge")
+ .maxIO(2, 6, 1, 1)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isOutput) {
+ return null;
+ }
+ if (isFluid) {
+ return GT_UITextures.OVERLAY_SLOT_CENTRIFUGE_FLUID;
+ } else {
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_CENTRIFUGE;
+ }
+ return GT_UITextures.OVERLAY_SLOT_CANISTER;
+ }
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_EXTRACT)
+ .recipeConfigFile("centrifuge", FIRST_ITEM_OR_FLUID_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> electrolyzerRecipes = RecipeMapBuilder.of("gt.recipe.electrolyzer")
+ .maxIO(2, 6, 1, 1)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isOutput) {
+ return null;
+ }
+ if (isFluid) {
+ return GT_UITextures.OVERLAY_SLOT_CHARGER_FLUID;
+ } else {
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_CHARGER;
+ }
+ return GT_UITextures.OVERLAY_SLOT_CANISTER;
+ }
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_EXTRACT)
+ .recipeConfigFile("electrolyzer", FIRST_ITEM_OR_FLUID_INPUT)
+ .build();
+ /**
+ * Use {@link GT_RecipeConstants#COIL_HEAT} as heat level.
+ */
+ public static final RecipeMap<RecipeMapBackend> blastFurnaceRecipes = RecipeMapBuilder.of("gt.recipe.blastfurnace")
+ .maxIO(6, 6, 1, 1)
+ .minInputs(1, 0)
+ .neiSpecialInfoFormatter(HeatingCoilSpecialValueFormatter.INSTANCE)
+ .recipeConfigFile("blastfurnace", FIRST_ITEM_INPUT)
+ .build();
+ /**
+ * Use {@link GT_RecipeConstants#COIL_HEAT} as heat level.
+ */
+ public static final RecipeMap<RecipeMapBackend> plasmaForgeRecipes = RecipeMapBuilder.of("gt.recipe.plasmaforge")
+ .maxIO(9, 9, 9, 9)
+ .disableOptimize()
+ .neiSpecialInfoFormatter(HeatingCoilSpecialValueFormatter.INSTANCE)
+ .neiHandlerInfo(
+ builder -> builder.setDisplayStack(ItemList.Machine_Multi_PlasmaForge.get(1))
+ .setMaxRecipesPerPage(1))
+ .frontend(LargeNEIFrontend::new)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> transcendentPlasmaMixerRecipes = RecipeMapBuilder
+ .of("gt.recipe.transcendentplasmamixerrecipes")
+ .maxIO(1, 0, 20, 1)
+ .progressBarPos(86, 44)
+ .logoPos(87, 99)
+ .neiRecipeBackgroundSize(170, 118)
+ .neiHandlerInfo(
+ builder -> builder.setDisplayStack(ItemList.Machine_Multi_TranscendentPlasmaMixer.get(1))
+ .setMaxRecipesPerPage(1))
+ .frontend(TranscendentPlasmaMixerFrontend::new)
+ .disableOptimize()
+ .build();
+ public static final RecipeMap<RecipeMapBackend> spaceProjectFakeRecipes = RecipeMapBuilder
+ .of("gt.recipe.fakespaceprojects")
+ .maxIO(12, 0, 4, 0)
+ .neiSpecialInfoFormatter(new SimpleSpecialValueFormatter("GT5U.nei.stages"))
+ .neiRecipeBackgroundOffset(3, 23)
+ .logo(UITexture.fullImage(GTNHIntergalactic.ID, "gui/picture/space_elevator_logo.png"))
+ .logoSize(18, 18)
+ .logoPos(152, 83)
+ .neiTransferRect(70, 28, 18, 72)
+ .neiTransferRect(106, 28, 18, 72)
+ .frontend(SpaceProjectFrontend::new)
+ .disableRenderRealStackSizes()
+ .disableOptimize()
+ .build();
+ /**
+ * Uses {@link GT_RecipeConstants#ADDITIVE_AMOUNT} for coal/charcoal amount.
+ */
+ public static final RecipeMap<RecipeMapBackend> primitiveBlastRecipes = RecipeMapBuilder
+ .of("gt.recipe.primitiveblastfurnace")
+ .maxIO(3, 3, 0, 0)
+ .minInputs(1, 0)
+ .recipeEmitter(builder -> {
+ Optional<GT_Recipe> rr = builder.eut(0)
+ .validateInputCount(1, 2)
+ .validateOutputCount(1, 2)
+ .validateNoInputFluid()
+ .validateNoOutputFluid()
+ .noOptimize()
+ .build();
+ if (!rr.isPresent()) return Collections.emptyList();
+ ItemStack aInput1 = builder.getItemInputBasic(0);
+ ItemStack aInput2 = builder.getItemInputBasic(1);
+ ItemStack aOutput1 = builder.getItemOutput(0);
+ ItemStack aOutput2 = builder.getItemOutput(1);
+ if ((aInput1 == null && aInput2 == null) || (aOutput1 == null && aOutput2 == null))
+ return Collections.emptyList();
+ int aCoalAmount = builder.getMetadataOrDefault(ADDITIVE_AMOUNT, 0);
+ if (aCoalAmount <= 0) return Collections.emptyList();
+ GT_RecipeTemplate coll = asTemplate(rr.get());
+ for (Materials coal : new Materials[] { Materials.Coal, Materials.Charcoal }) {
+ coll.derive()
+ .setInputs(aInput1, aInput2, coal.getGems(aCoalAmount))
+ .setOutputs(aOutput1, aOutput2, Materials.DarkAsh.getDustTiny(aCoalAmount));
+ coll.derive()
+ .setInputs(aInput1, aInput2, coal.getDust(aCoalAmount))
+ .setOutputs(aOutput1, aOutput2, Materials.DarkAsh.getDustTiny(aCoalAmount));
+ }
+ int aDuration = builder.getDuration();
+ if (Railcraft.isModLoaded()) {
+ coll.derive()
+ .setInputs(aInput1, aInput2, RailcraftToolItems.getCoalCoke(aCoalAmount / 2))
+ .setOutputs(aOutput1, aOutput2, Materials.Ash.getDustTiny(aCoalAmount / 2))
+ .setDuration(aDuration * 2 / 3);
+ }
+ if (GTPlusPlus.isModLoaded()) {
+ ItemStack cactusCoke = GT_ModHandler.getModItem(GTPlusPlus.ID, "itemCactusCoke", aCoalAmount * 2L);
+ ItemStack sugarCoke = GT_ModHandler.getModItem(GTPlusPlus.ID, "itemSugarCoke", aCoalAmount * 2L);
+ coll.derive()
+ .setInputs(aInput1, aInput2, cactusCoke)
+ .setOutputs(aOutput1, aOutput2, Materials.Ash.getDustTiny(aCoalAmount * 2))
+ .setDuration(aDuration * 2 / 3);
+ coll.derive()
+ .setInputs(aInput1, aInput2, sugarCoke)
+ .setOutputs(aOutput1, aOutput2, Materials.Ash.getDustTiny(aCoalAmount * 2))
+ .setDuration(aDuration * 2 / 3);
+ }
+ if ((aInput1 == null || aInput1.stackSize <= 6) && (aInput2 == null || aInput2.stackSize <= 6)
+ && (aOutput1 == null || aOutput1.stackSize <= 6)
+ && (aOutput2 == null || aOutput2.stackSize <= 6)) {
+ // we don't use GT_Utility.mul() here. It does not have the truncating we need here.
+ aInput1 = multiplyStack(10, aInput1);
+ aInput2 = multiplyStack(10, aInput2);
+ aOutput1 = multiplyStack(10, aOutput1);
+ aOutput2 = multiplyStack(10, aOutput2);
+ for (Materials coal : new Materials[] { Materials.Coal, Materials.Charcoal }) {
+ coll.derive()
+ .setInputs(aInput1, aInput2, coal.getBlocks(aCoalAmount))
+ .setOutputs(aOutput1, aOutput2, Materials.DarkAsh.getDust(aCoalAmount))
+ .setDuration(aDuration * 10);
+ coll.derive()
+ .setInputs(aInput1, aInput2, coal.getBlocks(aCoalAmount))
+ .setOutputs(aOutput1, aOutput2, Materials.DarkAsh.getDust(aCoalAmount))
+ .setDuration(aDuration * 10);
+ }
+ if (Railcraft.isModLoaded()) {
+ coll.derive()
+ .setInputs(aInput1, aInput2, EnumCube.COKE_BLOCK.getItem(aCoalAmount / 2))
+ .setOutputs(aOutput1, aOutput2, Materials.Ash.getDust(aCoalAmount / 2))
+ .setDuration(aDuration * 20 / 3);
+ }
+ }
+ return coll.getAll();
+ })
+ .recipeConfigFile("primitiveblastfurnace", FIRST_ITEM_INPUT)
+ .build();
+ /**
+ * Uses {@link GT_RecipeConstants#ADDITIVE_AMOUNT} for TNT/ITNT/... amount. Value is truncated to [0, 64]
+ */
+ public static final RecipeMap<RecipeMapBackend> implosionRecipes = RecipeMapBuilder
+ .of("gt.recipe.implosioncompressor")
+ .maxIO(2, 2, 0, 0)
+ .minInputs(2, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (!isFluid && !isOutput) {
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_IMPLOSION;
+ }
+ return GT_UITextures.OVERLAY_SLOT_EXPLOSIVE;
+ }
+ return null;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_COMPRESS)
+ .disableOptimize()
+ .recipeEmitter(b -> {
+ switch (b.getItemInputsBasic().length) {
+ case 0:
+ return Collections.emptyList();
+ case 1:
+ break;
+ default:
+ return b.build()
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList());
+ }
+ Optional<GT_Recipe> t = b.noOptimize()
+ .duration(20)
+ .eut(30)
+ .validateInputCount(1, 1)
+ .validateOutputCount(1, 2)
+ .build();
+ if (!t.isPresent()) return Collections.emptyList();
+ ItemStack input = b.getItemInputBasic(0);
+ GT_RecipeTemplate coll = asTemplate(t.get());
+ int tExplosives = Math.min(b.getMetadataOrDefault(ADDITIVE_AMOUNT, 0), 64);
+ int tGunpowder = tExplosives << 1; // Worst
+ int tDynamite = Math.max(1, tExplosives >> 1); // good
+ @SuppressWarnings("UnnecessaryLocalVariable")
+ int tTNT = tExplosives; // Slightly better
+ int tITNT = Math.max(1, tExplosives >> 2); // the best
+ if (tGunpowder < 65) coll.derive()
+ .setInputs(input, ItemList.Block_Powderbarrel.get(tGunpowder));
+ if (tDynamite < 17) coll.derive()
+ .setInputs(input, GT_ModHandler.getIC2Item("dynamite", tDynamite, null));
+ coll.derive()
+ .setInputs(input, new ItemStack(Blocks.tnt, tTNT));
+ coll.derive()
+ .setInputs(input, GT_ModHandler.getIC2Item("industrialTnt", tITNT, null));
+ return coll.getAll();
+ })
+ .recipeConfigFile("implosion", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> vacuumFreezerRecipes = RecipeMapBuilder
+ .of("gt.recipe.vacuumfreezer")
+ .maxIO(1, 1, 2, 1)
+ .recipeEmitter(b -> {
+ b.noOptimize();
+ FluidStack in, out;
+ if (isArrayOfLength(b.getItemInputsBasic(), 1) && isArrayOfLength(b.getItemOutputs(), 1)
+ && isArrayEmptyOrNull(b.getFluidInputs())
+ && isArrayEmptyOrNull(b.getFluidOutputs())
+ && (in = getFluidForFilledItem(b.getItemInputBasic(0), true)) != null
+ && (out = getFluidForFilledItem(b.getItemOutput(0), true)) != null) {
+ Collection<GT_Recipe> ret = new ArrayList<>();
+ b.build()
+ .ifPresent(ret::add);
+ b.itemInputs()
+ .itemOutputs()
+ .fluidInputs(in)
+ .fluidOutputs(out)
+ .build()
+ .ifPresent(ret::add);
+ return ret;
+ }
+ return buildOrEmpty(b);
+ })
+ .recipeConfigFile("vacuumfreezer", FIRST_ITEM_INPUT)
+ .build();
+ /**
+ * Using {@code .addTo(chemicalReactorRecipes)} will cause the recipe to be added to single block recipe map ONLY!
+ * Use {@link GT_RecipeConstants#UniversalChemical} to add to both.
+ */
+ public static final RecipeMap<RecipeMapBackend> chemicalReactorRecipes = RecipeMapBuilder
+ .of("gt.recipe.chemicalreactor")
+ .maxIO(2, 2, 1, 1)
+ .minInputs(1, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isFluid) {
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_VIAL_2;
+ }
+ return GT_UITextures.OVERLAY_SLOT_MOLECULAR_3;
+ } else {
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_VIAL_1;
+ }
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_MOLECULAR_1;
+ }
+ return GT_UITextures.OVERLAY_SLOT_MOLECULAR_2;
+ }
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_ARROW_MULTIPLE)
+ .disableOptimize()
+ .recipeConfigFile("chemicalreactor", FIRST_ITEM_OR_FLUID_OUTPUT)
+ .build();
+ /**
+ * Using {@code .addTo(multiblockChemicalReactorRecipes)} will cause the recipe to be added to
+ * multiblock recipe map ONLY! Use {@link GT_RecipeConstants#UniversalChemical} to add to both.
+ */
+ public static final RecipeMap<RecipeMapBackend> multiblockChemicalReactorRecipes = RecipeMapBuilder
+ .of("gt.recipe.largechemicalreactor")
+ .maxIO(6, 6, 6, 6)
+ .progressBar(GT_UITextures.PROGRESSBAR_ARROW_MULTIPLE)
+ .disableOptimize()
+ .frontend(LargeNEIFrontend::new)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> distillationTowerRecipes = RecipeMapBuilder
+ .of("gt.recipe.distillationtower")
+ .maxIO(2, 1, 1, 11)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (!isOutput) {
+ return null;
+ }
+ if (isFluid) {
+ return GT_UITextures.OVERLAY_SLOTS_NUMBER[index + 1];
+ } else {
+ return GT_UITextures.OVERLAY_SLOTS_NUMBER[0];
+ }
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_ARROW_MULTIPLE)
+ .logoPos(80, 62)
+ .frontend(DistillationTowerFrontend::new)
+ .disableOptimize()
+ .recipeConfigFile("distillation", FIRST_FLUIDSTACK_INPUT)
+ .build();
+ public static final RecipeMap<OilCrackerBackend> crackingRecipes = RecipeMapBuilder
+ .of("gt.recipe.craker", OilCrackerBackend::new)
+ .maxIO(1, 1, 2, 1)
+ .minInputs(1, 2)
+ .progressBar(GT_UITextures.PROGRESSBAR_ARROW_MULTIPLE)
+ .recipeConfigFile("cracking", FIRST_FLUIDSTACK_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> pyrolyseRecipes = RecipeMapBuilder.of("gt.recipe.pyro")
+ .maxIO(2, 1, 1, 1)
+ .minInputs(1, 0)
+ .disableOptimize()
+ .recipeConfigFile("pyrolyse", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> wiremillRecipes = RecipeMapBuilder.of("gt.recipe.wiremill")
+ .maxIO(2, 1, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_WIREMILL : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_WIREMILL)
+ .recipeConfigFile("wiremill", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> benderRecipes = RecipeMapBuilder.of("gt.recipe.metalbender")
+ .maxIO(2, 1, 0, 0)
+ .minInputs(2, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_BENDER : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_BENDING)
+ .recipeConfigFile("bender", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> alloySmelterRecipes = RecipeMapBuilder.of("gt.recipe.alloysmelter")
+ .maxIO(2, 1, 0, 0)
+ .minInputs(2, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_FURNACE : null)
+ .slotOverlaysSteam((index, isFluid, isOutput, isSpecial) -> GT_UITextures.OVERLAY_SLOT_FURNACE_STEAM)
+ .progressBarSteam(GT_UITextures.PROGRESSBAR_ARROW_STEAM)
+ .recipeEmitter(b -> {
+ if (Materials.Graphite.contains(b.getItemInputBasic(0))) return Collections.emptyList();
+ if (GT_Utility.isArrayOfLength(b.getItemInputsBasic(), 1)) {
+ ItemStack aInput1 = b.getItemInputBasic(0);
+ if (((OrePrefixes.ingot.contains(aInput1)) || (OrePrefixes.dust.contains(aInput1))
+ || (OrePrefixes.gem.contains(aInput1)))) return Collections.emptyList();
+ }
+ return buildOrEmpty(
+ b.validateNoInputFluid()
+ .validateNoOutputFluid()
+ .validateInputCount(1, 2)
+ .validateOutputCount(1, 1));
+ })
+ // Avoid steam machine being used as handler icon
+ .neiHandlerInfo(builder -> builder.setDisplayStack(ItemList.Machine_LV_AlloySmelter.get(1)))
+ .recipeConfigFile(
+ "alloysmelting",
+ r -> GT_Config.getStackConfigName(GT_Utility.isArrayOfLength(r.mInputs, 1) ? r.mInputs[0] : r.mOutputs[0]))
+ .build();
+ public static final RecipeMap<AssemblerBackend> assemblerRecipes = RecipeMapBuilder
+ .of("gt.recipe.assembler", AssemblerBackend::new)
+ .maxIO(9, 1, 1, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_CIRCUIT : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_ASSEMBLE)
+ .disableOptimize()
+ .recipeConfigFile("assembling", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> circuitAssemblerRecipes = RecipeMapBuilder
+ .of("gt.recipe.circuitassembler")
+ .maxIO(6, 1, 1, 0)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_CIRCUIT : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_CIRCUIT_ASSEMBLER)
+ .unificateOutputNEI(!NEICustomDiagrams.isModLoaded())
+ .recipeConfigFile("circuitassembler", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> cannerRecipes = RecipeMapBuilder.of("gt.recipe.canner")
+ .maxIO(2, 2, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isOutput) {
+ return null;
+ }
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_CANNER;
+ }
+ return GT_UITextures.OVERLAY_SLOT_CANISTER;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_CANNER)
+ .recipeConfigFile("canning", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> latheRecipes = RecipeMapBuilder.of("gt.recipe.lathe")
+ .maxIO(1, 2, 0, 0)
+ .minInputs(1, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isOutput) {
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_ROD_2;
+ }
+ return GT_UITextures.OVERLAY_SLOT_DUST;
+ }
+ return GT_UITextures.OVERLAY_SLOT_ROD_1;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_LATHE)
+ .addSpecialTexture(98, 24, 5, 18, GT_UITextures.PROGRESSBAR_LATHE_BASE)
+ .recipeConfigFile("lathe", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> cutterRecipes = RecipeMapBuilder.of("gt.recipe.cuttingsaw")
+ .maxIO(2, 4, 1, 0)
+ .minInputs(1, 1)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isFluid) {
+ return null;
+ }
+ if (isOutput) {
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_CUTTER_SLICED;
+ }
+ return GT_UITextures.OVERLAY_SLOT_DUST;
+ }
+ return GT_UITextures.OVERLAY_SLOT_BOX;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_CUT)
+ .recipeEmitter(b -> {
+ b.validateInputCount(1, 2)
+ .validateOutputCount(1, 4)
+ .validateNoOutputFluid();
+ if ((b.getFluidInputs() != null && b.getFluidInputs().length > 0) || !b.isValid())
+ return buildOrEmpty(b.validateInputFluidCount(1, 1));
+ int aDuration = b.getDuration(), aEUt = b.getEUt();
+ Collection<GT_Recipe> ret = new ArrayList<>();
+ b.copy()
+ .fluidInputs(Materials.Water.getFluid(clamp(aDuration * aEUt / 320, 4, 1000)))
+ .duration(aDuration * 2)
+ .build()
+ .ifPresent(ret::add);
+ b.copy()
+ .fluidInputs(GT_ModHandler.getDistilledWater(clamp(aDuration * aEUt / 426, 3, 750)))
+ .duration(aDuration * 2)
+ .build()
+ .ifPresent(ret::add);
+ b.fluidInputs(Materials.Lubricant.getFluid(clamp(aDuration * aEUt / 1280, 1, 250)))
+ .duration(aDuration)
+ .build()
+ .ifPresent(ret::add);
+ return ret;
+ })
+ .recipeConfigFile("cutting", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> slicerRecipes = RecipeMapBuilder.of("gt.recipe.slicer")
+ .maxIO(2, 1, 0, 0)
+ .minInputs(2, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_SLICER_SLICED;
+ }
+ if (index == 0) {
+ return GT_UITextures.OVERLAY_SLOT_SQUARE;
+ }
+ return GT_UITextures.OVERLAY_SLOT_SLICE_SHAPE;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_SLICE)
+ .recipeConfigFile("slicer", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> extruderRecipes = RecipeMapBuilder.of("gt.recipe.extruder")
+ .maxIO(2, 1, 0, 0)
+ .minInputs(2, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput,
+ isSpecial) -> !isFluid && !isOutput && index != 0 ? GT_UITextures.OVERLAY_SLOT_EXTRUDER_SHAPE : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_EXTRUDE)
+ .recipeConfigFile("extruder", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> hammerRecipes = RecipeMapBuilder.of("gt.recipe.hammer")
+ .maxIO(2, 2, 2, 2)
+ .minInputs(1, 0)
+ .slotOverlays(
+ (index, isFluid, isOutput, isSpecial) -> !isFluid && !isOutput ? GT_UITextures.OVERLAY_SLOT_HAMMER : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_HAMMER, ProgressBar.Direction.DOWN)
+ .addSpecialTexture(78, 42, 20, 6, GT_UITextures.PROGRESSBAR_HAMMER_BASE)
+ .slotOverlaysSteam(
+ (index, isFluid, isOutput, isSpecial) -> !isOutput ? GT_UITextures.OVERLAY_SLOT_HAMMER_STEAM : null)
+ .progressBarSteam(GT_UITextures.PROGRESSBAR_HAMMER_STEAM)
+ .addSpecialTextureSteam(78, 42, 20, 6, GT_UITextures.PROGRESSBAR_HAMMER_BASE_STEAM)
+ // Avoid steam machine being used as handler icon
+ .neiHandlerInfo(builder -> builder.setDisplayStack(ItemList.Machine_LV_Hammer.get(1)))
+ .recipeConfigFile("forgehammer", FIRST_ITEM_OUTPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> amplifierRecipes = RecipeMapBuilder.of("gt.recipe.uuamplifier")
+ .maxIO(1, 0, 0, 1)
+ .minInputs(1, 0)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (isFluid) {
+ return GT_UITextures.OVERLAY_SLOT_UUA;
+ }
+ if (!isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_CENTRIFUGE;
+ }
+ return null;
+ })
+ .progressBar(GT_UITextures.PROGRESSBAR_EXTRACT)
+ .recipeConfigFile("amplifier", FIRST_ITEM_INPUT)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> massFabFakeRecipes = RecipeMapBuilder.of("gt.recipe.massfab")
+ .maxIO(1, 0, 1, 1)
+ .minInputs(1, 0)
+ .amperage(8)
+ .slotOverlays((index, isFluid, isOutput, isSpecial) -> {
+ if (!isFluid) {
+ return null;
+ }
+ if (isOutput) {
+ return GT_UITextures.OVERLAY_SLOT_UUM;
+ }
+ return GT_UITextures.OVERLAY_SLOT_UUA;
+ })
+ .build();
+ public static final RecipeMap<FuelBackend> dieselFuels = RecipeMapBuilder
+ .of("gt.recipe.dieselgeneratorfuel", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> extremeDieselFuels = RecipeMapBuilder
+ .of("gt.recipe.extremedieselgeneratorfuel", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> gasTurbineFuels = RecipeMapBuilder
+ .of("gt.recipe.gasturbinefuel", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> hotFuels = RecipeMapBuilder
+ .of("gt.recipe.thermalgeneratorfuel", FuelBackend::new)
+ .maxIO(1, 4, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> denseLiquidFuels = RecipeMapBuilder
+ .of("gt.recipe.semifluidboilerfuels", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .disableRegisterNEI()
+ .build();
+ public static final RecipeMap<FuelBackend> plasmaFuels = RecipeMapBuilder
+ .of("gt.recipe.plasmageneratorfuels", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> magicFuels = RecipeMapBuilder
+ .of("gt.recipe.magicfuels", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> smallNaquadahReactorFuels = RecipeMapBuilder
+ .of("gt.recipe.smallnaquadahreactor", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> largeNaquadahReactorFuels = RecipeMapBuilder
+ .of("gt.recipe.largenaquadahreactor", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> hugeNaquadahReactorFuels = RecipeMapBuilder
+ .of("gt.recipe.fluidnaquadahreactor", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> extremeNaquadahReactorFuels = RecipeMapBuilder
+ .of("gt.recipe.hugenaquadahreactor", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> ultraHugeNaquadahReactorFuels = RecipeMapBuilder
+ .of("gt.recipe.extrahugenaquadahreactor", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<FuelBackend> fluidNaquadahReactorFuels = RecipeMapBuilder
+ .of("gt.recipe.fluidfuelnaquadahreactor", FuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .neiSpecialInfoFormatter(FuelSpecialValueFormatter.INSTANCE)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> electrolyzerNonCellRecipes = RecipeMapBuilder
+ .of("gt.recipe.largeelectrolyzer")
+ .maxIO(1, 6, 1, 6)
+ .disableRegisterNEI()
+ .recipeEmitter(GT_RecipeMapUtil::buildRecipeForMultiblock)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> centrifugeNonCellRecipes = RecipeMapBuilder
+ .of("gt.recipe.largecentrifuge")
+ .maxIO(2, 6, 1, 6)
+ .disableOptimize()
+ .disableRegisterNEI()
+ .recipeEmitter(GT_RecipeMapUtil::buildRecipeForMultiblock)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> mixerNonCellRecipes = RecipeMapBuilder.of("gt.recipe.largemixer")
+ .maxIO(9, 4, 6, 4)
+ .disableOptimize()
+ .disableRegisterNEI()
+ .recipeEmitter(GT_RecipeMapUtil::buildRecipeForMultiblockNoCircuit)
+ .build();
+ public static final RecipeMap<LargeBoilerFuelBackend> largeBoilerFakeFuels = RecipeMapBuilder
+ .of("gt.recipe.largeboilerfakefuels", LargeBoilerFuelBackend::new)
+ .maxIO(1, 1, 0, 0)
+ .minInputs(1, 0)
+ .disableOptimize()
+ .frontend(LargeBoilerFuelFrontend::new)
+ .build();
+ public static final RecipeMap<RecipeMapBackend> nanoForgeRecipes = RecipeMapBuilder.of("gt.recipe.nanoforge")
+ .maxIO(6, 2, 3, 0)
+ .minInputs(2, 1)
+ .slotOverlays(
+ (index, isFluid, isOutput,
+ isSpecial) -> !isFluid && !isOutput && index == 0 ? GT_UITextures.OVERLAY_SLOT_LENS : null)
+ .progressBar(GT_UITextures.PROGRESSBAR_ASSEMBLE)
+ .disableOptimize()
+ .neiSpecialInfoFormatter(new SimpleSpecialValueFormatter("GT5U.nei.tier"))
+ .build();
+ public static final RecipeMap<RecipeMapBackend> pcbFactoryRecipes = RecipeMapBuilder.of("gt.recipe.pcbfactory")
+ .maxIO(6, 9, 3, 0)
+ .minInputs(3, 1)
+ .progressBar(GT_UITextures.PROGRESSBAR_ASSEMBLE)
+ .disableOptimize()
+ .neiRecipeComparator(
+ Comparator
+ .<GT_Recipe, Integer>comparing(recipe -> recipe.getMetadataOrDefault(PCBFactoryTierKey.INSTANCE, 1))
+ .thenComparing(GT_Recipe::compareTo))
+ .build();
+ public static final RecipeMap<RecipeMapBackend> ic2NuclearFakeRecipes = RecipeMapBuilder.of("gt.recipe.ic2nuke")
+ .maxIO(1, 1, 0, 0)
+ .minInputs(1, 0)
+ .disableOptimize()
+ .logo(GT_UITextures.PICTURE_RADIATION_WARNING)
+ .logoPos(152, 41)
+ .neiRecipeBackgroundSize(170, 60)
+ .neiHandlerInfo(builder -> builder.setDisplayStack(GT_ModHandler.getIC2Item("nuclearReactor", 1, null)))
+ .build();
+
+ static {
+ RecipeMaps.centrifugeRecipes.addDownstream(RecipeMaps.centrifugeNonCellRecipes.deepCopyInput());
+ RecipeMaps.mixerRecipes.addDownstream(RecipeMaps.mixerNonCellRecipes.deepCopyInput());
+ RecipeMaps.electrolyzerRecipes.addDownstream(RecipeMaps.electrolyzerNonCellRecipes.deepCopyInput());
+ RecipeMaps.dieselFuels.addDownstream(
+ IRecipeMap.newRecipeMap(
+ b -> b.build()
+ .map(
+ r -> RecipeMaps.largeBoilerFakeFuels.getBackend()
+ .addDieselRecipe(r))
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList())));
+ RecipeMaps.dieselFuels.addDownstream(IRecipeMap.newRecipeMap(b -> {
+ if (b.getMetadataOrDefault(FUEL_VALUE, 0) < 1500) return Collections.emptyList();
+ return b.addTo(RecipeMaps.extremeDieselFuels);
+ }));
+ RecipeMaps.denseLiquidFuels.addDownstream(
+ IRecipeMap.newRecipeMap(
+ b -> b.build()
+ .map(
+ r -> RecipeMaps.largeBoilerFakeFuels.getBackend()
+ .addDenseLiquidRecipe(r))
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList())));
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/RecipeMetadataKey.java b/src/main/java/gregtech/api/recipe/RecipeMetadataKey.java
new file mode 100644
index 0000000000..2156421835
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/RecipeMetadataKey.java
@@ -0,0 +1,84 @@
+package gregtech.api.recipe;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.jetbrains.annotations.Contract;
+
+import gregtech.api.recipe.metadata.IRecipeMetadataStorage;
+import gregtech.api.util.FieldsAreNonnullByDefault;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.nei.RecipeDisplayInfo;
+
+/**
+ * Unique key for the {@link IRecipeMetadataStorage}. It's also responsible for drawing metadata info on NEI.
+ * <p>
+ * You can use {@link gregtech.api.recipe.metadata.SimpleRecipeMetadataKey} if your metadata does not need NEI handling.
+ *
+ * @param <T> Type of the metadata to use.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@FieldsAreNonnullByDefault
+public abstract class RecipeMetadataKey<T> {
+
+ private static final Set<RecipeMetadataKey<?>> allIdentifiers = new HashSet<>();
+ private final Class<T> clazz;
+ private final String identifier;
+
+ protected RecipeMetadataKey(Class<T> clazz, String identifier) {
+ this.clazz = clazz;
+ this.identifier = identifier;
+ if (allIdentifiers.contains(this)) {
+ throw new IllegalArgumentException(
+ "Cannot register metadata key with exact same properties: " + identifier + "@" + clazz);
+ }
+ allIdentifiers.add(this);
+ }
+
+ /**
+ * Draws info about the metadata.
+ *
+ * @param recipeInfo Object to use for drawing text.
+ * @param value Metadata stored in the recipe. Can be safely {@link #cast}ed to the desired type.
+ */
+ public abstract void drawInfo(RecipeDisplayInfo recipeInfo, @Nullable Object value);
+
+ @Nullable
+ public T cast(@Nullable Object o) {
+ return clazz.cast(o);
+ }
+
+ @Contract("_, !null -> !null")
+ @Nullable
+ public T cast(@Nullable Object o, @Nullable T defaultValue) {
+ T val = cast(o);
+ return val != null ? val : defaultValue;
+ }
+
+ @Override
+ public String toString() {
+ return "RecipeMetadataKey{" + "clazz=" + clazz.getName() + ", identifier=" + identifier + '\'' + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RecipeMetadataKey<?> that = (RecipeMetadataKey<?>) o;
+
+ if (!clazz.equals(that.clazz)) return false;
+ return identifier.equals(that.identifier);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = clazz.hashCode();
+ result = 31 * result + identifier.hashCode();
+ return result;
+ }
+}
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());
+ }
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/AssemblerBackend.java b/src/main/java/gregtech/api/recipe/maps/AssemblerBackend.java
new file mode 100644
index 0000000000..cfa25e9fe2
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/AssemblerBackend.java
@@ -0,0 +1,35 @@
+package gregtech.api.recipe.maps;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.enums.ItemList;
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class AssemblerBackend extends RecipeMapBackend {
+
+ public AssemblerBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected GT_Recipe modifyFoundRecipe(GT_Recipe recipe, ItemStack[] items, FluidStack[] fluids,
+ @Nullable ItemStack specialSlot) {
+ for (ItemStack item : items) {
+ if (ItemList.Paper_Printed_Pages.isStackEqual(item, false, true)) {
+ recipe = recipe.copy();
+ recipe.mCanBeBuffered = false;
+ recipe.mOutputs[0].setTagCompound(item.getTagCompound());
+ }
+ }
+ return recipe;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/AssemblyLineFrontend.java b/src/main/java/gregtech/api/recipe/maps/AssemblyLineFrontend.java
new file mode 100644
index 0000000000..3d56c96b82
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/AssemblyLineFrontend.java
@@ -0,0 +1,76 @@
+package gregtech.api.recipe.maps;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+import com.gtnewhorizons.modularui.common.widget.ProgressBar;
+
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.recipe.BasicUIPropertiesBuilder;
+import gregtech.api.recipe.NEIRecipePropertiesBuilder;
+import gregtech.api.recipe.RecipeMapFrontend;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.common.gui.modularui.UIHelper;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class AssemblyLineFrontend extends RecipeMapFrontend {
+
+ public AssemblyLineFrontend(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder) {
+ super(uiPropertiesBuilder, neiPropertiesBuilder);
+ }
+
+ @Override
+ public List<Pos2d> getItemInputPositions(int itemInputCount) {
+ return UIHelper.getGridPositions(itemInputCount, 16, 8, 4);
+ }
+
+ @Override
+ public List<Pos2d> getItemOutputPositions(int itemOutputCount) {
+ return Collections.singletonList(new Pos2d(142, 8));
+ }
+
+ @Override
+ public Pos2d getSpecialItemPosition() {
+ return new Pos2d(142, 44);
+ }
+
+ @Override
+ public List<Pos2d> getFluidInputPositions(int fluidInputCount) {
+ return UIHelper.getGridPositions(fluidInputCount, 106, 8, 1);
+ }
+
+ @Override
+ public void addProgressBar(ModularWindow.Builder builder, Supplier<Float> progressSupplier, Pos2d windowOffset) {
+ int bar1Width = 17;
+ int bar2Width = 18;
+ List<Supplier<Float>> splitProgress = splitProgress(progressSupplier, bar1Width, bar2Width);
+ builder.widget(
+ new ProgressBar().setTexture(GT_UITextures.PROGRESSBAR_ASSEMBLY_LINE_1, bar1Width)
+ .setDirection(ProgressBar.Direction.RIGHT)
+ .setProgress(splitProgress.get(0))
+ .setSynced(false, false)
+ .setPos(new Pos2d(88, 8).add(windowOffset))
+ .setSize(bar1Width, 72));
+ builder.widget(
+ new ProgressBar().setTexture(GT_UITextures.PROGRESSBAR_ASSEMBLY_LINE_2, bar2Width)
+ .setDirection(ProgressBar.Direction.RIGHT)
+ .setProgress(splitProgress.get(1))
+ .setSynced(false, false)
+ .setPos(new Pos2d(124, 8).add(windowOffset))
+ .setSize(bar2Width, 72));
+ builder.widget(
+ new ProgressBar().setTexture(GT_UITextures.PROGRESSBAR_ASSEMBLY_LINE_3, 18)
+ .setDirection(ProgressBar.Direction.UP)
+ .setProgress(progressSupplier)
+ .setSynced(false, false)
+ .setPos(new Pos2d(146, 26).add(windowOffset))
+ .setSize(10, 18));
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/DistillationTowerFrontend.java b/src/main/java/gregtech/api/recipe/maps/DistillationTowerFrontend.java
new file mode 100644
index 0000000000..b061d10d55
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/DistillationTowerFrontend.java
@@ -0,0 +1,38 @@
+package gregtech.api.recipe.maps;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+
+import gregtech.api.recipe.BasicUIPropertiesBuilder;
+import gregtech.api.recipe.NEIRecipePropertiesBuilder;
+import gregtech.api.recipe.RecipeMapFrontend;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class DistillationTowerFrontend extends RecipeMapFrontend {
+
+ public DistillationTowerFrontend(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder) {
+ super(uiPropertiesBuilder, neiPropertiesBuilder);
+ }
+
+ @Override
+ public List<Pos2d> getItemOutputPositions(int itemOutputCount) {
+ return Collections.singletonList(new Pos2d(106, 62));
+ }
+
+ @Override
+ public List<Pos2d> getFluidOutputPositions(int fluidOutputCount) {
+ List<Pos2d> results = new ArrayList<>();
+ for (int i = 1; i < fluidOutputCount + 1; i++) {
+ results.add(new Pos2d(106 + (i % 3) * 18, 62 - (i / 3) * 18));
+ }
+ return results;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/FluidCannerBackend.java b/src/main/java/gregtech/api/recipe/maps/FluidCannerBackend.java
new file mode 100644
index 0000000000..e5681f59aa
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/FluidCannerBackend.java
@@ -0,0 +1,73 @@
+package gregtech.api.recipe.maps;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+import net.minecraftforge.fluids.IFluidContainerItem;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class FluidCannerBackend extends RecipeMapBackend {
+
+ public FluidCannerBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected GT_Recipe findFallback(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot) {
+ if (items.length == 0 || items[0] == null) {
+ return null;
+ }
+
+ if (fluids.length > 0 && fluids[0] != null) {
+ ItemStack filledItem = GT_Utility.fillFluidContainer(fluids[0], items[0], false, true);
+ FluidStack fluidToTake = GT_Utility.getFluidForFilledItem(filledItem, true);
+ if (fluidToTake != null) {
+ return GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(1, items[0]))
+ .itemOutputs(filledItem)
+ .fluidInputs(fluidToTake)
+ .duration(Math.max(fluidToTake.amount / 64, 16))
+ .eut(1)
+ .noOptimize()
+ .noBuffer()
+ .build()
+ .orElse(null);
+ }
+ }
+ FluidStack drainedFluid = GT_Utility.getFluidForFilledItem(items[0], true);
+ if (drainedFluid != null) {
+ return GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(1, items[0]))
+ .itemOutputs(GT_Utility.getContainerItem(items[0], true))
+ .fluidOutputs(drainedFluid)
+ .duration(Math.max(drainedFluid.amount / 64, 16))
+ .eut(1)
+ .noBuffer()
+ .build()
+ .orElse(null);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean containsInput(ItemStack item) {
+ return super.containsInput(item) || (item.getItem() instanceof IFluidContainerItem
+ && ((IFluidContainerItem) item.getItem()).getCapacity(item) > 0);
+ }
+
+ @Override
+ public boolean containsInput(Fluid fluid) {
+ return true;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/FluidOnlyFrontend.java b/src/main/java/gregtech/api/recipe/maps/FluidOnlyFrontend.java
new file mode 100644
index 0000000000..8b37f6388c
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/FluidOnlyFrontend.java
@@ -0,0 +1,33 @@
+package gregtech.api.recipe.maps;
+
+import java.util.List;
+
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+
+import gregtech.api.recipe.BasicUIPropertiesBuilder;
+import gregtech.api.recipe.NEIRecipePropertiesBuilder;
+import gregtech.api.recipe.RecipeMapFrontend;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.common.gui.modularui.UIHelper;
+
+/**
+ * Display fluids where normally items are placed on NEI.
+ */
+@MethodsReturnNonnullByDefault
+public class FluidOnlyFrontend extends RecipeMapFrontend {
+
+ public FluidOnlyFrontend(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder) {
+ super(uiPropertiesBuilder, neiPropertiesBuilder);
+ }
+
+ @Override
+ public List<Pos2d> getFluidInputPositions(int fluidInputCount) {
+ return UIHelper.getItemInputPositions(fluidInputCount);
+ }
+
+ @Override
+ public List<Pos2d> getFluidOutputPositions(int fluidOutputCount) {
+ return UIHelper.getItemOutputPositions(fluidOutputCount);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/FormingPressBackend.java b/src/main/java/gregtech/api/recipe/maps/FormingPressBackend.java
new file mode 100644
index 0000000000..ce3ea3e89c
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/FormingPressBackend.java
@@ -0,0 +1,92 @@
+package gregtech.api.recipe.maps;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.enums.ItemList;
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Special Class for Forming Press handling.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class FormingPressBackend extends RecipeMapBackend {
+
+ public FormingPressBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected GT_Recipe modifyFoundRecipe(GT_Recipe recipe, ItemStack[] items, FluidStack[] fluids,
+ @Nullable ItemStack specialSlot) {
+ for (ItemStack mold : items) {
+ if (ItemList.Shape_Mold_Credit.isStackEqual(mold, false, true)) {
+ NBTTagCompound nbt = mold.getTagCompound();
+ if (nbt == null) nbt = new NBTTagCompound();
+ if (!nbt.hasKey("credit_security_id")) nbt.setLong("credit_security_id", System.nanoTime());
+ mold.setTagCompound(nbt);
+
+ recipe = recipe.copy();
+ recipe.mCanBeBuffered = false;
+ recipe.mOutputs[0].setTagCompound(nbt);
+ return recipe;
+ }
+ }
+ return recipe;
+ }
+
+ @Override
+ protected GT_Recipe findFallback(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot) {
+ if (items.length < 2) {
+ return null;
+ }
+ return findRenamingRecipe(items);
+ }
+
+ @Nullable
+ private GT_Recipe findRenamingRecipe(ItemStack[] inputs) {
+ ItemStack mold = findNameMoldIndex(inputs);
+ if (mold == null) return null;
+ ItemStack input = findStackToRename(inputs, mold);
+ if (input == null) return null;
+ ItemStack output = GT_Utility.copyAmount(1, input);
+ if (output == null) return null;
+ output.setStackDisplayName(mold.getDisplayName());
+ return GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(0, mold), GT_Utility.copyAmount(1, input))
+ .itemOutputs(output)
+ .duration(128)
+ .eut(8)
+ .noBuffer()
+ .nbtSensitive()
+ .build()
+ .orElse(null);
+ }
+
+ @Nullable
+ private ItemStack findNameMoldIndex(ItemStack[] inputs) {
+ for (ItemStack stack : inputs) {
+ if (ItemList.Shape_Mold_Name.isStackEqual(stack, false, true)) return stack;
+ }
+ return null;
+ }
+
+ @Nullable
+ private ItemStack findStackToRename(ItemStack[] inputs, ItemStack mold) {
+ for (ItemStack stack : inputs) {
+ if (stack == mold || stack == null) continue;
+ return stack;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/FuelBackend.java b/src/main/java/gregtech/api/recipe/maps/FuelBackend.java
new file mode 100644
index 0000000000..49c989e174
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/FuelBackend.java
@@ -0,0 +1,75 @@
+package gregtech.api.recipe.maps;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class FuelBackend extends RecipeMapBackend {
+
+ private final Map<String, GT_Recipe> recipesByFluidInput = new HashMap<>();
+
+ public FuelBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder.disableOptimize());
+ }
+
+ @Override
+ protected Collection<GT_Recipe> doAdd(GT_RecipeBuilder builder) {
+ if (builder.getDuration() == -1) {
+ builder.duration(0);
+ }
+ if (builder.getEUt() == -1) {
+ builder.eut(0);
+ }
+ return super.doAdd(builder);
+ }
+
+ @Override
+ public GT_Recipe compileRecipe(GT_Recipe recipe) {
+ super.compileRecipe(recipe);
+ if (recipe.mInputs != null && GT_Utility.getNonnullElementCount(recipe.mInputs) == 1
+ && (recipe.mFluidInputs == null || GT_Utility.getNonnullElementCount(recipe.mFluidInputs) == 0)) {
+ FluidStack fluidStack = GT_Utility.getFluidForFilledItem(recipe.mInputs[0], true);
+ if (fluidStack != null) {
+ fluidStack.amount = 0;
+ recipesByFluidInput.put(
+ fluidStack.getFluid()
+ .getName(),
+ recipe);
+ }
+ } else if ((recipe.mInputs == null || GT_Utility.getNonnullElementCount(recipe.mInputs) == 0)
+ && recipe.mFluidInputs != null
+ && GT_Utility.getNonnullElementCount(recipe.mFluidInputs) >= 1
+ && recipe.mFluidInputs[0] != null) {
+ recipesByFluidInput.put(
+ recipe.mFluidInputs[0].getFluid()
+ .getName(),
+ recipe);
+ }
+ return recipe;
+ }
+
+ @Nullable
+ public GT_Recipe findFuel(FluidStack fluidStack) {
+ return findFuel(fluidStack.getFluid());
+ }
+
+ @Nullable
+ public GT_Recipe findFuel(Fluid fluid) {
+ return recipesByFluidInput.get(fluid.getName());
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/FurnaceBackend.java b/src/main/java/gregtech/api/recipe/maps/FurnaceBackend.java
new file mode 100644
index 0000000000..c4095eeb4e
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/FurnaceBackend.java
@@ -0,0 +1,52 @@
+package gregtech.api.recipe.maps;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Special Class for Furnace Recipe handling.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class FurnaceBackend extends NonGTBackend {
+
+ public FurnaceBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected GT_Recipe overwriteFindRecipe(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot,
+ @Nullable GT_Recipe cachedRecipe) {
+ if (items.length == 0 || items[0] == null) {
+ return null;
+ }
+ if (cachedRecipe != null && cachedRecipe.isRecipeInputEqual(false, true, fluids, items)) {
+ return cachedRecipe;
+ }
+ ItemStack output = GT_ModHandler.getSmeltingOutput(items[0], false, null);
+ return output == null ? null
+ : GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(1, items[0]))
+ .itemOutputs(output)
+ .duration(128)
+ .eut(4)
+ .noOptimize()
+ .build()
+ .orElse(null);
+ }
+
+ @Override
+ public boolean containsInput(ItemStack item) {
+ return GT_ModHandler.getSmeltingOutput(item, false, null) != null;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/LargeBoilerFuelBackend.java b/src/main/java/gregtech/api/recipe/maps/LargeBoilerFuelBackend.java
new file mode 100644
index 0000000000..53152312f4
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/LargeBoilerFuelBackend.java
@@ -0,0 +1,132 @@
+package gregtech.api.recipe.maps;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.ConfigCategories;
+import gregtech.api.enums.GT_Values;
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@SuppressWarnings({ "unused", "UnusedReturnValue" })
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class LargeBoilerFuelBackend extends RecipeMapBackend {
+
+ private static boolean addedGeneralDesc = false;
+
+ private static final List<String> ALLOWED_SOLID_FUELS = Arrays.asList(
+ GregTech_API.sMachineFile.mConfig.getStringList(
+ "LargeBoiler.allowedFuels",
+ ConfigCategories.machineconfig.toString(),
+ new String[] { "gregtech:gt.blockreinforced:6", "gregtech:gt.blockreinforced:7" },
+ "Allowed fuels for the Large Titanium Boiler and Large Tungstensteel Boiler"));
+
+ public LargeBoilerFuelBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ public static boolean isAllowedSolidFuel(ItemStack stack) {
+ return isAllowedSolidFuel(Item.itemRegistry.getNameForObject(stack.getItem()), stack.getItemDamage());
+ }
+
+ public static boolean isAllowedSolidFuel(String itemRegistryName, int meta) {
+ return ALLOWED_SOLID_FUELS.contains(itemRegistryName + ":" + meta);
+ }
+
+ public static boolean addAllowedSolidFuel(ItemStack stack) {
+ return addAllowedSolidFuel(Item.itemRegistry.getNameForObject(stack.getItem()), stack.getItemDamage());
+ }
+
+ public static boolean addAllowedSolidFuel(String itemregistryName, int meta) {
+ return ALLOWED_SOLID_FUELS.add(itemregistryName + ":" + meta);
+ }
+
+ public GT_Recipe addDenseLiquidRecipe(GT_Recipe recipe) {
+ return addRecipe(recipe, ((double) recipe.mSpecialValue) / 10, false);
+ }
+
+ public GT_Recipe addDieselRecipe(GT_Recipe recipe) {
+ return addRecipe(recipe, ((double) recipe.mSpecialValue) / 40, false);
+ }
+
+ public void addSolidRecipes(ItemStack... itemStacks) {
+ for (ItemStack itemStack : itemStacks) {
+ addSolidRecipe(itemStack);
+ }
+ }
+
+ @Nullable
+ public GT_Recipe addSolidRecipe(@Nullable ItemStack fuelItemStack) {
+ if (fuelItemStack == null) {
+ return null;
+ }
+ if (!addedGeneralDesc) {
+ GT_Values.RA.stdBuilder()
+ .duration(1)
+ .eut(1)
+ .specialValue(1)
+ .setNEIDesc(
+ "Not all solid fuels are listed.",
+ "Any item that burns in a",
+ "vanilla furnace will burn in",
+ "a Large Bronze or Steel Boiler.")
+ .build()
+ .map(this::compileRecipe);
+ addedGeneralDesc = true;
+ }
+
+ String registryName = Item.itemRegistry.getNameForObject(fuelItemStack.getItem());
+ boolean isHighTierAllowed = ALLOWED_SOLID_FUELS.contains(registryName + ":" + fuelItemStack.getItemDamage());
+ return GT_Values.RA.stdBuilder()
+ .itemInputs(fuelItemStack)
+ .duration(1)
+ .eut(0)
+ .specialValue(GT_ModHandler.getFuelValue(fuelItemStack) / 1600)
+ .build()
+ .map(r -> addRecipe(r, ((double) GT_ModHandler.getFuelValue(fuelItemStack)) / 1600, isHighTierAllowed))
+ .orElse(null);
+ }
+
+ private GT_Recipe addRecipe(GT_Recipe recipe, double baseBurnTime, boolean isHighTierAllowed) {
+ // Some recipes will have a burn time like 15.9999999 and % always rounds down
+ double floatErrorCorrection = 0.0001;
+
+ double bronzeBurnTime = baseBurnTime * 2 + floatErrorCorrection;
+ bronzeBurnTime -= bronzeBurnTime % 0.05;
+ double steelBurnTime = baseBurnTime + floatErrorCorrection;
+ steelBurnTime -= steelBurnTime % 0.05;
+ double titaniumBurnTime = baseBurnTime * 0.3 + floatErrorCorrection;
+ titaniumBurnTime -= titaniumBurnTime % 0.05;
+ double tungstensteelBurnTime = baseBurnTime * 0.15 + floatErrorCorrection;
+ tungstensteelBurnTime -= tungstensteelBurnTime % 0.05;
+
+ if (isHighTierAllowed) {
+ recipe.setNeiDesc(
+ "Burn time in seconds:",
+ String.format("Bronze Boiler: %.4f", bronzeBurnTime),
+ String.format("Steel Boiler: %.4f", steelBurnTime),
+ String.format("Titanium Boiler: %.4f", titaniumBurnTime),
+ String.format("Tungstensteel Boiler: %.4f", tungstensteelBurnTime));
+ } else {
+ recipe.setNeiDesc(
+ "Burn time in seconds:",
+ String.format("Bronze Boiler: %.4f", bronzeBurnTime),
+ String.format("Steel Boiler: %.4f", steelBurnTime),
+ "Titanium Boiler: Not allowed",
+ "Tungstenst. Boiler: Not allowed");
+ }
+
+ return compileRecipe(recipe);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/LargeBoilerFuelFrontend.java b/src/main/java/gregtech/api/recipe/maps/LargeBoilerFuelFrontend.java
new file mode 100644
index 0000000000..dbe7f6fe2f
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/LargeBoilerFuelFrontend.java
@@ -0,0 +1,25 @@
+package gregtech.api.recipe.maps;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import gregtech.api.recipe.BasicUIPropertiesBuilder;
+import gregtech.api.recipe.NEIRecipePropertiesBuilder;
+import gregtech.api.recipe.RecipeMapFrontend;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.nei.RecipeDisplayInfo;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class LargeBoilerFuelFrontend extends RecipeMapFrontend {
+
+ public LargeBoilerFuelFrontend(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder) {
+ super(uiPropertiesBuilder, neiPropertiesBuilder);
+ }
+
+ @Override
+ protected void drawEnergyInfo(RecipeDisplayInfo recipeInfo) {}
+
+ @Override
+ protected void drawDurationInfo(RecipeDisplayInfo recipeInfo) {}
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/LargeNEIFrontend.java b/src/main/java/gregtech/api/recipe/maps/LargeNEIFrontend.java
new file mode 100644
index 0000000000..70184a83de
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/LargeNEIFrontend.java
@@ -0,0 +1,65 @@
+package gregtech.api.recipe.maps;
+
+import java.util.List;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.math.Size;
+
+import gregtech.api.recipe.BasicUIPropertiesBuilder;
+import gregtech.api.recipe.NEIRecipePropertiesBuilder;
+import gregtech.api.recipe.RecipeMapFrontend;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.common.gui.modularui.UIHelper;
+
+/**
+ * Nicely display NEI with many items and fluids. Remember to call
+ * If row count >= 6, it doesn't fit in 2 recipes per page, so change it via IMC.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class LargeNEIFrontend extends RecipeMapFrontend {
+
+ private static final int xDirMaxCount = 3;
+ private static final int yOrigin = 8;
+
+ private final int itemRowCount;
+ private final int fluidRowCount;
+
+ public LargeNEIFrontend(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder) {
+ super(uiPropertiesBuilder.logoPos(new Pos2d(80, 62)), neiPropertiesBuilder);
+ this.itemRowCount = getItemRowCount();
+ this.fluidRowCount = getFluidRowCount();
+ neiProperties.recipeBackgroundSize = new Size(170, 82 + (Math.max(itemRowCount + fluidRowCount - 4, 0)) * 18);
+ }
+
+ @Override
+ public List<Pos2d> getItemInputPositions(int itemInputCount) {
+ return UIHelper.getGridPositions(itemInputCount, 16, yOrigin, xDirMaxCount);
+ }
+
+ @Override
+ public List<Pos2d> getItemOutputPositions(int itemOutputCount) {
+ return UIHelper.getGridPositions(itemOutputCount, 106, yOrigin, xDirMaxCount);
+ }
+
+ @Override
+ public List<Pos2d> getFluidInputPositions(int fluidInputCount) {
+ return UIHelper.getGridPositions(fluidInputCount, 16, yOrigin + itemRowCount * 18, xDirMaxCount);
+ }
+
+ @Override
+ public List<Pos2d> getFluidOutputPositions(int fluidOutputCount) {
+ return UIHelper.getGridPositions(fluidOutputCount, 106, yOrigin + itemRowCount * 18, xDirMaxCount);
+ }
+
+ private int getItemRowCount() {
+ return (Math.max(uiProperties.maxItemInputs, uiProperties.maxItemOutputs) - 1) / xDirMaxCount + 1;
+ }
+
+ private int getFluidRowCount() {
+ return (Math.max(uiProperties.maxFluidInputs, uiProperties.maxFluidOutputs) - 1) / xDirMaxCount + 1;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/MicrowaveBackend.java b/src/main/java/gregtech/api/recipe/maps/MicrowaveBackend.java
new file mode 100644
index 0000000000..53623cb0c7
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/MicrowaveBackend.java
@@ -0,0 +1,145 @@
+package gregtech.api.recipe.maps;
+
+import static gregtech.api.enums.GT_Values.W;
+import static gregtech.api.util.GT_RecipeConstants.EXPLODE;
+import static gregtech.api.util.GT_RecipeConstants.ON_FIRE;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.init.Blocks;
+import net.minecraft.init.Items;
+import net.minecraft.item.ItemStack;
+import net.minecraft.tileentity.TileEntityFurnace;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.SubTag;
+import gregtech.api.objects.ItemData;
+import gregtech.api.objects.MaterialStack;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_Log;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_OreDictUnificator;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Special Class for Microwave Recipe handling.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class MicrowaveBackend extends NonGTBackend {
+
+ public MicrowaveBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected GT_Recipe overwriteFindRecipe(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot,
+ @Nullable GT_Recipe cachedRecipe) {
+ if (items.length == 0 || items[0] == null) {
+ return null;
+ }
+ if (cachedRecipe != null && cachedRecipe.isRecipeInputEqual(false, true, fluids, items)) {
+ return cachedRecipe;
+ }
+
+ ItemStack output = GT_ModHandler.getSmeltingOutput(items[0], false, null);
+
+ if (GT_Utility.areStacksEqual(items[0], new ItemStack(Items.book, 1, W))) {
+ return GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(1, items[0]))
+ .itemOutputs(GT_Utility.getWrittenBook("Manual_Microwave", ItemList.Book_Written_03.get(1)))
+ .duration(32)
+ .eut(4)
+ .noOptimize()
+ .build()
+ .orElse(null);
+ }
+
+ // Check Container Item of Input since it is around the Input, then the Input itself, then Container Item of
+ // Output and last check the Output itself
+ for (ItemStack item : new ItemStack[] { GT_Utility.getContainerItem(items[0], true), items[0],
+ GT_Utility.getContainerItem(output, true), output }) {
+ if (item == null) continue;
+ if (GT_Utility.areStacksEqual(item, new ItemStack(Blocks.netherrack, 1, W), true)
+ || GT_Utility.areStacksEqual(item, new ItemStack(Blocks.tnt, 1, W), true)
+ || GT_Utility.areStacksEqual(item, new ItemStack(Items.egg, 1, W), true)
+ || GT_Utility.areStacksEqual(item, new ItemStack(Items.firework_charge, 1, W), true)
+ || GT_Utility.areStacksEqual(item, new ItemStack(Items.fireworks, 1, W), true)
+ || GT_Utility.areStacksEqual(item, new ItemStack(Items.fire_charge, 1, W), true)) {
+ GT_Log.exp
+ .println("Microwave Explosion due to TNT || EGG || FIREWORKCHARGE || FIREWORK || FIRE CHARGE");
+ return GT_RecipeBuilder.empty()
+ .metadata(EXPLODE, true)
+ .build()
+ .orElse(null);
+ }
+
+ ItemData itemData = GT_OreDictUnificator.getItemData(item);
+ if (itemData != null) {
+ if (itemData.mMaterial != null && itemData.mMaterial.mMaterial != null) {
+ if (itemData.mMaterial.mMaterial.contains(SubTag.METAL)
+ || itemData.mMaterial.mMaterial.contains(SubTag.EXPLOSIVE)) {
+ GT_Log.exp.println("Microwave Explosion due to METAL insertion");
+ return GT_RecipeBuilder.empty()
+ .metadata(EXPLODE, true)
+ .build()
+ .orElse(null);
+ }
+ if (itemData.mMaterial.mMaterial.contains(SubTag.FLAMMABLE)) {
+ GT_Log.exp.println("Microwave INFLAMMATION due to FLAMMABLE insertion");
+ return GT_RecipeBuilder.empty()
+ .metadata(ON_FIRE, true)
+ .build()
+ .orElse(null);
+ }
+ }
+ for (MaterialStack materialStack : itemData.mByProducts) {
+ if (materialStack == null) continue;
+ if (materialStack.mMaterial.contains(SubTag.METAL)
+ || materialStack.mMaterial.contains(SubTag.EXPLOSIVE)) {
+ GT_Log.exp.println("Microwave Explosion due to METAL insertion");
+ return GT_RecipeBuilder.empty()
+ .metadata(EXPLODE, true)
+ .build()
+ .orElse(null);
+ }
+ if (materialStack.mMaterial.contains(SubTag.FLAMMABLE)) {
+ GT_Log.exp.println("Microwave INFLAMMATION due to FLAMMABLE insertion");
+ return GT_RecipeBuilder.empty()
+ .metadata(ON_FIRE, true)
+ .build()
+ .orElse(null);
+ }
+ }
+ }
+ if (TileEntityFurnace.getItemBurnTime(item) > 0) {
+ GT_Log.exp.println("Microwave INFLAMMATION due to BURNABLE insertion");
+ return GT_RecipeBuilder.empty()
+ .metadata(ON_FIRE, true)
+ .build()
+ .orElse(null);
+ }
+ }
+
+ return output == null ? null
+ : GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(1, items[0]))
+ .itemOutputs(output)
+ .duration(32)
+ .eut(4)
+ .noOptimize()
+ .build()
+ .orElse(null);
+ }
+
+ @Override
+ public boolean containsInput(ItemStack item) {
+ return GT_ModHandler.getSmeltingOutput(item, false, null) != null;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/NonGTBackend.java b/src/main/java/gregtech/api/recipe/maps/NonGTBackend.java
new file mode 100644
index 0000000000..1e871df372
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/NonGTBackend.java
@@ -0,0 +1,52 @@
+package gregtech.api.recipe.maps;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Abstract class for general recipe handling of non GT recipes
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public abstract class NonGTBackend extends RecipeMapBackend {
+
+ public NonGTBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected abstract GT_Recipe overwriteFindRecipe(ItemStack[] items, FluidStack[] fluids,
+ @Nullable ItemStack specialSlot, @Nullable GT_Recipe cachedRecipe);
+
+ @Override
+ protected boolean doesOverwriteFindRecipe() {
+ return true;
+ }
+
+ @Override
+ public boolean containsInput(ItemStack item) {
+ return false;
+ }
+
+ @Override
+ public boolean containsInput(Fluid fluid) {
+ return false;
+ }
+
+ @Override
+ public void reInit() {}
+
+ @Override
+ protected GT_Recipe addToItemMap(GT_Recipe recipe) {
+ return recipe;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/OilCrackerBackend.java b/src/main/java/gregtech/api/recipe/maps/OilCrackerBackend.java
new file mode 100644
index 0000000000..c2c312a48a
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/OilCrackerBackend.java
@@ -0,0 +1,41 @@
+package gregtech.api.recipe.maps;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class OilCrackerBackend extends RecipeMapBackend {
+
+ private final Set<String> validCatalystFluidNames = new HashSet<>();
+
+ public OilCrackerBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ public GT_Recipe compileRecipe(GT_Recipe recipe) {
+ super.compileRecipe(recipe);
+ if (recipe.mFluidInputs != null && recipe.mFluidInputs.length > 1 && recipe.mFluidInputs[1] != null) {
+ validCatalystFluidNames.add(
+ recipe.mFluidInputs[1].getFluid()
+ .getName());
+ }
+ return recipe;
+ }
+
+ public boolean isValidCatalystFluid(FluidStack fluid) {
+ return validCatalystFluidNames.contains(
+ fluid.getFluid()
+ .getName());
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/PrinterBackend.java b/src/main/java/gregtech/api/recipe/maps/PrinterBackend.java
new file mode 100644
index 0000000000..a933886447
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/PrinterBackend.java
@@ -0,0 +1,142 @@
+package gregtech.api.recipe.maps;
+
+import static gregtech.api.enums.GT_Values.L;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.init.Items;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.enums.Dyes;
+import gregtech.api.enums.GT_Values;
+import gregtech.api.enums.ItemList;
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Special Class for Printer handling.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class PrinterBackend extends RecipeMapBackend {
+
+ public PrinterBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected GT_Recipe modifyFoundRecipe(GT_Recipe recipe, ItemStack[] items, FluidStack[] fluids,
+ @Nullable ItemStack specialSlot) {
+ if (items[0].getItem() == Items.paper) {
+ assert specialSlot != null;
+ if (!ItemList.Tool_DataStick.isStackEqual(specialSlot, false, true)) return null;
+ NBTTagCompound nbt = specialSlot.getTagCompound();
+ if (nbt == null || GT_Utility.isStringInvalid(nbt.getString("title"))
+ || GT_Utility.isStringInvalid(nbt.getString("author"))) return null;
+
+ recipe = recipe.copy();
+ recipe.mCanBeBuffered = false;
+ recipe.mOutputs[0].setTagCompound(nbt);
+ return recipe;
+ }
+ if (items[0].getItem() == Items.map) {
+ assert specialSlot != null;
+ if (!ItemList.Tool_DataStick.isStackEqual(specialSlot, false, true)) return null;
+ NBTTagCompound nbt = specialSlot.getTagCompound();
+ if (nbt == null || !nbt.hasKey("map_id")) return null;
+
+ recipe = recipe.copy();
+ recipe.mCanBeBuffered = false;
+ recipe.mOutputs[0].setItemDamage(nbt.getShort("map_id"));
+ return recipe;
+ }
+ if (ItemList.Paper_Punch_Card_Empty.isStackEqual(items[0], false, true)) {
+ assert specialSlot != null;
+ if (!ItemList.Tool_DataStick.isStackEqual(specialSlot, false, true)) return null;
+ NBTTagCompound nbt = specialSlot.getTagCompound();
+ if (nbt == null || !nbt.hasKey("GT.PunchCardData")) return null;
+
+ recipe = recipe.copy();
+ recipe.mCanBeBuffered = false;
+ recipe.mOutputs[0].setTagCompound(
+ GT_Utility.getNBTContainingString(
+ new NBTTagCompound(),
+ "GT.PunchCardData",
+ nbt.getString("GT.PunchCardData")));
+ return recipe;
+ }
+ return recipe;
+ }
+
+ @Override
+ protected GT_Recipe findFallback(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot) {
+ if (items.length == 0 || items[0] == null || fluids.length == 0 || fluids[0] == null) {
+ return null;
+ }
+ Dyes dye = null;
+ for (Dyes tDye : Dyes.VALUES) if (tDye.isFluidDye(fluids[0])) {
+ dye = tDye;
+ break;
+ }
+ if (dye == null) return null;
+
+ ItemStack batchRecolorOutput = GT_ModHandler.getAllRecipeOutput(
+ null,
+ items[0],
+ items[0],
+ items[0],
+ items[0],
+ ItemList.DYE_ONLY_ITEMS[dye.mIndex].get(1),
+ items[0],
+ items[0],
+ items[0],
+ items[0]);
+ if (batchRecolorOutput != null) {
+ return GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(8, items[0]))
+ .itemOutputs(batchRecolorOutput)
+ .fluidInputs(new FluidStack(fluids[0].getFluid(), (int) L))
+ .duration(256)
+ .eut(2)
+ .hidden()
+ .build()
+ .map(this::compileRecipe)
+ .orElse(null);
+ }
+
+ ItemStack singleRecolorOutput = GT_ModHandler
+ .getAllRecipeOutput(null, items[0], ItemList.DYE_ONLY_ITEMS[dye.mIndex].get(1));
+ if (singleRecolorOutput != null) {
+ return GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(1, items[0]))
+ .itemOutputs(singleRecolorOutput)
+ .fluidInputs(new FluidStack(fluids[0].getFluid(), (int) L))
+ .duration(32)
+ .eut(2)
+ .hidden()
+ .build()
+ .map(this::compileRecipe)
+ .orElse(null);
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean containsInput(ItemStack item) {
+ return true;
+ }
+
+ @Override
+ public boolean containsInput(Fluid fluid) {
+ return super.containsInput(fluid) || Dyes.isAnyFluidDye(fluid);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/RecyclerBackend.java b/src/main/java/gregtech/api/recipe/maps/RecyclerBackend.java
new file mode 100644
index 0000000000..55fb9b4cc4
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/RecyclerBackend.java
@@ -0,0 +1,55 @@
+package gregtech.api.recipe.maps;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Special Class for Recycler Recipe handling.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class RecyclerBackend extends NonGTBackend {
+
+ public RecyclerBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected GT_Recipe overwriteFindRecipe(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot,
+ @Nullable GT_Recipe cachedRecipe) {
+ if (items.length == 0 || items[0] == null) {
+ return null;
+ }
+ if (cachedRecipe != null && cachedRecipe.isRecipeInputEqual(false, true, fluids, items)) {
+ return cachedRecipe;
+ }
+ GT_RecipeBuilder builder = GT_Values.RA.stdBuilder()
+ .itemInputs(GT_Utility.copyAmount(1, items[0]));
+ ItemStack output = GT_ModHandler.getRecyclerOutput(items[0], 0);
+ if (output != null) {
+ builder.itemOutputs(output)
+ .outputChances(1250);
+ }
+ return builder.duration(45)
+ .eut(1)
+ .noOptimize()
+ .build()
+ .orElse(null);
+ }
+
+ @Override
+ public boolean containsInput(ItemStack item) {
+ return GT_ModHandler.getRecyclerOutput(item, 0) != null;
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/ReplicatorBackend.java b/src/main/java/gregtech/api/recipe/maps/ReplicatorBackend.java
new file mode 100644
index 0000000000..f201698457
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/ReplicatorBackend.java
@@ -0,0 +1,100 @@
+package gregtech.api.recipe.maps;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.GT_Mod;
+import gregtech.api.enums.Element;
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.TierEU;
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_RecipeBuilder;
+import gregtech.api.util.GT_RecipeConstants;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.common.items.behaviors.Behaviour_DataOrb;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class ReplicatorBackend extends RecipeMapBackend {
+
+ private final Map<Materials, GT_Recipe> recipesByMaterial = new HashMap<>();
+
+ public ReplicatorBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder.recipeEmitter(ReplicatorBackend::replicatorRecipeEmitter));
+ }
+
+ @Override
+ public GT_Recipe compileRecipe(GT_Recipe recipe) {
+ super.compileRecipe(recipe);
+ Materials material = recipe.getMetadata(GT_RecipeConstants.MATERIAL);
+ assert material != null; // checked by replicatorRecipeEmitter
+ recipesByMaterial.put(material, recipe);
+ return recipe;
+ }
+
+ @Override
+ protected boolean doesOverwriteFindRecipe() {
+ return true;
+ }
+
+ @Override
+ protected GT_Recipe overwriteFindRecipe(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot,
+ @Nullable GT_Recipe cachedRecipe) {
+ if (specialSlot == null) {
+ return null;
+ }
+ Materials foundMaterial = getMaterialFromDataOrb(specialSlot);
+ if (foundMaterial == null) {
+ return null;
+ }
+ return recipesByMaterial.getOrDefault(foundMaterial, null);
+ }
+
+ @Nullable
+ private static Materials getMaterialFromDataOrb(ItemStack stack) {
+ if (ItemList.Tool_DataOrb.isStackEqual(stack, false, true) && Behaviour_DataOrb.getDataTitle(stack)
+ .equals("Elemental-Scan")) {
+ return Element.get(Behaviour_DataOrb.getDataName(stack)).mLinkedMaterials.stream()
+ .findFirst()
+ .orElse(null);
+ }
+ return null;
+ }
+
+ private static Collection<GT_Recipe> replicatorRecipeEmitter(GT_RecipeBuilder builder) {
+ Materials material = builder.getMetadata(GT_RecipeConstants.MATERIAL);
+ if (material == null) {
+ throw new IllegalStateException("GT_RecipeConstants.MATERIAL must be set for replicator recipe");
+ }
+ return Optional.of(material)
+ .map(material1 -> material1.mElement)
+ .map(Element::getMass)
+ .map(ReplicatorBackend::getUUMAmountFromMass)
+ .flatMap(
+ uum -> builder.fluidInputs(Materials.UUMatter.getFluid(uum))
+ .duration(GT_Utility.safeInt(uum * 512L, 1))
+ .eut(TierEU.RECIPE_LV)
+ .ignoreCollision()
+ .noOptimize()
+ .build())
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList());
+ }
+
+ private static int getUUMAmountFromMass(long mass) {
+ return GT_Utility.safeInt((long) Math.pow(mass, GT_Mod.gregtechproxy.replicatorExponent), 1);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/SpaceProjectFrontend.java b/src/main/java/gregtech/api/recipe/maps/SpaceProjectFrontend.java
new file mode 100644
index 0000000000..98463dcc4d
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/SpaceProjectFrontend.java
@@ -0,0 +1,131 @@
+package gregtech.api.recipe.maps;
+
+import static gregtech.api.util.GT_Utility.formatNumbers;
+import static net.minecraft.util.EnumChatFormatting.GRAY;
+import static net.minecraft.util.StatCollector.translateToLocal;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.util.EnumChatFormatting;
+
+import com.gtnewhorizons.modularui.api.drawable.IDrawable;
+import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable;
+import com.gtnewhorizons.modularui.api.math.Alignment;
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+import com.gtnewhorizons.modularui.common.widget.DrawableWidget;
+import com.gtnewhorizons.modularui.common.widget.ProgressBar;
+
+import appeng.util.ReadableNumberConverter;
+import codechicken.lib.gui.GuiDraw;
+import codechicken.nei.PositionedStack;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.recipe.BasicUIPropertiesBuilder;
+import gregtech.api.recipe.NEIRecipePropertiesBuilder;
+import gregtech.api.recipe.RecipeMapFrontend;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.common.gui.modularui.UIHelper;
+import gregtech.common.misc.spaceprojects.SpaceProjectManager;
+import gregtech.common.misc.spaceprojects.SpaceProjectManager.FakeSpaceProjectRecipe;
+import gregtech.common.misc.spaceprojects.interfaces.ISpaceProject;
+import gregtech.nei.GT_NEI_DefaultHandler;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class SpaceProjectFrontend extends RecipeMapFrontend {
+
+ IDrawable projectTexture;
+
+ public SpaceProjectFrontend(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder) {
+ super(uiPropertiesBuilder, neiPropertiesBuilder);
+ }
+
+ @Override
+ public ModularWindow.Builder createNEITemplate(IItemHandlerModifiable itemInputsInventory,
+ IItemHandlerModifiable itemOutputsInventory, IItemHandlerModifiable specialSlotInventory,
+ IItemHandlerModifiable fluidInputsInventory, IItemHandlerModifiable fluidOutputsInventory,
+ Supplier<Float> progressSupplier, Pos2d windowOffset) {
+ ModularWindow.Builder builder = super.createNEITemplate(
+ itemInputsInventory,
+ itemOutputsInventory,
+ specialSlotInventory,
+ fluidInputsInventory,
+ fluidOutputsInventory,
+ progressSupplier,
+ windowOffset);
+ builder.widget(
+ new DrawableWidget().setDrawable(() -> projectTexture)
+ .setSize(18, 18)
+ .setPos(new Pos2d(124, 28).add(windowOffset)));
+ return builder;
+ }
+
+ @Override
+ public List<Pos2d> getItemInputPositions(int itemInputCount) {
+ return UIHelper.getGridPositions(itemInputCount, 16, 28, 3);
+ }
+
+ @Override
+ public List<Pos2d> getFluidInputPositions(int fluidInputCount) {
+ return UIHelper.getGridPositions(fluidInputCount, 88, 28, 1);
+ }
+
+ @Override
+ protected List<String> handleNEIItemInputTooltip(List<String> currentTip,
+ GT_NEI_DefaultHandler.FixedPositionedStack pStack) {
+ super.handleNEIItemOutputTooltip(currentTip, pStack);
+ if (pStack.isFluid()) return currentTip;
+ currentTip.add(GRAY + translateToLocal("Item Count: ") + formatNumbers(pStack.realStackSize));
+ return currentTip;
+ }
+
+ @Override
+ public void drawNEIOverlays(GT_NEI_DefaultHandler.CachedDefaultRecipe neiCachedRecipe) {
+ for (PositionedStack stack : neiCachedRecipe.mInputs) {
+ if (stack instanceof GT_NEI_DefaultHandler.FixedPositionedStack pStack && stack.item != null
+ && !pStack.isFluid()) {
+ int stackSize = ((GT_NEI_DefaultHandler.FixedPositionedStack) stack).realStackSize;
+ String displayString;
+ if (stack.item.stackSize > 9999) {
+ displayString = ReadableNumberConverter.INSTANCE.toWideReadableForm(stackSize);
+ } else {
+ displayString = String.valueOf(stackSize);
+ }
+ drawNEIOverlayText(displayString, stack, 0xffffff, 0.5f, true, Alignment.BottomRight);
+ }
+ }
+ if (neiCachedRecipe.mRecipe instanceof FakeSpaceProjectRecipe) {
+ ISpaceProject project = SpaceProjectManager
+ .getProject(((FakeSpaceProjectRecipe) neiCachedRecipe.mRecipe).projectName);
+ if (project != null) {
+ projectTexture = project.getTexture();
+ GuiDraw.drawStringC(EnumChatFormatting.BOLD + project.getLocalizedName(), 85, 0, 0x404040, false);
+ }
+ }
+ }
+
+ @Override
+ public void addProgressBar(ModularWindow.Builder builder, Supplier<Float> progressSupplier, Pos2d windowOffset) {
+ int bar1Width = 17;
+ int bar2Width = 18;
+ List<Supplier<Float>> splitProgress = splitProgress(progressSupplier, bar1Width, bar2Width);
+ builder.widget(
+ new ProgressBar().setTexture(GT_UITextures.PROGRESSBAR_ASSEMBLY_LINE_1, 17)
+ .setDirection(ProgressBar.Direction.RIGHT)
+ .setProgress(splitProgress.get(0))
+ .setSynced(false, false)
+ .setPos(new Pos2d(70, 28).add(windowOffset))
+ .setSize(bar1Width, 72));
+ builder.widget(
+ new ProgressBar().setTexture(GT_UITextures.PROGRESSBAR_ASSEMBLY_LINE_2, 18)
+ .setDirection(ProgressBar.Direction.RIGHT)
+ .setProgress(splitProgress.get(1))
+ .setSynced(false, false)
+ .setPos(new Pos2d(106, 28).add(windowOffset))
+ .setSize(bar2Width, 72));
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/TranscendentPlasmaMixerFrontend.java b/src/main/java/gregtech/api/recipe/maps/TranscendentPlasmaMixerFrontend.java
new file mode 100644
index 0000000000..7a5d7ff164
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/TranscendentPlasmaMixerFrontend.java
@@ -0,0 +1,56 @@
+package gregtech.api.recipe.maps;
+
+import static gregtech.api.util.GT_Utility.formatNumbers;
+
+import java.util.List;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+
+import gregtech.api.recipe.BasicUIPropertiesBuilder;
+import gregtech.api.recipe.NEIRecipePropertiesBuilder;
+import gregtech.api.recipe.RecipeMapFrontend;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.common.gui.modularui.UIHelper;
+import gregtech.nei.RecipeDisplayInfo;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class TranscendentPlasmaMixerFrontend extends RecipeMapFrontend {
+
+ public TranscendentPlasmaMixerFrontend(BasicUIPropertiesBuilder uiPropertiesBuilder,
+ NEIRecipePropertiesBuilder neiPropertiesBuilder) {
+ super(uiPropertiesBuilder, neiPropertiesBuilder);
+ }
+
+ @Override
+ public List<Pos2d> getItemInputPositions(int itemInputCount) {
+ return UIHelper.getGridPositions(itemInputCount, 60, 8, 1);
+ }
+
+ @Override
+ public List<Pos2d> getFluidInputPositions(int fluidInputCount) {
+ return UIHelper.getGridPositions(fluidInputCount, 6, 26, 4, 5);
+ }
+
+ @Override
+ public List<Pos2d> getFluidOutputPositions(int fluidOutputCount) {
+ return UIHelper.getGridPositions(fluidOutputCount, 114, 44, 1);
+ }
+
+ @Override
+ protected void drawEnergyInfo(RecipeDisplayInfo recipeInfo) {
+ // These look odd because recipeInfo.recipe.mEUt is actually the EU per litre of fluid processed, not
+ // the EU/t.
+ recipeInfo.drawText(
+ GT_Utility.trans("152", "Total: ")
+ + formatNumbers(1000L * recipeInfo.recipe.mDuration / 100L * recipeInfo.recipe.mEUt)
+ + " EU");
+ // 1000 / (20 ticks * 5 seconds) = 10L/t. 10L/t * x EU/L = 10 * x EU/t.
+ long averageUsage = 10L * recipeInfo.recipe.mEUt;
+ recipeInfo.drawText(
+ "Average: " + formatNumbers(averageUsage) + " EU/t" + GT_Utility.getTierNameWithParentheses(averageUsage));
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/maps/UnpackagerBackend.java b/src/main/java/gregtech/api/recipe/maps/UnpackagerBackend.java
new file mode 100644
index 0000000000..e7297f0609
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/maps/UnpackagerBackend.java
@@ -0,0 +1,53 @@
+package gregtech.api.recipe.maps;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.enums.ItemList;
+import gregtech.api.recipe.RecipeMapBackend;
+import gregtech.api.recipe.RecipeMapBackendPropertiesBuilder;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class UnpackagerBackend extends RecipeMapBackend {
+
+ public UnpackagerBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
+ super(propertiesBuilder);
+ }
+
+ @Override
+ protected GT_Recipe findFallback(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot) {
+ if (items.length == 0 || !ItemList.IC2_Scrapbox.isStackEqual(items[0], false, true)) {
+ return null;
+ }
+
+ ItemStack output = GT_ModHandler.getRandomScrapboxDrop();
+ if (output == null) {
+ return null;
+ }
+ return GT_Values.RA.stdBuilder()
+ .itemInputs(ItemList.IC2_Scrapbox.get(1))
+ .itemOutputs(output)
+ .duration(16)
+ .eut(1)
+ // It is not allowed to be buffered due to the random Output
+ .noBuffer()
+ // Due to its randomness it is not good if there are Items in the Output Slot, because those Items could
+ // manipulate the outcome.
+ .needsEmptyOutput()
+ .build()
+ .orElse(null);
+ }
+
+ @Override
+ public boolean containsInput(ItemStack item) {
+ return ItemList.IC2_Scrapbox.isStackEqual(item, false, true) || super.containsInput(item);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/metadata/EmptyRecipeMetadataStorage.java b/src/main/java/gregtech/api/recipe/metadata/EmptyRecipeMetadataStorage.java
new file mode 100644
index 0000000000..f7831f1485
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/metadata/EmptyRecipeMetadataStorage.java
@@ -0,0 +1,50 @@
+package gregtech.api.recipe.metadata;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.jetbrains.annotations.Contract;
+
+import gregtech.api.recipe.RecipeMetadataKey;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public final class EmptyRecipeMetadataStorage implements IRecipeMetadataStorage {
+
+ public static EmptyRecipeMetadataStorage INSTANCE = new EmptyRecipeMetadataStorage();
+
+ private EmptyRecipeMetadataStorage() {}
+
+ @Override
+ public <T> void store(RecipeMetadataKey<T> key, @Nullable T value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Nullable
+ @Override
+ public <T> T getMetadata(RecipeMetadataKey<T> key) {
+ return null;
+ }
+
+ @Contract("_, !null -> !null")
+ @Nullable
+ @Override
+ public <T> T getMetadataOrDefault(RecipeMetadataKey<T> key, @Nullable T defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public Set<Map.Entry<RecipeMetadataKey<?>, Object>> getEntries() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public IRecipeMetadataStorage copy() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/metadata/IRecipeMetadataStorage.java b/src/main/java/gregtech/api/recipe/metadata/IRecipeMetadataStorage.java
new file mode 100644
index 0000000000..52141937cf
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/metadata/IRecipeMetadataStorage.java
@@ -0,0 +1,34 @@
+package gregtech.api.recipe.metadata;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.jetbrains.annotations.Contract;
+
+import gregtech.api.recipe.RecipeMetadataKey;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+/**
+ * Stores set of metadata for the recipe with key {@link RecipeMetadataKey}. More explicit way to store various info
+ * on recipe than special value or special object. Type of the metadata can be anything.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public interface IRecipeMetadataStorage {
+
+ <T> void store(RecipeMetadataKey<T> key, @Nullable T value);
+
+ @Nullable
+ <T> T getMetadata(RecipeMetadataKey<T> key);
+
+ @Contract("_, !null -> !null")
+ @Nullable
+ <T> T getMetadataOrDefault(RecipeMetadataKey<T> key, @Nullable T defaultValue);
+
+ Set<Map.Entry<RecipeMetadataKey<?>, Object>> getEntries();
+
+ IRecipeMetadataStorage copy();
+}
diff --git a/src/main/java/gregtech/api/recipe/metadata/PCBFactoryTierKey.java b/src/main/java/gregtech/api/recipe/metadata/PCBFactoryTierKey.java
new file mode 100644
index 0000000000..05db919b57
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/metadata/PCBFactoryTierKey.java
@@ -0,0 +1,30 @@
+package gregtech.api.recipe.metadata;
+
+import static gregtech.api.util.GT_Utility.trans;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import gregtech.api.recipe.RecipeMetadataKey;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.nei.RecipeDisplayInfo;
+
+/**
+ * Minimum tier required for the PCB factory recipe.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class PCBFactoryTierKey extends RecipeMetadataKey<Integer> {
+
+ public static final PCBFactoryTierKey INSTANCE = new PCBFactoryTierKey();
+
+ private PCBFactoryTierKey() {
+ super(Integer.class, "pcb_factory_tier");
+ }
+
+ @Override
+ public void drawInfo(RecipeDisplayInfo recipeInfo, @Nullable Object value) {
+ int tier = cast(value, 1);
+ recipeInfo.drawText(trans("336", "PCB Factory Tier: ") + tier);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/metadata/PCBFactoryUpgrade.java b/src/main/java/gregtech/api/recipe/metadata/PCBFactoryUpgrade.java
new file mode 100644
index 0000000000..6d76ae05c3
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/metadata/PCBFactoryUpgrade.java
@@ -0,0 +1,7 @@
+package gregtech.api.recipe.metadata;
+
+public enum PCBFactoryUpgrade {
+
+ NONE,
+ BIO
+}
diff --git a/src/main/java/gregtech/api/recipe/metadata/PCBFactoryUpgradeKey.java b/src/main/java/gregtech/api/recipe/metadata/PCBFactoryUpgradeKey.java
new file mode 100644
index 0000000000..8257f1e6ef
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/metadata/PCBFactoryUpgradeKey.java
@@ -0,0 +1,32 @@
+package gregtech.api.recipe.metadata;
+
+import static gregtech.api.util.GT_Utility.trans;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import gregtech.api.recipe.RecipeMetadataKey;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.nei.RecipeDisplayInfo;
+
+/**
+ * If bio upgrade is required for the PCB factory recipe.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class PCBFactoryUpgradeKey extends RecipeMetadataKey<PCBFactoryUpgrade> {
+
+ public static final PCBFactoryUpgradeKey INSTANCE = new PCBFactoryUpgradeKey();
+
+ private PCBFactoryUpgradeKey() {
+ super(PCBFactoryUpgrade.class, "pcb_factory_bio_upgrade");
+ }
+
+ @Override
+ public void drawInfo(RecipeDisplayInfo recipeInfo, @Nullable Object value) {
+ PCBFactoryUpgrade upgrade = cast(value);
+ if (upgrade == PCBFactoryUpgrade.BIO) {
+ recipeInfo.drawText(trans("337", "Upgrade Required: ") + trans("338", "Bio"));
+ }
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/metadata/RecipeMetadataStorage.java b/src/main/java/gregtech/api/recipe/metadata/RecipeMetadataStorage.java
new file mode 100644
index 0000000000..5b65d8a600
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/metadata/RecipeMetadataStorage.java
@@ -0,0 +1,56 @@
+package gregtech.api.recipe.metadata;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.jetbrains.annotations.Contract;
+
+import gregtech.api.recipe.RecipeMetadataKey;
+import gregtech.api.util.FieldsAreNonnullByDefault;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@FieldsAreNonnullByDefault
+public final class RecipeMetadataStorage implements IRecipeMetadataStorage {
+
+ private final Map<RecipeMetadataKey<?>, Object> metadata = new HashMap<>();
+
+ public RecipeMetadataStorage() {}
+
+ private RecipeMetadataStorage(Map<RecipeMetadataKey<?>, Object> metadata) {
+ this.metadata.putAll(metadata);
+ }
+
+ @Override
+ public <T> void store(RecipeMetadataKey<T> key, @Nullable T value) {
+ metadata.put(key, key.cast(value));
+ }
+
+ @Nullable
+ @Override
+ public <T> T getMetadata(RecipeMetadataKey<T> key) {
+ return key.cast(metadata.get(key));
+ }
+
+ @Contract("_, !null -> !null")
+ @Nullable
+ @Override
+ public <T> T getMetadataOrDefault(RecipeMetadataKey<T> key, @Nullable T defaultValue) {
+ return key.cast(metadata.getOrDefault(key, defaultValue));
+ }
+
+ @Override
+ public Set<Map.Entry<RecipeMetadataKey<?>, Object>> getEntries() {
+ return metadata.entrySet();
+ }
+
+ @Override
+ public IRecipeMetadataStorage copy() {
+ return new RecipeMetadataStorage(metadata);
+ }
+}
diff --git a/src/main/java/gregtech/api/recipe/metadata/SimpleRecipeMetadataKey.java b/src/main/java/gregtech/api/recipe/metadata/SimpleRecipeMetadataKey.java
new file mode 100644
index 0000000000..19395f11a0
--- /dev/null
+++ b/src/main/java/gregtech/api/recipe/metadata/SimpleRecipeMetadataKey.java
@@ -0,0 +1,27 @@
+package gregtech.api.recipe.metadata;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import gregtech.api.recipe.RecipeMetadataKey;
+import gregtech.api.util.MethodsReturnNonnullByDefault;
+import gregtech.nei.RecipeDisplayInfo;
+
+/**
+ * Simple metadata key that does not draw anything on NEI.
+ */
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+public class SimpleRecipeMetadataKey<T> extends RecipeMetadataKey<T> {
+
+ private SimpleRecipeMetadataKey(Class<T> clazz, String identifier) {
+ super(clazz, identifier);
+ }
+
+ public static <T> RecipeMetadataKey<T> create(Class<T> clazz, String identifier) {
+ return new SimpleRecipeMetadataKey<>(clazz, identifier);
+ }
+
+ @Override
+ public void drawInfo(RecipeDisplayInfo recipeInfo, @Nullable Object value) {}
+}