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 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.GTUITextures; import gregtech.api.gui.modularui.SteamTexture; import gregtech.api.objects.overclockdescriber.OverclockDescriber; import gregtech.api.util.GTRecipe; import gregtech.api.util.GTRecipeBuilder; 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(GTUITextures.fallbackableProgressbar(unlocalizedName, GTUITextures.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 GTRecipeBuilder, ? extends Iterable<? extends GTRecipe>> 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 GTRecipeBuilder, ? extends GTRecipe> 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 GTRecipeBuilder, ? extends Iterable<? extends GTRecipe>> 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 GTRecipeBuilder, ? extends GTRecipe> 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 GTRecipe, ? extends GTRecipe> 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<GTRecipe> 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 GTRecipe, ? extends GTRecipe> 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<GTRecipe> recipeTransformer) { return chainRecipeTransformer(withIdentityReturn(recipeTransformer)); } // 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(GTUITextures.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 GTRecipe#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<GTRecipe> 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; }; } }