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