Files
NotEnoughItems/src/main/java/codechicken/nei/recipe/GuiRecipe.java
T
Caedis db1fd255bb Slot Highlighting Overlay Option (#458)
* Add option to highlight the entire slot instead of showing an icon for recipe overlay

* Move skip to full slot highlight only
2024-02-19 13:12:01 +01:00

1247 lines
45 KiB
Java

package codechicken.nei.recipe;
import static codechicken.lib.gui.GuiDraw.fontRenderer;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.RenderHelper;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumChatFormatting;
import net.minecraftforge.fluids.FluidStack;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.GL11;
import codechicken.lib.gui.GuiDraw;
import codechicken.nei.Button;
import codechicken.nei.GuiNEIButton;
import codechicken.nei.LayoutManager;
import codechicken.nei.NEICPH;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.NEIClientUtils;
import codechicken.nei.PositionedStack;
import codechicken.nei.RecipeSearchField;
import codechicken.nei.RestartableTask;
import codechicken.nei.VisiblityData;
import codechicken.nei.api.IGuiContainerOverlay;
import codechicken.nei.api.INEIGuiHandler;
import codechicken.nei.api.IOverlayHandler;
import codechicken.nei.api.IRecipeFilter;
import codechicken.nei.api.IRecipeFilter.IRecipeFilterProvider;
import codechicken.nei.api.IRecipeOverlayRenderer;
import codechicken.nei.api.ItemFilter;
import codechicken.nei.api.TaggedInventoryArea;
import codechicken.nei.drawable.DrawableBuilder;
import codechicken.nei.drawable.DrawableResource;
import codechicken.nei.guihook.GuiContainerManager;
import codechicken.nei.guihook.IContainerTooltipHandler;
import codechicken.nei.guihook.IGuiClientSide;
import codechicken.nei.guihook.IGuiHandleMouseWheel;
public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer implements IGuiContainerOverlay,
IGuiClientSide, IGuiHandleMouseWheel, IContainerTooltipHandler, INEIGuiHandler {
// Background image calculations
private static final int BG_TOP_HEIGHT = 6;
private static final int BG_MIDDLE_HEIGHT = 154;
private static final int BG_BOTTOM_HEIGHT = 6;
private static final int BG_TOP_Y = 0;
private static final int BG_MIDDLE_Y = BG_TOP_Y + BG_TOP_HEIGHT;
private static final int BG_BOTTOM_Y = BG_MIDDLE_Y + BG_MIDDLE_HEIGHT;
// Button sizes
private static final int borderPadding = 6;
private static final int buttonWidth = 13;
private static final int buttonHeight = 12;
public static final List<IRecipeFilterProvider> recipeFilterers = new LinkedList<>();
protected boolean limitToOneRecipe = false;
// Background image
final DrawableResource bgTop = new DrawableBuilder("nei:textures/gui/recipebg.png", 0, BG_TOP_Y, 176, BG_TOP_HEIGHT)
.build();
final DrawableResource bgMiddle = new DrawableBuilder(
"nei:textures/gui/recipebg.png",
0,
BG_MIDDLE_Y,
176,
BG_MIDDLE_HEIGHT).build();
final DrawableResource bgBottom = new DrawableBuilder(
"nei:textures/gui/recipebg.png",
0,
BG_BOTTOM_Y,
176,
BG_BOTTOM_HEIGHT).build();
public ArrayList<H> currenthandlers = new ArrayList<>();
public int page;
public int recipetype;
public BookmarkRecipeId recipeId;
public ContainerRecipe slotcontainer;
public GuiContainer firstGui;
public GuiScreen firstGuiGeneral;
public GuiScreen prevGui;
public GuiButton nextpage;
public GuiButton prevpage;
private GuiButton nexttype;
private GuiButton prevtype;
private final int OVERLAY_BUTTON_ID_START = 4;
private GuiButton[] overlayButtons;
private final Rectangle area = new Rectangle();
private final GuiRecipeTabs recipeTabs;
private final GuiRecipeCatalyst guiRecipeCatalyst;
private SearchRecipeHandler<H> handler;
private HandlerInfo handlerInfo;
private int yShift = 0;
public static class ItemRecipeFilter implements IRecipeFilter {
public ItemFilter filter;
public ItemRecipeFilter(ItemFilter filter) {
this.filter = filter;
}
@Override
public boolean matches(IRecipeHandler handler, List<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
if (matchPositionedStack(ingredients)) {
return true;
}
if (matchPositionedStack(result)) {
return true;
}
if (matchPositionedStack(others)) {
return true;
}
return false;
}
private boolean matchPositionedStack(List<PositionedStack> items) {
for (PositionedStack pStack : items) {
if (matchPositionedStack(pStack)) {
return true;
}
}
return false;
}
private boolean matchPositionedStack(PositionedStack pStack) {
if (pStack == null) return false;
for (ItemStack stack : pStack.items) {
if (filter.matches(stack)) {
return true;
}
}
return false;
}
}
public static class AllMultiRecipeFilter implements IRecipeFilter {
public List<IRecipeFilter> filters;
public AllMultiRecipeFilter(List<IRecipeFilter> filters) {
this.filters = filters;
}
public AllMultiRecipeFilter(IRecipeFilter filters) {
this(Arrays.asList(filters));
}
public AllMultiRecipeFilter() {
this(new ArrayList<>());
}
@Override
public boolean matches(IRecipeHandler handler, List<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
for (IRecipeFilter filter : filters) {
try {
if (filter != null && !filter.matches(handler, ingredients, result, others)) return false;
} catch (Exception e) {
NEIClientConfig.logger.error("Exception filtering " + handler + " with " + filter, e);
}
}
return true;
}
}
protected static final RestartableTask updateFilter = new RestartableTask("NEI Recipe Filtering") {
@Override
public void execute() {
final GuiScreen currentScreen = NEIClientUtils.mc().currentScreen;
if (currentScreen instanceof GuiRecipe) {
final GuiRecipe<?> guiRecipe = (GuiRecipe<?>) currentScreen;
final SearchRecipeHandler<?> searchHandler = guiRecipe.handler;
if (searchHandler != null && searchHandler.searchingAvailable()) {
if (GuiRecipe.searchField.text().isEmpty()) {
searchHandler.setSearchIndices(null);
guiRecipe.changePage(0);
} else {
final IRecipeFilter filter = new ItemRecipeFilter(GuiRecipe.searchField.getFilter());
ArrayList<Integer> filtered = searchHandler.getSearchResult(filter);
if (filtered == null) {
stop();
}
if (interrupted()) return;
searchHandler.setSearchIndices(filtered);
guiRecipe.changePage(0);
}
}
}
}
};
protected static final RecipeSearchField searchField = new RecipeSearchField("") {
@Override
protected boolean noResults() {
GuiScreen currentScreen = NEIClientUtils.mc().currentScreen;
return !(currentScreen instanceof GuiRecipe) || ((GuiRecipe<?>) currentScreen).numRecipes() > 0;
}
@Override
public void onTextChange(String oldText) {
updateFilter.restart();
}
};
protected static final Button toggleSearch = new Button() {
@Override
public boolean onButtonPress(boolean rightclick) {
if (rightclick) return false;
if (searchField.isVisible()) {
searchField.setText("");
searchField.setFocus(false);
searchField.setVisible(false);
state = 0;
} else {
searchField.setVisible(true);
searchField.setFocus(true);
state = 2;
}
return true;
}
};
/**
* This will only be true iff height hacking has been configured for the current recipe handler AND we are currently
* within the scope of an active {@link CompatibilityHacks} instance.
*/
private boolean isHeightHackApplied = false;
protected GuiRecipe(GuiScreen prevgui) {
super(new ContainerRecipe());
recipeTabs = new GuiRecipeTabs(this);
guiRecipeCatalyst = new GuiRecipeCatalyst(this);
slotcontainer = (ContainerRecipe) inventorySlots;
this.prevGui = prevgui;
this.firstGuiGeneral = prevgui;
if (prevgui instanceof GuiContainer) {
this.firstGui = (GuiContainer) prevgui;
}
if (prevgui instanceof IGuiContainerOverlay) {
this.firstGui = ((IGuiContainerOverlay) prevgui).getFirstScreen();
this.firstGuiGeneral = ((IGuiContainerOverlay) prevgui).getFirstScreenGeneral();
}
}
/** Changes the GUI mode to only display one recipe at a time, e.g. for tooltip usage */
public void limitToOneRecipe() {
limitToOneRecipe = true;
}
/** Checks if the gui only displays one recipe at a time, e.g. for tooltip usage */
public boolean isLimitedToOneRecipe() {
return limitToOneRecipe;
}
/**
* Many old mods assumed a fixed NEI window height of {@code 166} pixels. Now that this is no longer the case, their
* tooltip and click zone handling is broken. This helper class fixes these old mods by hacking the {@link #height}
* property's value so that these old mods' calculations return the correct value for the new height.
*
* <p>
* New and old mods often assume that the handler is only drawn when {@link GuiRecipe} is the currently active
* screen in {@link net.minecraft.client.Minecraft}. This is no longer true since the addition of recipe tooltips
* which need to render the recipe in other containers on mouse hover, this class temporarily switches the active
* screen to {@code this} while rendering them to avoid ClassCastExceptions and other similar crashes.
*
* <p>
* This class is an {@link AutoCloseable} so that it can be used with try-with-resources, which will ensure that
* {@link #height} and {@link net.minecraft.client.Minecraft#currentScreen} is returned to the correct value
* afterwards.
*/
private class CompatibilityHacks implements AutoCloseable {
private final int trueHeight;
private final int trueGuiTop;
private final GuiScreen trueGui;
private CompatibilityHacks() {
trueHeight = height;
trueGuiTop = guiTop;
isHeightHackApplied = NEIClientConfig.heightHackHandlerRegex.stream()
.map(pattern -> pattern.matcher(handler.original.getHandlerId())).anyMatch(Matcher::matches);
if (isHeightHackApplied) {
// guiTop is the top edge of the recipe screen on the y-axis. Old recipe handlers expect a single paging
// widget at the top and at the bottom of the recipe screen with a height of 16px each, but GTNH NEI
// moved the bottom paging widget to the top following JEI's design, increasing the "forbidden" zone at
// the top to 32px. To fix these old recipe handlers we move guiTop down 16px so that they can keep
// working with the old 16px gap.
guiTop += 16;
// The old NEI recipe screen had a fixed width and height and was always centered on the screen. Legacy
// recipe handlers use the calculation ((height - 166) / 2) to compute the y-value of the top edge of
// the NEI window. GTNH NEI changes the layout so that the recipe screen length now has a flexible
// length and is no longer centered vertically. In order for the top-of-screen calculation to return the
// correct result, we have to hack the height field with an inverse of the calculation using the actual
// top of the recipe screen stored in guiTop.
height = (2 * guiTop) + 166;
// For reference, in case we ever need to modify width as well:
// the legacy calculation used for width is ((width - 176) / 2), which should evaluate to be equal to
// guiWidth (the x-value of the left edge of the NEI recipe screen). So if we wanted to override width
// as well, we'd do this:
// width = (2 * guiWidth) + 176;
}
// Recipe handlers may assume the current screen is the GuiRecipe object, which is not the case in
// recipe tooltips drawn on the bookmarks panel with the main inventory open.
trueGui = NEIClientUtils.mc().currentScreen;
if (limitToOneRecipe) {
NEIClientUtils.mc().currentScreen = GuiRecipe.this;
}
}
@Override
public void close() {
guiTop = trueGuiTop;
height = trueHeight;
// Only restore currentScreen if it hasn't been altered by a recipe handler (for example through
// Minecraft#displayGuiScreen).
if (limitToOneRecipe && NEIClientUtils.mc().currentScreen == GuiRecipe.this) {
NEIClientUtils.mc().currentScreen = trueGui;
}
isHeightHackApplied = false;
}
}
@SuppressWarnings("unchecked")
public void initGui() {
xSize = 176;
ySize = Math.min(Math.max(height - 68, 166), 370);
if (!this.limitToOneRecipe) {
super.initGui();
} else {
this.guiLeft = (this.width - this.xSize) / 2;
this.ySize = this.getHeightAsWidget();
this.mc = NEIClientUtils.mc();
this.fontRendererObj = mc.fontRenderer;
ScaledResolution scaledresolution = new ScaledResolution(
this.mc,
this.mc.displayWidth,
this.mc.displayHeight);
this.width = scaledresolution.getScaledWidth();
this.height = scaledresolution.getScaledHeight();
}
guiTop = (height - ySize) / 2 + 10;
if (handler == null) {
setRecipePage(recipetype);
} else {
initOverlayButtons();
}
checkYShift();
final int rightButtonX = guiLeft + xSize - borderPadding - buttonWidth;
final int leftButtonX = guiLeft + borderPadding;
nexttype = new GuiNEIButton(0, leftButtonX, guiTop + 3, buttonWidth, buttonHeight, "<");
prevtype = new GuiNEIButton(1, rightButtonX, guiTop + 3, buttonWidth, buttonHeight, ">");
nextpage = new GuiNEIButton(2, leftButtonX, guiTop + 17, buttonWidth, buttonHeight, "<");
prevpage = new GuiNEIButton(3, rightButtonX, guiTop + 17, buttonWidth, buttonHeight, ">");
toggleSearch.icon = new DrawableBuilder("nei:textures/nei_sprites.png", 0, 38, 8, 8).setTextureSize(128, 128)
.build();
buttonList.addAll(Arrays.asList(nexttype, prevtype, nextpage, prevpage));
if (currenthandlers.size() == 1) {
nexttype.visible = false;
prevtype.visible = false;
}
recipeTabs.initLayout();
refreshPage();
}
@SuppressWarnings("unchecked")
private void initOverlayButtons() {
if (overlayButtons != null) {
buttonList.removeIf(Arrays.asList(overlayButtons)::contains);
}
final int recipesPerPage = getRecipesPerPage();
overlayButtons = new GuiButton[recipesPerPage];
final int xOffset = limitToOneRecipe ? ((xSize / 2) - 2) : (width / 2);
final int yOffset = limitToOneRecipe ? 0 : guiTop - 18;
final String overlayKeyName = NEIClientConfig
.getKeyName(NEIClientConfig.getKeyBinding("gui.overlay_use"), true);
for (int i = 0; i < recipesPerPage; i++) {
overlayButtons[i] = new GuiNEIButton(
OVERLAY_BUTTON_ID_START + i,
xOffset + 65,
yOffset + getRecipePosition(i).y + handlerInfo.getHeight(),
buttonWidth,
buttonHeight,
"?");
if (overlayKeyName != null && limitToOneRecipe) {
overlayButtons[i].visible = true;
overlayButtons[i].displayString = overlayKeyName;
overlayButtons[i].width = fontRenderer.getStringWidth(overlayKeyName) + 6;
overlayButtons[i].xPosition = xOffset + 85 - overlayButtons[i].width;
}
}
Collections.addAll(buttonList, overlayButtons);
itemPresenceCacheRecipe = -1;
}
public static IRecipeFilter getRecipeListFilter() {
if (recipeFilterers.isEmpty()) {
return null;
}
final AllMultiRecipeFilter recipeFilter = new AllMultiRecipeFilter();
synchronized (recipeFilterers) {
for (IRecipeFilterProvider p : recipeFilterers) {
IRecipeFilter filter = p.getFilter();
if (filter != null) {
recipeFilter.filters.add(filter);
}
}
}
return recipeFilter.filters.size() == 1 ? recipeFilter.filters.get(0) : recipeFilter;
}
private void checkYShift() {
yShift = handlerInfo == null ? 0 : handlerInfo.getYShift();
}
public void setRecipePage(int idx) {
setRecipePage(idx, 0);
}
public void setRecipePage(int idx, int refIndex) {
recipetype = (currenthandlers.size() + idx) % currenthandlers.size();
handler = new SearchRecipeHandler<>(currenthandlers.get(recipetype));
handlerInfo = GuiRecipeTab.getHandlerInfo(handler.original);
if (!limitToOneRecipe) {
searchField.setText("");
searchField.setVisible(false);
toggleSearch.state = 0;
}
page = Math.min(Math.max(0, refIndex), this.numRecipes() - 1) / getRecipesPerPage();
changePage(0);
recipeTabs.calcPageNumber();
checkYShift();
initOverlayButtons();
}
public List<GuiButton> getOverlayButtons() {
return Collections.unmodifiableList(Arrays.asList(overlayButtons));
}
public int openTargetRecipe(BookmarkRecipeId recipeId) {
int refIndex = -1;
int recipetype = 0;
this.recipeId = recipeId;
if (this.recipeId != null) {
for (int j = 0; j < currenthandlers.size(); j++) {
H localHandler = currenthandlers.get(j);
HandlerInfo localHandlerInfo = GuiRecipeTab.getHandlerInfo(localHandler);
if (localHandlerInfo.getHandlerName().equals(this.recipeId.handlerName)) {
recipetype = j;
if (!this.recipeId.ingredients.isEmpty()) {
refIndex = SearchRecipeHandler.findFirst(
localHandler,
(recipeIndex) -> this.recipeId
.equalsIngredients(localHandler.getIngredientStacks(recipeIndex)));
}
break;
}
}
}
setRecipePage(recipetype, Math.max(0, refIndex));
return refIndex;
}
public List<PositionedStack> getFocusedRecipeIngredients() {
for (int recipeIndex : getRecipeIndices()) {
if (recipeInFocus(recipeIndex)) {
return handler.original.getIngredientStacks(recipeIndex);
}
}
return null;
}
public int prepareFocusedRecipeResultStackSize(ItemStack stackover) {
for (int recipeIndex : getRecipeIndices()) {
if (recipeInFocus(recipeIndex)) {
final PositionedStack result = handler.original.getResultStack(recipeIndex);
int stackSize = 0;
if (result != null && StackInfo.equalItemAndNBT(result.item, stackover, true)) {
stackSize += result.item.stackSize;
}
final List<PositionedStack> stacks = handler.original.getOtherStacks(recipeIndex);
for (PositionedStack pStack : stacks) {
if (StackInfo.equalItemAndNBT(pStack.item, stackover, true)) {
stackSize += pStack.item.stackSize;
}
}
return stackSize;
}
}
return stackover.stackSize;
}
protected boolean recipeInFocus(int recipeIndex) {
final PositionedStack result = handler.original.getResultStack(recipeIndex);
if (result != null && isMouseOver(result, recipeIndex)) {
return true;
}
final List<PositionedStack> stacks = handler.original.getOtherStacks(recipeIndex);
for (PositionedStack stack : stacks) {
if (isMouseOver(stack, recipeIndex)) {
return true;
}
}
return false;
}
public String getHandlerName() {
return handlerInfo.getHandlerName();
}
public H getHandler() {
return handler.original;
}
public List<Integer> getRecipeIndices() {
final int recipesPerPage = getRecipesPerPage();
final int minIndex = page * recipesPerPage;
final int maxIndex = Math.min(this.numRecipes(), (page + 1) * recipesPerPage);
final ArrayList<Integer> range = new ArrayList<>();
for (int index = minIndex; index < maxIndex; index++) {
range.add(handler.ref(index));
}
return range;
}
private int numRecipes() {
return handler == null ? 0 : handler.numRecipes();
}
@Override
public void keyTyped(char c, int i) {
if (searchField.isVisible() && searchField.focused() && searchField.handleKeyPress(i, c)) {
return;
}
if (i == Keyboard.KEY_ESCAPE) { // esc
mc.displayGuiScreen(firstGuiGeneral);
NEICPH.sendRequestContainer();
return;
}
if (searchField.isVisible() && searchField.focused()) {
searchField.lastKeyTyped(i, c);
return;
}
if (GuiContainerManager.getManager(this).lastKeyTyped(i, c)) {
return;
}
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
for (int recipeIndex : getRecipeIndices()) {
if (handler.original.keyTyped(this, c, i, recipeIndex)) {
return;
}
}
}
if (i == mc.gameSettings.keyBindInventory.getKeyCode()) {
mc.displayGuiScreen(firstGuiGeneral);
NEICPH.sendRequestContainer();
} else if (NEIClientConfig.isKeyHashDown("gui.back")) {
mc.displayGuiScreen(prevGui);
} else if (NEIClientConfig.isKeyHashDown("gui.prev_machine")) {
prevType();
} else if (NEIClientConfig.isKeyHashDown("gui.next_machine")) {
nextType();
} else if (NEIClientConfig.isKeyHashDown("gui.prev_recipe")) {
prevPage();
} else if (NEIClientConfig.isKeyHashDown("gui.next_recipe")) {
nextPage();
}
}
@Override
protected void mouseClicked(int mousex, int mousey, int button) {
if (!limitToOneRecipe && handler != null && handler.searchingAvailable()) {
if (toggleSearch.contains(mousex - guiLeft, mousey - guiTop)) {
toggleSearch.handleClick(mousex - guiLeft, mousey - guiTop, button);
} else if (searchField.contains(mousex - guiLeft, mousey - guiTop)) {
searchField.handleClick(mousex - guiLeft, mousey - guiTop, button);
} else {
searchField.onGuiClick(mousex - guiLeft, mousey - guiTop);
}
}
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
for (int recipeIndex : getRecipeIndices()) {
if (handler.original.mouseClicked(this, button, recipeIndex)) {
return;
}
}
}
if (recipeTabs.mouseClicked(mousex, mousey, button)) {
return;
}
super.mouseClicked(mousex, mousey, button);
}
@Override
public void mouseScrolled(int scroll) {
// Height hacking is not necessary here since mouse scrolling is a new feature, added in
// GTNH NEI. So no old mods will use this. Though not hacking the height here does mean that
// the value of the height field will be different from in other mouseover methods, which
// could be confusing...
// First, invoke scroll handling over recipe handler tabbar. Makes sure it is not overwritten by recipe
// handler-specific scroll behavior.
if (recipeTabs.mouseScrolled(scroll)) return;
for (int recipeIndex : getRecipeIndices()) {
if (handler.original.mouseScrolled(this, scroll, recipeIndex)) {
return;
}
}
// If shift is held, try switching to the next recipe handler. Replicates the GuiRecipeTabs.mouseScrolled()
// without the checking for the cursor being inside the tabbar.
if (NEIClientUtils.shiftKey()) {
if (scroll < 0) {
nextType();
} else {
prevType();
}
return;
}
// Finally, if nothing else has handled scrolling, try changing to the next recipe page.
if (new Rectangle(guiLeft, guiTop, xSize, ySize).contains(GuiDraw.getMousePosition())) {
if (scroll > 0) {
prevPage();
} else {
nextPage();
}
}
}
@Override
protected void actionPerformed(GuiButton guibutton) {
super.actionPerformed(guibutton);
switch (guibutton.id) {
case 0:
prevType();
return;
case 1:
nextType();
return;
case 2:
prevPage();
return;
case 3:
nextPage();
return;
}
if (overlayButtons != null && guibutton.id >= OVERLAY_BUTTON_ID_START
&& guibutton.id < OVERLAY_BUTTON_ID_START + overlayButtons.length) {
final List<Integer> indices = getRecipeIndices();
final int refIndex = guibutton.id - OVERLAY_BUTTON_ID_START;
if (refIndex >= 0 && refIndex < indices.size()) {
mc.displayGuiScreen(firstGui);
overlayRecipe(indices.get(refIndex), NEIClientUtils.shiftKey());
}
}
}
@Override
public void updateScreen() {
super.updateScreen();
updateAsTooltip();
}
/** updateScreen() that's safe to call when this isn't the currently open GUI */
public void updateAsTooltip() {
handler.original.onUpdate();
refreshPage();
}
@Override
public List<String> handleTooltip(GuiContainer gui, int mousex, int mousey, List<String> currenttip) {
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
for (int recipeIndex : getRecipeIndices()) {
currenttip = handler.original.handleTooltip(this, currenttip, recipeIndex);
}
}
recipeTabs.handleTooltip(mousex, mousey, currenttip);
if (currenttip.isEmpty() && searchField.isVisible()
&& new Rectangle(searchField.x + searchField.w, 15, 44, 16)
.contains(mousex - guiLeft, mousey - guiTop)) {
final String s = String.format("%d/%d", page + 1, (this.numRecipes() - 1) / getRecipesPerPage() + 1);
if (fontRendererObj.getStringWidth(s) >= 45) {
currenttip.add(s);
}
}
return currenttip;
}
@Override
public List<String> handleItemTooltip(GuiContainer gui, ItemStack stack, int mousex, int mousey,
List<String> currenttip) {
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
for (int recipeIndex : getRecipeIndices()) {
currenttip = handler.original.handleItemTooltip(this, stack, currenttip, recipeIndex);
}
}
return currenttip;
}
@Override
public List<String> handleItemDisplayName(GuiContainer gui, ItemStack itemstack, List<String> currenttip) {
return currenttip;
}
private void nextPage() {
changePage(1);
}
private void prevPage() {
changePage(-1);
}
protected void nextType() {
setRecipePage(++recipetype);
}
protected void prevType() {
setRecipePage(--recipetype);
}
protected void overlayRecipe(int recipeIndex, final boolean shift) {
if (handler == null || !handler.original.hasOverlay(firstGui, firstGui.inventorySlots, recipeIndex)) {
return;
}
final boolean moveItems = shift || !NEIClientConfig.requireShiftForOverlayRecipe();
final IRecipeOverlayRenderer renderer = handler.original.getOverlayRenderer(firstGui, recipeIndex);
final IOverlayHandler overlayHandler = handler.original.getOverlayHandler(firstGui, recipeIndex);
if (renderer == null || moveItems) {
if (overlayHandler != null) {
overlayHandler.overlayRecipe(firstGui, handler.original, recipeIndex, moveItems);
}
} else {
LayoutManager.overlayRenderer = renderer;
}
}
protected void changePage(int shift) {
final int recipesPerPage = getRecipesPerPage();
final int numRecipes = this.numRecipes();
if (numRecipes > 0) {
final int numPages = (int) Math.ceil(numRecipes / (float) recipesPerPage);
this.page = Math.min(Math.max(0, this.page), numPages);
this.page = (numPages + this.page + shift) % numPages;
} else {
this.page = 0;
}
}
public void refreshPage() {
RecipeCatalysts.updatePosition(ySize - BG_TOP_HEIGHT - (GuiRecipeCatalyst.fullBorder * 2));
changePage(0);
refreshSlots();
final int recipesPerPage = getRecipesPerPage();
final boolean multiplepages = this.numRecipes() > recipesPerPage;
final int numRecipes = Math.min(this.numRecipes() - (page * recipesPerPage), recipesPerPage);
area.width = handlerInfo.getWidth();
area.height = handlerInfo.getHeight() * numRecipes;
area.x = guiLeft - 2;
area.y = guiTop - 4 + yShift;
checkYShift();
if (!limitToOneRecipe) {
toggleSearch.w = toggleSearch.h = 12;
toggleSearch.x = borderPadding + buttonWidth;
toggleSearch.y = 17;
searchField.y = 16;
searchField.x = borderPadding + buttonWidth + toggleSearch.w - 1;
searchField.w = xSize - (borderPadding + buttonWidth) * 2 + 1 - toggleSearch.w - 45;
searchField.h = 14;
}
nextpage.enabled = prevpage.enabled = multiplepages;
if (firstGui == null) {
for (GuiButton overlay : overlayButtons) {
overlay.visible = false;
}
} else {
final List<Integer> indices = getRecipeIndices();
for (int i = 0; i < overlayButtons.length; i++) {
if (i >= indices.size()) {
overlayButtons[i].visible = false;
} else {
overlayButtons[i].visible = handler.original
.hasOverlay(firstGui, firstGui.inventorySlots, indices.get(i));
}
}
}
recipeTabs.refreshPage();
}
private void refreshSlots() {
slotcontainer.inventorySlots.clear();
final List<Integer> indices = getRecipeIndices();
for (int refIndex = 0; refIndex < indices.size(); refIndex++) {
int recipeIndex = indices.get(refIndex);
Point p = getRecipePosition(refIndex);
// Legacy recipe handlers only expect a single paging widget at the top of the recipe screen, in contrast,
// GTNH NEI moves the recipe paging widget from the bottom to the top, which means said legacy handlers will
// position item slots 16px too high in the screen.
if (isHeightHackApplied) {
p.translate(0, 16);
}
TemplateRecipeHandler.disableCycledIngredients = false;
List<PositionedStack> stacks = handler.original.getIngredientStacks(recipeIndex);
for (PositionedStack stack : stacks) slotcontainer.addSlot(stack, p.x, p.y);
stacks = handler.original.getOtherStacks(recipeIndex);
for (PositionedStack stack : stacks) slotcontainer.addSlot(stack, p.x, p.y);
PositionedStack result = handler.original.getResultStack(recipeIndex);
if (result != null) slotcontainer.addSlot(result, p.x, p.y);
if (!limitToOneRecipe) {
List<PositionedStack> catalysts = RecipeCatalysts.getRecipeCatalysts(handler.original);
for (PositionedStack catalyst : catalysts) {
int xOffset = -GuiRecipeCatalyst.ingredientSize + 1;
int yOffset = BG_TOP_Y + GuiRecipeCatalyst.fullBorder;
slotcontainer.addSlot(catalyst, xOffset, yOffset);
}
}
TemplateRecipeHandler.disableCycledIngredients = true;
}
}
private int itemPresenceCacheRecipe = -1;
private ArrayList<Boolean> itemPresenceCacheSlots;
private void updateItemPresenceCache(int recipeIndex) {
if (itemPresenceCacheSlots == null) {
itemPresenceCacheSlots = new ArrayList<>();
}
itemPresenceCacheRecipe = recipeIndex;
itemPresenceCacheSlots.clear();
List<PositionedStack> ingredients = handler.original.getIngredientStacks(recipeIndex);
ArrayList<ItemStack> invStacks = ((List<Slot>) firstGui.inventorySlots.inventorySlots).stream()
.filter(
s -> s != null && s.getStack() != null
&& s.getStack().stackSize > 0
&& s.isItemValid(s.getStack())
&& s.canTakeStack(mc.thePlayer))
.map(s -> s.getStack().copy()).collect(Collectors.toCollection(ArrayList::new));
for (PositionedStack stack : ingredients) {
Optional<ItemStack> used = invStacks.stream().filter(is -> is.stackSize > 0 && stack.contains(is))
.findFirst();
itemPresenceCacheSlots.add(used.isPresent());
if (used.isPresent()) {
ItemStack is = used.get();
is.stackSize -= 1;
}
}
}
@Override
public void drawGuiContainerForegroundLayer(int mouseX, int mouseY) {
GuiContainerManager.enable2DRender();
final int recipesPerPage = getRecipesPerPage();
final int ySkip = limitToOneRecipe ? 25 : 0;
if (!limitToOneRecipe) {
String s = handler.original.getRecipeName().trim();
fontRendererObj.drawStringWithShadow(s, (xSize - fontRendererObj.getStringWidth(s)) / 2, 5, 0xffffff);
if (handler.searchingAvailable()) {
toggleSearch.draw(mouseX - guiLeft, mouseY - guiTop);
}
if (searchField.isVisible()) {
searchField.draw(mouseX - guiLeft, mouseY - guiTop);
s = NEIClientUtils.cropText(
fontRendererObj,
String.format("%d/%d", page + 1, (this.numRecipes() - 1) / recipesPerPage + 1),
45);
fontRendererObj.drawStringWithShadow(
s,
searchField.x + searchField.w + (44 - fontRendererObj.getStringWidth(s)) / 2,
19,
0xffffff);
} else {
s = NEIClientUtils.translate("recipe.page", page + 1, (this.numRecipes() - 1) / recipesPerPage + 1);
fontRendererObj.drawStringWithShadow(s, (xSize - fontRendererObj.getStringWidth(s)) / 2, 19, 0xffffff);
}
}
final boolean drawItemPresence = NEIClientConfig.isJEIStyleItemPresenceOverlayVisible();
final boolean slotHighlight = NEIClientConfig.isSlotHighlightEnabled();
GL11.glPushMatrix();
GL11.glTranslatef(5, 32 - ySkip + yShift, 0);
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
List<Integer> indices = getRecipeIndices();
for (int refIndex = 0; refIndex < indices.size(); refIndex++) {
int recipeIndex = indices.get(refIndex);
handler.original.drawForeground(recipeIndex);
if (drawItemPresence && firstGui != null
&& firstGui.inventorySlots != null
&& (isMouseOverOverlayButton(refIndex) || limitToOneRecipe)) {
List<PositionedStack> ingredients = handler.original.getIngredientStacks(recipeIndex);
if (itemPresenceCacheRecipe != recipeIndex || itemPresenceCacheSlots == null
|| itemPresenceCacheSlots.size() != ingredients.size()) {
updateItemPresenceCache(recipeIndex);
}
for (int j = 0; j < ingredients.size(); j++) {
PositionedStack stack = ingredients.get(j);
boolean isPresent = itemPresenceCacheSlots.get(j);
LayoutManager.drawItemPresenceOverlay(stack.relx, stack.rely, isPresent, slotHighlight);
}
}
GL11.glTranslatef(0, handlerInfo.getHeight(), 0);
}
}
GL11.glPopMatrix();
}
@Override
public void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) {
final int ySkip = limitToOneRecipe ? 25 : 0;
GL11.glColor4f(1, 1, 1, 1);
drawBackgroundTiled(guiLeft, guiTop);
if (NEIClientConfig.areJEIStyleTabsVisible() && !limitToOneRecipe) {
drawRect(
guiLeft + borderPadding + buttonWidth - 1,
nexttype.yPosition,
guiLeft + xSize - borderPadding - buttonWidth,
nexttype.yPosition + buttonHeight,
0x30000000);
drawRect(
guiLeft + borderPadding + buttonWidth - 1,
nextpage.yPosition,
guiLeft + xSize - borderPadding - buttonWidth,
nextpage.yPosition + buttonHeight,
0x30000000);
RenderHelper.enableGUIStandardItemLighting();
OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
recipeTabs.draw(mouseX, mouseY);
if (NEIClientConfig.areJEIStyleRecipeCatalystsVisible()) {
guiRecipeCatalyst.draw();
}
RenderHelper.disableStandardItemLighting();
}
GL11.glPushMatrix();
GL11.glTranslatef(guiLeft + 5, guiTop - ySkip + 32 + yShift, 0);
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
for (int recipeIndex : getRecipeIndices()) {
handler.original.drawBackground(recipeIndex);
GL11.glTranslatef(0, handlerInfo.getHeight(), 0);
}
}
GL11.glPopMatrix();
}
private void drawBackgroundTiled(int j, int k) {
// Draw the top
final int ySkip = limitToOneRecipe ? 25 : 0;
final int handlerHeight = limitToOneRecipe ? getHeightAsWidget() : ySize;
bgTop.draw(j, k + BG_TOP_Y);
// Maybe Tile the middle
final int tiledHeight = handlerHeight - ySkip - BG_TOP_HEIGHT - BG_BOTTOM_HEIGHT;
if (tiledHeight > 0) {
final int yTileCount = tiledHeight / BG_MIDDLE_HEIGHT;
final int yRemainder = tiledHeight - (yTileCount * BG_MIDDLE_HEIGHT);
final int yStart = k + BG_TOP_HEIGHT + tiledHeight;
for (int yTile = 0; yTile <= yTileCount; yTile++) {
final int tileHeight = (yTile == yTileCount) ? yRemainder : BG_MIDDLE_HEIGHT;
final int y = yStart - ((yTile + 1) * BG_MIDDLE_HEIGHT);
if (tileHeight > 0) {
bgMiddle.draw(j, y, BG_MIDDLE_HEIGHT - tileHeight, 0, 0, 0);
}
}
}
// Draw the bottom
bgBottom.draw(j, k + handlerHeight - ySkip - BG_BOTTOM_HEIGHT);
}
public int getHeightAsWidget() {
return (handlerInfo == null) ? ySize : (handlerInfo.getHeight() + 56);
}
@Override
public GuiContainer getFirstScreen() {
return firstGui;
}
@Override
public GuiScreen getFirstScreenGeneral() {
return firstGuiGeneral;
}
public boolean isMouseOver(PositionedStack stack, int refIndex) {
Point p = getRecipePosition(refIndex);
Point mousepos = GuiDraw.getMousePosition();
Slot stackSlot = slotcontainer.getSlotWithStack(stack, p.x, p.y);
Slot mouseoverSlot = getSlotAtPosition(mousepos.x, mousepos.y);
return stackSlot == mouseoverSlot;
}
private boolean isMouseOverOverlayButton(int buttonId) {
if (buttonId >= 0 && buttonId < overlayButtons.length) {
GuiButton button = overlayButtons[buttonId];
Point mousePos = GuiDraw.getMousePosition();
return button.visible && mousePos.x >= button.xPosition
&& mousePos.y >= button.yPosition
&& mousePos.x < button.xPosition + button.width
&& mousePos.y < button.yPosition + button.height;
} else {
return false;
}
}
private int getRecipesPerPage() {
return limitToOneRecipe ? 1 : getRecipesPerPage(handlerInfo);
}
private int getRecipesPerPage(HandlerInfo handlerInfo) {
if (handlerInfo != null) {
return Math.max(
Math.min(
((ySize - (buttonHeight * 3)) / handlerInfo.getHeight()),
handlerInfo.getMaxRecipesPerPage()),
1);
} else if (handler != null) {
return handler.original.recipiesPerPage();
}
return 1;
}
public Point getRecipePosition(int refIndex) {
// Legacy recipe handlers using the height hack might use getRecipePosition in combination with guiTop/height to
// position certain elements like tooltips. Since guiTop is moved down by 16px during height hacking, we need to
// reduce the vertical shift here to 16px instead of 32px.
return new Point(
5,
(isHeightHackApplied ? 16 : 32) - (limitToOneRecipe ? 25 : 0)
+ yShift
+ ((refIndex % getRecipesPerPage()) * handlerInfo.getHeight()));
}
public abstract ArrayList<H> getCurrentRecipeHandlers();
@Override
public VisiblityData modifyVisiblity(GuiContainer gui, VisiblityData currentVisibility) {
return currentVisibility;
}
@Override
public Iterable<Integer> getItemSpawnSlots(GuiContainer gui, ItemStack item) {
return Collections.emptyList();
}
@Override
public List<TaggedInventoryArea> getInventoryAreas(GuiContainer gui) {
return null;
}
@Override
public boolean handleDragNDrop(GuiContainer gui, int mousex, int mousey, ItemStack draggedStack, int button) {
if (searchField.isVisible() && searchField.contains(mousex - guiLeft, mousey - guiTop)) {
final FluidStack fluidStack = StackInfo.getFluid(draggedStack);
if (fluidStack != null) {
searchField.setText(formattingText(fluidStack.getLocalizedName()));
} else {
searchField.setText(formattingText(draggedStack.getDisplayName()));
}
return true;
}
return false;
}
protected String formattingText(String displayName) {
return Pattern.compile("[{}()\\[\\].+*?^$\\\\|]")
.matcher(EnumChatFormatting.getTextWithoutFormattingCodes(displayName)).replaceAll("\\\\$0");
}
@Override
public boolean hideItemPanelSlot(GuiContainer gui, int x, int y, int w, int h) {
// Because some of the handlers *cough avaritia* are oversized
return area.intersects(x, y, w, h);
}
protected static BookmarkRecipeId getCurrentRecipe(GuiScreen gui) {
if (gui instanceof GuiRecipe && ((GuiRecipe<?>) gui).handler.numRecipes() > 0) {
final GuiRecipe<?> gRecipe = (GuiRecipe<?>) gui;
final List<Integer> indices = gRecipe.getRecipeIndices();
final int curRecipe = indices.isEmpty() ? 0 : indices.get(0);
return new BookmarkRecipeId(
gRecipe.handlerInfo.getHandlerName(),
gRecipe.handler.original.getIngredientStacks(curRecipe));
}
return null;
}
}