package net.minecraft.client.gui.screens.recipebook; import com.google.common.collect.Lists; import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.ChatFormatting; import net.minecraft.client.ClientRecipeBook; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.components.StateSwitchingButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.WidgetSprites; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.narration.NarratableEntry.NarrationPriority; import net.minecraft.client.gui.navigation.CommonInputs; import net.minecraft.client.gui.navigation.ScreenAxis; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen.NarratableSearchResult; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.resources.language.LanguageInfo; import net.minecraft.client.resources.language.LanguageManager; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.ServerboundRecipeBookChangeSettingsPacket; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import net.minecraft.util.context.ContextMap; import net.minecraft.world.entity.player.StackedItemContents; import net.minecraft.world.inventory.AbstractFurnaceMenu; import net.minecraft.world.inventory.RecipeBookMenu; import net.minecraft.world.inventory.RecipeBookType; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.ExtendedRecipeBookCategory; import net.minecraft.world.item.crafting.RecipeBookCategory; import net.minecraft.world.item.crafting.display.RecipeDisplay; import net.minecraft.world.item.crafting.display.RecipeDisplayId; import net.minecraft.world.item.crafting.display.SlotDisplayContext; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; @Environment(EnvType.CLIENT) public abstract class RecipeBookComponent implements Renderable, GuiEventListener, NarratableEntry { public static final WidgetSprites RECIPE_BUTTON_SPRITES = new WidgetSprites( ResourceLocation.withDefaultNamespace("recipe_book/button"), ResourceLocation.withDefaultNamespace("recipe_book/button_highlighted") ); protected static final ResourceLocation RECIPE_BOOK_LOCATION = ResourceLocation.withDefaultNamespace("textures/gui/recipe_book.png"); private static final int BACKGROUND_TEXTURE_WIDTH = 256; private static final int BACKGROUND_TEXTURE_HEIGHT = 256; private static final Component SEARCH_HINT = Component.translatable("gui.recipebook.search_hint") .withStyle(ChatFormatting.ITALIC) .withStyle(ChatFormatting.GRAY); public static final int IMAGE_WIDTH = 147; public static final int IMAGE_HEIGHT = 166; private static final int OFFSET_X_POSITION = 86; private static final int BORDER_WIDTH = 8; private static final Component ALL_RECIPES_TOOLTIP = Component.translatable("gui.recipebook.toggleRecipes.all"); private static final int TICKS_TO_SWAP_SLOT = 30; private int xOffset; private int width; private int height; private float time; @Nullable private RecipeDisplayId lastPlacedRecipe; private final GhostSlots ghostSlots; private final List tabButtons = Lists.newArrayList(); @Nullable private RecipeBookTabButton selectedTab; protected StateSwitchingButton filterButton; protected final T menu; protected Minecraft minecraft; @Nullable private EditBox searchBox; private String lastSearch = ""; private final List tabInfos; private ClientRecipeBook book; private final RecipeBookPage recipeBookPage; @Nullable private RecipeDisplayId lastRecipe; @Nullable private RecipeCollection lastRecipeCollection; private final StackedItemContents stackedContents = new StackedItemContents(); private int timesInventoryChanged; private boolean ignoreTextInput; private boolean visible; private boolean widthTooNarrow; @Nullable private ScreenRectangle magnifierIconPlacement; public RecipeBookComponent(T menu, List tabInfos) { this.menu = menu; this.tabInfos = tabInfos; SlotSelectTime slotSelectTime = () -> Mth.floor(this.time / 30.0F); this.ghostSlots = new GhostSlots(slotSelectTime); this.recipeBookPage = new RecipeBookPage(this, slotSelectTime, menu instanceof AbstractFurnaceMenu); } public void init(int width, int height, Minecraft minecraft, boolean widthTooNarrow) { this.minecraft = minecraft; this.width = width; this.height = height; this.widthTooNarrow = widthTooNarrow; this.book = minecraft.player.getRecipeBook(); this.timesInventoryChanged = minecraft.player.getInventory().getTimesChanged(); this.visible = this.isVisibleAccordingToBookData(); if (this.visible) { this.initVisuals(); } } private void initVisuals() { boolean bl = this.isFiltering(); this.xOffset = this.widthTooNarrow ? 0 : 86; int i = this.getXOrigin(); int j = this.getYOrigin(); this.stackedContents.clear(); this.minecraft.player.getInventory().fillStackedContents(this.stackedContents); this.menu.fillCraftSlotsStackedContents(this.stackedContents); String string = this.searchBox != null ? this.searchBox.getValue() : ""; this.searchBox = new EditBox(this.minecraft.font, i + 25, j + 13, 81, 9 + 5, Component.translatable("itemGroup.search")); this.searchBox.setMaxLength(50); this.searchBox.setVisible(true); this.searchBox.setTextColor(16777215); this.searchBox.setValue(string); this.searchBox.setHint(SEARCH_HINT); this.magnifierIconPlacement = ScreenRectangle.of( ScreenAxis.HORIZONTAL, i + 8, this.searchBox.getY(), this.searchBox.getX() - this.getXOrigin(), this.searchBox.getHeight() ); this.recipeBookPage.init(this.minecraft, i, j); this.filterButton = new StateSwitchingButton(i + 110, j + 12, 26, 16, bl); this.updateFilterButtonTooltip(); this.initFilterButtonTextures(); this.tabButtons.clear(); for (RecipeBookComponent.TabInfo tabInfo : this.tabInfos) { this.tabButtons.add(new RecipeBookTabButton(tabInfo)); } if (this.selectedTab != null) { this.selectedTab = (RecipeBookTabButton)this.tabButtons .stream() .filter(recipeBookTabButton -> recipeBookTabButton.getCategory().equals(this.selectedTab.getCategory())) .findFirst() .orElse(null); } if (this.selectedTab == null) { this.selectedTab = (RecipeBookTabButton)this.tabButtons.get(0); } this.selectedTab.setStateTriggered(true); this.selectMatchingRecipes(); this.updateTabs(bl); this.updateCollections(false, bl); } private int getYOrigin() { return (this.height - 166) / 2; } private int getXOrigin() { return (this.width - 147) / 2 - this.xOffset; } private void updateFilterButtonTooltip() { this.filterButton.setTooltip(this.filterButton.isStateTriggered() ? Tooltip.create(this.getRecipeFilterName()) : Tooltip.create(ALL_RECIPES_TOOLTIP)); } protected abstract void initFilterButtonTextures(); public int updateScreenPosition(int width, int imageWidth) { int i; if (this.isVisible() && !this.widthTooNarrow) { i = 177 + (width - imageWidth - 200) / 2; } else { i = (width - imageWidth) / 2; } return i; } public void toggleVisibility() { this.setVisible(!this.isVisible()); } public boolean isVisible() { return this.visible; } private boolean isVisibleAccordingToBookData() { return this.book.isOpen(this.menu.getRecipeBookType()); } protected void setVisible(boolean visible) { if (visible) { this.initVisuals(); } this.visible = visible; this.book.setOpen(this.menu.getRecipeBookType(), visible); if (!visible) { this.recipeBookPage.setInvisible(); } this.sendUpdateSettings(); } protected abstract boolean isCraftingSlot(Slot slot); public void slotClicked(@Nullable Slot slot) { if (slot != null && this.isCraftingSlot(slot)) { this.lastPlacedRecipe = null; this.ghostSlots.clear(); if (this.isVisible()) { this.updateStackedContents(); } } } private void selectMatchingRecipes() { for (RecipeBookComponent.TabInfo tabInfo : this.tabInfos) { for (RecipeCollection recipeCollection : this.book.getCollection(tabInfo.category())) { this.selectMatchingRecipes(recipeCollection, this.stackedContents); } } } protected abstract void selectMatchingRecipes(RecipeCollection possibleRecipes, StackedItemContents stackedItemContents); private void updateCollections(boolean resetPageNumber, boolean isFiltering) { List list = this.book.getCollection(this.selectedTab.getCategory()); List list2 = Lists.newArrayList(list); list2.removeIf(recipeCollection -> !recipeCollection.hasAnySelected()); String string = this.searchBox.getValue(); if (!string.isEmpty()) { ClientPacketListener clientPacketListener = this.minecraft.getConnection(); if (clientPacketListener != null) { ObjectSet objectSet = new ObjectLinkedOpenHashSet<>(clientPacketListener.searchTrees().recipes().search(string.toLowerCase(Locale.ROOT))); list2.removeIf(recipeCollection -> !objectSet.contains(recipeCollection)); } } if (isFiltering) { list2.removeIf(recipeCollection -> !recipeCollection.hasCraftable()); } this.recipeBookPage.updateCollections(list2, resetPageNumber, isFiltering); } private void updateTabs(boolean isFiltering) { int i = (this.width - 147) / 2 - this.xOffset - 30; int j = (this.height - 166) / 2 + 3; int k = 27; int l = 0; for (RecipeBookTabButton recipeBookTabButton : this.tabButtons) { ExtendedRecipeBookCategory extendedRecipeBookCategory = recipeBookTabButton.getCategory(); if (extendedRecipeBookCategory instanceof SearchRecipeBookCategory) { recipeBookTabButton.visible = true; recipeBookTabButton.setPosition(i, j + 27 * l++); } else if (recipeBookTabButton.updateVisibility(this.book)) { recipeBookTabButton.setPosition(i, j + 27 * l++); recipeBookTabButton.startAnimation(this.book, isFiltering); } } } public void tick() { boolean bl = this.isVisibleAccordingToBookData(); if (this.isVisible() != bl) { this.setVisible(bl); } if (this.isVisible()) { if (this.timesInventoryChanged != this.minecraft.player.getInventory().getTimesChanged()) { this.updateStackedContents(); this.timesInventoryChanged = this.minecraft.player.getInventory().getTimesChanged(); } } } private void updateStackedContents() { this.stackedContents.clear(); this.minecraft.player.getInventory().fillStackedContents(this.stackedContents); this.menu.fillCraftSlotsStackedContents(this.stackedContents); this.selectMatchingRecipes(); this.updateCollections(false, this.isFiltering()); } private boolean isFiltering() { return this.book.isFiltering(this.menu.getRecipeBookType()); } @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { if (this.isVisible()) { if (!Screen.hasControlDown()) { this.time += partialTick; } guiGraphics.pose().pushPose(); guiGraphics.pose().translate(0.0F, 0.0F, 100.0F); int i = this.getXOrigin(); int j = this.getYOrigin(); guiGraphics.blit(RenderType::guiTextured, RECIPE_BOOK_LOCATION, i, j, 1.0F, 1.0F, 147, 166, 256, 256); this.searchBox.render(guiGraphics, mouseX, mouseY, partialTick); for (RecipeBookTabButton recipeBookTabButton : this.tabButtons) { recipeBookTabButton.render(guiGraphics, mouseX, mouseY, partialTick); } this.filterButton.render(guiGraphics, mouseX, mouseY, partialTick); this.recipeBookPage.render(guiGraphics, i, j, mouseX, mouseY, partialTick); guiGraphics.pose().popPose(); } } public void renderTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, @Nullable Slot slot) { if (this.isVisible()) { this.recipeBookPage.renderTooltip(guiGraphics, mouseX, mouseY); this.ghostSlots.renderTooltip(guiGraphics, this.minecraft, mouseX, mouseY, slot); } } protected abstract Component getRecipeFilterName(); public void renderGhostRecipe(GuiGraphics guiGraphics, boolean isBiggerResultSlot) { this.ghostSlots.render(guiGraphics, this.minecraft, isBiggerResultSlot); } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (this.isVisible() && !this.minecraft.player.isSpectator()) { if (this.recipeBookPage.mouseClicked(mouseX, mouseY, button, this.getXOrigin(), this.getYOrigin(), 147, 166)) { RecipeDisplayId recipeDisplayId = this.recipeBookPage.getLastClickedRecipe(); RecipeCollection recipeCollection = this.recipeBookPage.getLastClickedRecipeCollection(); if (recipeDisplayId != null && recipeCollection != null) { if (!this.tryPlaceRecipe(recipeCollection, recipeDisplayId)) { return false; } this.lastRecipeCollection = recipeCollection; this.lastRecipe = recipeDisplayId; if (!this.isOffsetNextToMainGUI()) { this.setVisible(false); } } return true; } else { if (this.searchBox != null) { boolean bl = this.magnifierIconPlacement != null && this.magnifierIconPlacement.containsPoint(Mth.floor(mouseX), Mth.floor(mouseY)); if (bl || this.searchBox.mouseClicked(mouseX, mouseY, button)) { this.searchBox.setFocused(true); return true; } this.searchBox.setFocused(false); } if (this.filterButton.mouseClicked(mouseX, mouseY, button)) { boolean bl = this.toggleFiltering(); this.filterButton.setStateTriggered(bl); this.updateFilterButtonTooltip(); this.sendUpdateSettings(); this.updateCollections(false, bl); return true; } else { for (RecipeBookTabButton recipeBookTabButton : this.tabButtons) { if (recipeBookTabButton.mouseClicked(mouseX, mouseY, button)) { if (this.selectedTab != recipeBookTabButton) { if (this.selectedTab != null) { this.selectedTab.setStateTriggered(false); } this.selectedTab = recipeBookTabButton; this.selectedTab.setStateTriggered(true); this.updateCollections(true, this.isFiltering()); } return true; } } return false; } } } else { return false; } } private boolean tryPlaceRecipe(RecipeCollection recipeCollection, RecipeDisplayId recipe) { if (!recipeCollection.isCraftable(recipe) && recipe.equals(this.lastPlacedRecipe)) { return false; } else { this.lastPlacedRecipe = recipe; this.ghostSlots.clear(); this.minecraft.gameMode.handlePlaceRecipe(this.minecraft.player.containerMenu.containerId, recipe, Screen.hasShiftDown()); return true; } } private boolean toggleFiltering() { RecipeBookType recipeBookType = this.menu.getRecipeBookType(); boolean bl = !this.book.isFiltering(recipeBookType); this.book.setFiltering(recipeBookType, bl); return bl; } public boolean hasClickedOutside(double mouseX, double mouseY, int x, int y, int width, int height, int mouseButton) { if (!this.isVisible()) { return true; } else { boolean bl = mouseX < x || mouseY < y || mouseX >= x + width || mouseY >= y + height; boolean bl2 = x - 147 < mouseX && mouseX < x && y < mouseY && mouseY < y + height; return bl && !bl2 && !this.selectedTab.isHoveredOrFocused(); } } @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { this.ignoreTextInput = false; if (!this.isVisible() || this.minecraft.player.isSpectator()) { return false; } else if (keyCode == 256 && !this.isOffsetNextToMainGUI()) { this.setVisible(false); return true; } else if (this.searchBox.keyPressed(keyCode, scanCode, modifiers)) { this.checkSearchStringUpdate(); return true; } else if (this.searchBox.isFocused() && this.searchBox.isVisible() && keyCode != 256) { return true; } else if (this.minecraft.options.keyChat.matches(keyCode, scanCode) && !this.searchBox.isFocused()) { this.ignoreTextInput = true; this.searchBox.setFocused(true); return true; } else if (CommonInputs.selected(keyCode) && this.lastRecipeCollection != null && this.lastRecipe != null) { AbstractWidget.playButtonClickSound(Minecraft.getInstance().getSoundManager()); return this.tryPlaceRecipe(this.lastRecipeCollection, this.lastRecipe); } else { return false; } } @Override public boolean keyReleased(int keyCode, int scanCode, int modifiers) { this.ignoreTextInput = false; return GuiEventListener.super.keyReleased(keyCode, scanCode, modifiers); } @Override public boolean charTyped(char codePoint, int modifiers) { if (this.ignoreTextInput) { return false; } else if (!this.isVisible() || this.minecraft.player.isSpectator()) { return false; } else if (this.searchBox.charTyped(codePoint, modifiers)) { this.checkSearchStringUpdate(); return true; } else { return GuiEventListener.super.charTyped(codePoint, modifiers); } } @Override public boolean isMouseOver(double mouseX, double mouseY) { return false; } @Override public void setFocused(boolean focused) { } @Override public boolean isFocused() { return false; } private void checkSearchStringUpdate() { String string = this.searchBox.getValue().toLowerCase(Locale.ROOT); this.pirateSpeechForThePeople(string); if (!string.equals(this.lastSearch)) { this.updateCollections(false, this.isFiltering()); this.lastSearch = string; } } /** * Check if we should activate the pirate speak easter egg. */ private void pirateSpeechForThePeople(String text) { if ("excitedze".equals(text)) { LanguageManager languageManager = this.minecraft.getLanguageManager(); String string = "en_pt"; LanguageInfo languageInfo = languageManager.getLanguage("en_pt"); if (languageInfo == null || languageManager.getSelected().equals("en_pt")) { return; } languageManager.setSelected("en_pt"); this.minecraft.options.languageCode = "en_pt"; this.minecraft.reloadResourcePacks(); this.minecraft.options.save(); } } private boolean isOffsetNextToMainGUI() { return this.xOffset == 86; } public void recipesUpdated() { this.selectMatchingRecipes(); this.updateTabs(this.isFiltering()); if (this.isVisible()) { this.updateCollections(false, this.isFiltering()); } } public void recipeShown(RecipeDisplayId recipe) { this.minecraft.player.removeRecipeHighlight(recipe); } public void fillGhostRecipe(RecipeDisplay recipeDisplay) { this.ghostSlots.clear(); ContextMap contextMap = SlotDisplayContext.fromLevel((Level)Objects.requireNonNull(this.minecraft.level)); this.fillGhostRecipe(this.ghostSlots, recipeDisplay, contextMap); } protected abstract void fillGhostRecipe(GhostSlots ghostSlots, RecipeDisplay recipeDisplay, ContextMap contextMap); protected void sendUpdateSettings() { if (this.minecraft.getConnection() != null) { RecipeBookType recipeBookType = this.menu.getRecipeBookType(); boolean bl = this.book.getBookSettings().isOpen(recipeBookType); boolean bl2 = this.book.getBookSettings().isFiltering(recipeBookType); this.minecraft.getConnection().send(new ServerboundRecipeBookChangeSettingsPacket(recipeBookType, bl, bl2)); } } @Override public NarrationPriority narrationPriority() { return this.visible ? NarrationPriority.HOVERED : NarrationPriority.NONE; } @Override public void updateNarration(NarrationElementOutput narrationElementOutput) { List list = Lists.newArrayList(); this.recipeBookPage.listButtons(abstractWidget -> { if (abstractWidget.isActive()) { list.add(abstractWidget); } }); list.add(this.searchBox); list.add(this.filterButton); list.addAll(this.tabButtons); NarratableSearchResult narratableSearchResult = Screen.findNarratableWidget(list, null); if (narratableSearchResult != null) { narratableSearchResult.entry.updateNarration(narrationElementOutput.nest()); } } @Environment(EnvType.CLIENT) public record TabInfo(ItemStack primaryIcon, Optional secondaryIcon, ExtendedRecipeBookCategory category) { public TabInfo(SearchRecipeBookCategory category) { this(new ItemStack(Items.COMPASS), Optional.empty(), category); } public TabInfo(Item primaryIcon, RecipeBookCategory category) { this(new ItemStack(primaryIcon), Optional.empty(), category); } public TabInfo(Item primaryIcon, Item secondaryIcon, RecipeBookCategory category) { this(new ItemStack(primaryIcon), Optional.of(new ItemStack(secondaryIcon)), category); } } }