From adf206530556db6b3d4fcc6b6702ec5e47d2bd80 Mon Sep 17 00:00:00 2001 From: slprime <31038811+slprime@users.noreply.github.com> Date: Sun, 4 Feb 2024 14:09:24 +0200 Subject: [PATCH] Create Bookmarks Groups; Search Recipe (#451) * Create Bookmarks Groups; Search Recipe * fix crash after DnD; add remove group function * change hiddenhandler handlerId * Pull all Bookmarked Items: use groups * Optimize Chain Algorithm * remove println * fix input V in ME terminal * Add Search Recipe Icon * fix recipe extra information * Change Search Recipe Algorithm * fixed rounding in chain calculation * fix creating groups for 2-line recipes * fix fast DnD and recipe tooltip * Bookmarks Recipe Chains: fluid canner * Bookmarks Recipe Chains: fluid cell * Open GuiRecipe: normalizeItemStack --------- Co-authored-by: slprime --- .../nei/BookmarkCraftingChain.java | 315 ++++ .../java/codechicken/nei/BookmarkPanel.java | 1673 +++++++++++++---- .../java/codechicken/nei/ClientHandler.java | 37 + .../nei/ContainerPotionCreator.java | 1 - .../nei/GuiEnchantmentModifier.java | 1 - src/main/java/codechicken/nei/ItemList.java | 9 +- src/main/java/codechicken/nei/ItemPanel.java | 8 +- .../java/codechicken/nei/ItemStackMap.java | 5 + .../java/codechicken/nei/ItemStackSet.java | 7 + src/main/java/codechicken/nei/ItemsGrid.java | 206 +- .../java/codechicken/nei/LayoutManager.java | 35 +- .../java/codechicken/nei/NEIClientConfig.java | 36 +- .../java/codechicken/nei/NEIClientUtils.java | 1 - .../java/codechicken/nei/PanelWidget.java | 2 +- .../java/codechicken/nei/PositionedStack.java | 18 +- .../java/codechicken/nei/PresetsWidget.java | 265 ++- .../codechicken/nei/RecipeSearchField.java | 78 + .../java/codechicken/nei/SearchField.java | 22 +- .../java/codechicken/nei/SubsetWidget.java | 1 - src/main/java/codechicken/nei/api/API.java | 40 +- .../codechicken/nei/api/IRecipeFilter.java | 18 + .../nei/api/ShortcutInputHandler.java | 42 +- .../nei/recipe/BookmarkRecipeId.java | 41 - .../nei/recipe/GuiCraftingRecipe.java | 97 +- .../codechicken/nei/recipe/GuiRecipe.java | 624 ++++-- .../codechicken/nei/recipe/GuiRecipeTabs.java | 8 +- .../nei/recipe/GuiUsageRecipe.java | 53 +- .../nei/recipe/IRecipeHandler.java | 8 +- .../nei/recipe/ProfilerRecipeHandler.java | 2 +- .../nei/recipe/RecipeHandlerQuery.java | 19 +- .../nei/recipe/SearchRecipeHandler.java | 120 ++ .../codechicken/nei/recipe/StackInfo.java | 43 +- .../nei/recipe/TemplateRecipeHandler.java | 132 +- .../DefaultStackStringifyHandler.java | 2 +- .../GTFluidStackStringifyHandler.java | 39 +- src/main/resources/assets/nei/lang/en_US.lang | 14 +- src/main/resources/assets/nei/lang/zh_CN.lang | 5 +- .../assets/nei/textures/nei_sprites.png | Bin 37711 -> 35117 bytes 38 files changed, 3135 insertions(+), 892 deletions(-) create mode 100644 src/main/java/codechicken/nei/BookmarkCraftingChain.java create mode 100644 src/main/java/codechicken/nei/RecipeSearchField.java create mode 100644 src/main/java/codechicken/nei/api/IRecipeFilter.java create mode 100644 src/main/java/codechicken/nei/recipe/SearchRecipeHandler.java diff --git a/src/main/java/codechicken/nei/BookmarkCraftingChain.java b/src/main/java/codechicken/nei/BookmarkCraftingChain.java new file mode 100644 index 00000000..6e5ffaa2 --- /dev/null +++ b/src/main/java/codechicken/nei/BookmarkCraftingChain.java @@ -0,0 +1,315 @@ +package codechicken.nei; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import net.minecraft.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; + +import codechicken.nei.BookmarkPanel.ItemStackMetadata; +import codechicken.nei.recipe.BookmarkRecipeId; +import codechicken.nei.recipe.StackInfo; + +public class BookmarkCraftingChain { + + public HashMap calculatedItems = new HashMap<>(); + public HashMap multiplier = new HashMap<>(); + + public HashMap inputs = new HashMap<>(); + public HashMap outputs = new HashMap<>(); + public HashMap intermediate = new HashMap<>(); + + protected static class CraftingChainItem { + + public String guid; + public int stackIndex = 0; + public int recipeIndex = 0; + public int factor = 0; + public int count = 0; + public boolean ingredient = false; + public int fluidCellAmount = 1; + public ItemStack stack = null; + + public CraftingChainItem(ItemStack stack, ItemStackMetadata stackMetadata) { + FluidStack fluid = StackInfo.getFluid(stack); + this.recipeIndex = getRecipeIndex(stackMetadata.recipeId); + this.ingredient = stackMetadata.ingredient; + this.stack = stack; + + if (fluid != null) { + this.stackIndex = getStackIndex(fluid); + this.count = fluid.amount * Math.max(0, stack.stackSize); + this.fluidCellAmount = StackInfo.isFluidContainer(stack) ? fluid.amount : 1; + this.factor = this.fluidCellAmount * stackMetadata.factor; + } else { + this.stackIndex = getStackIndex(stack); + this.count = StackInfo.itemStackToNBT(stack).getInteger("Count"); + this.factor = stackMetadata.factor; + } + + this.guid = this.stackIndex + " " + this.recipeIndex; + } + + public ItemStack getItemStack(long count) { + return StackInfo.loadFromNBT( + StackInfo.itemStackToNBT(this.stack), + this.factor > 0 ? (count / this.fluidCellAmount) : 0); + } + + public static void clearStatic() { + itemCache.clear(); + fluidCache.clear(); + recipeCache.clear(); + } + + private static HashMap itemCache = new HashMap<>(); + + private static int getStackIndex(ItemStack stackA) { + + if (!itemCache.containsKey(stackA)) { + + for (ItemStack item : itemCache.keySet()) { + if (StackInfo.equalItemAndNBT(stackA, item, true)) { + itemCache.put(stackA, itemCache.get(item)); + return itemCache.get(stackA); + } + } + + itemCache.put(stackA, itemCache.size() + fluidCache.size()); + } + + return itemCache.get(stackA); + } + + private static HashMap fluidCache = new HashMap<>(); + + private static int getStackIndex(FluidStack fluidA) { + + if (!fluidCache.containsKey(fluidA)) { + for (FluidStack item : fluidCache.keySet()) { + if (fluidA.isFluidEqual(item)) { + fluidCache.put(fluidA, fluidCache.get(item)); + return fluidCache.get(fluidA); + } + } + + fluidCache.put(fluidA, itemCache.size() + fluidCache.size()); + } + + return fluidCache.get(fluidA); + } + + private static HashMap recipeCache = new HashMap<>(); + + private static int getRecipeIndex(BookmarkRecipeId recipe) { + + if (recipe == null) { + return -1; + } + + if (!recipeCache.containsKey(recipe)) { + for (BookmarkRecipeId item : recipeCache.keySet()) { + if (item.equals(recipe)) { + recipeCache.put(recipe, recipeCache.get(item)); + return recipeCache.get(recipe); + } + } + + recipeCache.put(recipe, recipeCache.size()); + } + + return recipeCache.get(recipe); + } + + } + + protected static class CraftingChainRequest { + + public List items = new ArrayList<>(); + + public HashMap multiplier = new HashMap<>(); + public HashMap counts = new HashMap<>(); + + public HashMap> inputs = new HashMap<>(); + public HashMap> outputs = new HashMap<>(); + + public HashSet initialItems = new HashSet<>(); + public HashSet startedIngrs = new HashSet<>(); + + public CraftingChainRequest(List items, List metadata) { + HashSet inputs = new HashSet<>(); + HashSet outputs = new HashSet<>(); + + for (int index = 0; index < items.size(); index++) { + CraftingChainItem item = new CraftingChainItem(items.get(index), metadata.get(index)); + item.guid = item.ingredient ? "i" + item.guid : "r" + item.guid; + this.items.add(item); + + if (this.multiplier.get(item.recipeIndex) == null) { + this.multiplier.put(item.recipeIndex, 0); + } + + this.counts.put(item.guid, this.counts.getOrDefault(item.guid, 0L) + item.count); + + if (item.ingredient) { + inputs.add(item.recipeIndex); + + if (this.inputs.get(item.stackIndex) == null) { + this.inputs.put(item.stackIndex, new HashSet<>()); + } + + this.inputs.get(item.stackIndex).add(item); + } else { + outputs.add(item.recipeIndex); + + if (this.outputs.get(item.stackIndex) == null) { + this.outputs.put(item.stackIndex, new HashSet<>()); + } + + this.outputs.get(item.stackIndex).add(item); + } + + } + + for (int recipeIndex : this.multiplier.keySet()) { + if (recipeIndex == -1 || !inputs.contains(recipeIndex) || !outputs.contains(recipeIndex)) { + this.initialItems.add(recipeIndex); + } + } + + for (CraftingChainItem item : this.items) { + if (item.ingredient == false && !this.initialItems.contains(item.recipeIndex)) { + this.startedIngrs.add(item.stackIndex); + } + } + + CraftingChainItem.clearStatic(); + } + + } + + public void refresh(List items, List metadata) { + CraftingChainRequest request = new CraftingChainRequest(items, metadata); + HashMap iShift = new HashMap<>(); + HashMap oShift = new HashMap<>(); + boolean change = true; + int iteration = 100; + + while (--iteration > 0 && change) { + change = false; + for (int stackIndex : request.startedIngrs) { + change = calculateShift(request, stackIndex) || change; + } + } + + this.inputs.clear(); + this.outputs.clear(); + this.intermediate.clear(); + this.calculatedItems.clear(); + this.multiplier.clear(); + + for (CraftingChainItem item : request.items) { + if (request.initialItems.contains(item.recipeIndex)) { + long iCount = calculateCount(request, request.inputs.get(item.stackIndex)) + - iShift.getOrDefault(item.stackIndex, 0L); + this.calculatedItems.put(item.stack, item.stack); + + if (iCount == 0) { + this.inputs.put(item.stack, item.getItemStack(0)); + } else { + long count = Math.min(iCount, item.count); + this.inputs.put(item.stack, item.getItemStack(count)); + iShift.put(item.stackIndex, iShift.getOrDefault(item.stackIndex, 0L) + count); + } + + } + } + + for (CraftingChainItem item : request.items) { + if (!request.initialItems.contains(item.recipeIndex)) { + long count = item.count + item.factor * request.multiplier.get(item.recipeIndex); + this.calculatedItems.put(item.stack, item.getItemStack(count)); + + if (item.ingredient) { // input + long oCount = calculateCount(request, request.outputs.get(item.stackIndex)) + - oShift.getOrDefault(item.stackIndex, 0L); + + if (oCount < count) { + this.inputs.put(item.stack, item.getItemStack(count - oCount)); + oShift.put(item.stackIndex, oShift.getOrDefault(item.stackIndex, 0L) + oCount); + } else if (oCount >= count) { + this.intermediate.put(item.stack, item.getItemStack(0)); + oShift.put(item.stackIndex, oShift.getOrDefault(item.stackIndex, 0L) + count); + } + + } else { // output + long iCount = calculateCount(request, request.inputs.get(item.stackIndex)) + - iShift.getOrDefault(item.stackIndex, 0L); + this.multiplier.put(item.stack, item.factor > 0 ? (int) (count / item.factor) : 0); + + if (iCount < count) { + this.outputs.put(item.stack, item.getItemStack(count - iCount)); + iShift.put(item.stackIndex, iShift.getOrDefault(item.stackIndex, 0L) + iCount); + } else if (iCount >= count) { + this.intermediate.put(item.stack, item.getItemStack(0)); + iShift.put(item.stackIndex, iShift.getOrDefault(item.stackIndex, 0L) + count); + } + + } + + } + } + + } + + private boolean calculateShift(CraftingChainRequest request, int stackIndex) { + + if (!request.outputs.containsKey(stackIndex)) { + return false; + } + + int minRecipeIndex = 0; + int minShift = Integer.MAX_VALUE; + + for (CraftingChainItem item : request.outputs.get(stackIndex)) { + if (!request.initialItems.contains(item.recipeIndex) && item.factor > 0) { + long ingrCount = calculateCount(request, request.inputs.get(item.stackIndex), item.recipeIndex); + long outputCount = calculateCount(request, request.outputs.get(item.stackIndex)); + long shift = (long) Math.ceil((ingrCount - outputCount) / (double) item.factor); + + if (shift > 0 && shift < minShift) { + minShift = (int) shift; + minRecipeIndex = item.recipeIndex; + } + } + } + + if (minShift < Integer.MAX_VALUE) { + request.multiplier.put(minRecipeIndex, request.multiplier.get(minRecipeIndex) + minShift); + return true; + } + + return false; + } + + private long calculateCount(CraftingChainRequest request, HashSet items) { + return calculateCount(request, items, -1); + } + + private long calculateCount(CraftingChainRequest request, HashSet items, int recipeIndex) { + long count = 0L; + + if (items != null) { + for (CraftingChainItem item : items) { + if (item.recipeIndex != recipeIndex || recipeIndex == -1) { + count += request.counts.get(item.guid) + item.factor * request.multiplier.get(item.recipeIndex); + } + } + } + + return count; + } + +} diff --git a/src/main/java/codechicken/nei/BookmarkPanel.java b/src/main/java/codechicken/nei/BookmarkPanel.java index db12cfb8..0ab98d07 100644 --- a/src/main/java/codechicken/nei/BookmarkPanel.java +++ b/src/main/java/codechicken/nei/BookmarkPanel.java @@ -15,20 +15,28 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import javax.annotation.Nullable; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import org.apache.commons.io.IOUtils; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; @@ -40,6 +48,8 @@ import codechicken.nei.ItemPanel.ItemPanelSlot; import codechicken.nei.api.IBookmarkContainerHandler; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.recipe.BookmarkRecipeId; +import codechicken.nei.recipe.GuiCraftingRecipe; +import codechicken.nei.recipe.GuiRecipe; import codechicken.nei.recipe.StackInfo; import codechicken.nei.util.NBTJson; import codechicken.nei.util.ReadableNumberConverter; @@ -47,9 +57,9 @@ import codechicken.nei.util.ReadableNumberConverter; public class BookmarkPanel extends PanelWidget { protected File bookmarkFile; - protected boolean bookmarksIsLoaded = false; - public int sortedStackIndex = -1; - public int sortedNamespaceIndex = -1; + protected BookmarkLoadingState bookmarksState; + protected SortableItem sortableItem; + protected GroupingItem groupingItem; public Button namespacePrev; public Button namespaceNext; @@ -59,19 +69,88 @@ public class BookmarkPanel extends PanelWidget { protected List namespaces = new ArrayList<>(); protected int activeNamespaceIndex = 0; - protected static class BookmarkStackMeta { + protected static class SortableItem { + + public List items = new ArrayList<>(); + public List metadata = new ArrayList<>(); + + public SortableItem(List items, List metadata) { + this.items = items; + this.metadata = metadata; + } + + } + + protected static class GroupingItem { + + public boolean ungroup; + public int rowIndexA; + public int rowIndexB = -1; + + public GroupingItem(boolean ungroup, int rowIndexA) { + this.ungroup = ungroup; + this.rowIndexA = rowIndexA; + } + + public int getTopRowIndex(BookmarkGrid BGrid) { + int topRowIndex = Math.min(this.rowIndexA, this.rowIndexB); + int topItemIndex = BGrid.getRowItemIndex(topRowIndex, true); + ItemStackMetadata meta = topItemIndex >= 0 ? BGrid.metadata.get(topItemIndex) : null; + + if (meta != null && meta.recipeId != null) { + while (topItemIndex > 0 && meta.groupId == BGrid.metadata.get(topItemIndex - 1).groupId + && meta.recipeId.equals(BGrid.metadata.get(topItemIndex - 1).recipeId)) { + topItemIndex--; + } + + int index = BGrid.getMask().indexOf(topItemIndex); + topRowIndex = index == -1 ? 0 : index / BGrid.columns; + } + + return topRowIndex; + } + + public int getBottomRowIndex(BookmarkGrid BGrid) { + int bottomRowIndex = Math.max(this.rowIndexA, this.rowIndexB); + int bottomItemIndex = BGrid.getRowItemIndex(bottomRowIndex, false); + ItemStackMetadata meta = bottomItemIndex >= 0 ? BGrid.metadata.get(bottomItemIndex) : null; + + if (meta != null && meta.recipeId != null) { + int size = BGrid.size(); + + while (bottomItemIndex < size - 1 && meta.groupId == BGrid.metadata.get(bottomItemIndex + 1).groupId + && meta.recipeId.equals(BGrid.metadata.get(bottomItemIndex + 1).recipeId)) { + bottomItemIndex++; + } + + int index = BGrid.getMask().indexOf(bottomItemIndex); + bottomRowIndex = index == -1 ? BGrid.getLastRowIndex() : index / BGrid.columns; + } + + return bottomRowIndex; + } + } + + protected static class ItemStackMetadata { public int factor; + public Integer groupId; public BookmarkRecipeId recipeId; public boolean ingredient = false; public boolean fluidDisplay = false; - public BookmarkStackMeta(BookmarkRecipeId recipeId, int count, boolean ingredient, boolean fluidDisplay) { + public ItemStackMetadata(BookmarkRecipeId recipeId, int factor, boolean ingredient, Integer groupId, + boolean fluidDisplay) { this.recipeId = recipeId; - this.factor = count; + this.factor = factor; this.ingredient = ingredient; + this.groupId = groupId; this.fluidDisplay = fluidDisplay; } + + public ItemStackMetadata copy() { + return new ItemStackMetadata(this.recipeId, this.factor, this.ingredient, this.groupId, this.fluidDisplay); + } } public static enum BookmarkViewMode { @@ -79,164 +158,606 @@ public class BookmarkPanel extends PanelWidget { TODO_LIST } + public static enum BookmarkLoadingState { + LOADING, + LOADED + } + + protected static class BookmarkGroup { + + public BookmarkCraftingChain crafting = null; + public BookmarkViewMode viewMode; + + public BookmarkGroup(BookmarkViewMode viewMode) { + this.viewMode = viewMode; + } + + public BookmarkGroup(BookmarkViewMode viewMode, boolean crafting) { + this.viewMode = viewMode; + this.crafting = crafting ? new BookmarkCraftingChain() : null; + } + + public void toggleViewMode() { + if (this.viewMode == BookmarkViewMode.DEFAULT) { + this.viewMode = BookmarkViewMode.TODO_LIST; + } else { + this.viewMode = BookmarkViewMode.DEFAULT; + } + } + + public void toggleCraftingMode() { + if (this.crafting == null) { + this.crafting = new BookmarkCraftingChain(); + } else { + this.crafting = null; + } + } + + public BookmarkGroup copy() { + return new BookmarkGroup(this.viewMode); + } + } + public static class BookmarkGrid extends ItemsGrid { + protected static final int GROUP_PANEL_WIDTH = 7; + protected static final int DEFAULT_GROUP_ID = 0; protected static final float SCALE_SPEED = 0.1f; - protected List> gridMask; - protected List metadata = new ArrayList<>(); + protected List metadata = new ArrayList<>(); protected WeakHashMap animation = new WeakHashMap<>(); + protected RecipeTooltipRenderer recipeTooltipRenderer = null; - protected BookmarkViewMode viewMode = BookmarkViewMode.DEFAULT; - protected boolean[] todoSpaces; - protected int todoItemsCount = 0; + protected Map groups = new HashMap<>(); + protected List gridGroupMask = new ArrayList<>(); + protected int previousPageGroupId = DEFAULT_GROUP_ID; + protected int nextPageGroupId = DEFAULT_GROUP_ID; + protected int focusedGroupId = -1; + protected int pageCount = 0; + + private static class RecipeTooltipRenderer { + + public ItemStack stack = null; + public BookmarkRecipeId recipeId = null; + + public GuiRecipe gui = null; + public Runnable createRecipeGui = null; + } public BookmarkGrid() { - this.setShowRecipeTooltips(true); + this.groups.put(DEFAULT_GROUP_ID, new BookmarkGroup(BookmarkViewMode.DEFAULT)); } - public void setViewMode(BookmarkViewMode mode) { - if (viewMode == mode) { - return; - } - - viewMode = mode; - - if (mode == BookmarkViewMode.DEFAULT) { - sortIngredients(false); - todoItemsCount = 0; - gridMask = null; - } - - onGridChanged(); + public BookmarkViewMode getViewMode(int groupId) { + return this.groups.get(groupId).viewMode; } - public BookmarkViewMode getViewMode() { - return viewMode; + public void setViewMode(int groupId, BookmarkViewMode mode) { + if (this.groups.get(groupId).viewMode != mode) { + this.groups.get(groupId).viewMode = mode; + if (mode == BookmarkViewMode.DEFAULT) { + sortGroup(groupId); + } + + onItemsChanged(); + } + } + + public void toggleViewMode(int groupId) { + this.groups.get(groupId).toggleViewMode(); + if (this.groups.get(groupId).viewMode == BookmarkViewMode.DEFAULT) { + sortGroup(groupId); + } + onItemsChanged(); + } + + public boolean getCraftingMode(int groupId) { + return this.groups.get(groupId).crafting != null; + } + + public void setCraftingMode(int groupId, boolean on) { + if ((this.groups.get(groupId).crafting != null) != on) { + this.groups.get(groupId).crafting = on ? new BookmarkCraftingChain() : null; + onItemsChanged(); + } + } + + public void toggleCraftingMode(int groupId) { + this.groups.get(groupId).toggleCraftingMode(); + onItemsChanged(); } @Override public int getNumPages() { - if (gridMask != null) { - return gridMask.size(); + + if (gridMask == null) { + getMask(); } - return super.getNumPages(); + return this.pageCount; } @Override protected List getMask() { - if (gridMask != null) { - return gridMask.size() > page ? gridMask.get(page) : new ArrayList<>(); + + if (this.gridMask != null) { + return this.gridMask; } - return super.getMask(); - } + if (this.perPage == 0 || size() == 0) { + this.gridGroupMask = new ArrayList<>(); + this.gridMask = new ArrayList<>(); + return this.gridMask; + } - private void sortIngredients(final boolean forward) { - BookmarkStackMeta meta; - int dir = forward ? 1 : -1; + ItemStackMetadata previousMeta = new ItemStackMetadata(null, 1, false, DEFAULT_GROUP_ID, false); + List itemsMask = new ArrayList<>(); + List groupsMask = new ArrayList<>(); + ItemStackMetadata meta; + int size = size(); + int lastIdx = -1; + int index = 0; + int idx = 0; - for (int i = 0; i < realItems.size(); i++) { - meta = metadata.get(i); + while (idx < size && lastIdx != idx) { + lastIdx = idx; - if (meta.recipeId != null && !meta.ingredient) { - int shift = 0; + for (int r = 0; r < rows && idx < size; r++) { + for (int c = 0; c < columns && idx < size; c++) { + index = r * columns + c; + + if (isInvalidSlot(index)) { + itemsMask.add(null); + } else { + meta = this.metadata.get(idx); + + if (c > 0 && previousMeta.groupId != meta.groupId) { + // new group must start on a new line + itemsMask.add(null); + } else if (getViewMode(meta.groupId) == BookmarkViewMode.DEFAULT) { + previousMeta = meta; + itemsMask.add(idx++); + } else { + + if (c == 0 && (meta.recipeId == null || meta.ingredient == false + || index + 1 < rows * columns && isInvalidSlot(index + 1) + || previousMeta.groupId != meta.groupId + || meta.ingredient && !meta.recipeId.equals(previousMeta.recipeId))) { + // In first column must be an item without recipe, a recipe result, or an ingredient + // if the second column is occupied + previousMeta = meta; + itemsMask.add(idx++); + } else + if (c > 0 && meta.recipeId != null && meta.recipeId.equals(previousMeta.recipeId)) { + previousMeta = meta; + itemsMask.add(idx++); + } else { + itemsMask.add(null); + } - for (int j = i + 1; j < realItems.size(); j++) { - if (meta.recipeId.equals(metadata.get(j).recipeId)) { - if (i + shift + dir != j) { - realItems.add(i + shift + Math.max(dir, 0), realItems.remove(j)); - metadata.add(i + shift + Math.max(dir, 0), metadata.remove(j)); } - shift++; - } - } - for (int j = i + shift - 1; j >= 0; j--) { - if (meta.recipeId.equals(metadata.get(j).recipeId)) { - if (i + shift + dir != j) { - realItems.add(i + shift + Math.min(dir, 0), realItems.remove(j)); - metadata.add(i + shift + Math.min(dir, 0), metadata.remove(j)); - } - shift--; } - } - i += Math.max(0, shift); + } + groupsMask.add(previousMeta.groupId); } } + + this.pageCount = (int) Math.ceil(itemsMask.size() / (float) (this.rows * this.columns)); + this.page = Math.max(0, Math.min(this.page, this.pageCount - 1)); + this.gridMask = itemsMask + .subList(page * rows * columns, Math.min(itemsMask.size(), (page + 1) * rows * columns)); + this.previousPageGroupId = DEFAULT_GROUP_ID; + this.nextPageGroupId = DEFAULT_GROUP_ID; + + if (page > 0 && (this.page * this.rows) < groupsMask.size() + && groupsMask.get(this.page * this.rows) == groupsMask.get(this.page * this.rows - 1)) { + this.previousPageGroupId = groupsMask.get(this.page * this.rows); + } + + if ((this.page + 1) * this.rows < groupsMask.size() + && groupsMask.get((this.page + 1) * this.rows) == groupsMask.get((this.page + 1) * this.rows - 1)) { + this.nextPageGroupId = groupsMask.get((this.page + 1) * this.rows); + } + + this.gridGroupMask = groupsMask.subList(page * rows, Math.min(groupsMask.size(), (page + 1) * rows)); + + return this.gridMask; } - private void generateToDoSpaces() { - todoItemsCount = 0; - gridMask = new ArrayList<>(); + protected int getRowGroupId(int rowIndex) { + if (gridMask == null) { + getMask(); + } - if (rows * columns == 0) { + return rowIndex < this.gridGroupMask.size() ? this.gridGroupMask.get(rowIndex) : DEFAULT_GROUP_ID; + } + + protected int getRowItemIndex(int rowIndex, boolean dir) { + final List mask = getMask(); + final int size = mask.size(); + int i = dir ? 0 : columns - 1; + + while (i >= 0 && i < columns) { + + if ((rowIndex * columns + i) < size && mask.get(rowIndex * columns + i) != null) { + return mask.get(rowIndex * columns + i); + } + + i += dir ? 1 : -1; + } + + return -1; + } + + protected int getHoveredRowIndex(boolean groupPanel) { + final Point mouse = getMousePosition(); + final int leftBorder = marginLeft + paddingLeft; + final int r = (int) ((mouse.y - marginTop) / SLOT_SIZE); + + if (!(new Rectangle4i( + leftBorder - (groupPanel ? BookmarkGrid.GROUP_PANEL_WIDTH : 0), + marginTop, + columns * SLOT_SIZE, + (getLastRowIndex() + 1) * SLOT_SIZE)).contains(mouse.x, mouse.y)) { + return -1; + } + + if (groupPanel && mouse.x >= leftBorder - GROUP_PANEL_WIDTH && mouse.x < leftBorder) { + return r; + } + + if (!groupPanel && !isInvalidSlot(columns * r + ((mouse.x - leftBorder) / SLOT_SIZE))) { + return r; + } + + return -1; + } + + @Override + public void setGridSize(int mleft, int mtop, int w, int h) { + super.setGridSize(mleft + GROUP_PANEL_WIDTH, mtop, w, h); + } + + @Override + public void draw(int mousex, int mousey) { + if (getPerPage() == 0) { return; } - BookmarkStackMeta meta; - boolean isFirstColumn = false; - boolean isIngredient = false; - int size = size(); + final int focusedRowIndex = getHoveredRowIndex(true); + + if (focusedRowIndex != -1) { + drawRect( + marginLeft + paddingLeft - GROUP_PANEL_WIDTH, + marginTop + focusedRowIndex * SLOT_SIZE, + GROUP_PANEL_WIDTH, + SLOT_SIZE, + 0xee555555); + } + + final GroupingItem groupingItem = LayoutManager.bookmarkPanel.groupingItem; + List groupMask = this.gridGroupMask; + int groupStartIndex = -2; + int previoudGroupId = DEFAULT_GROUP_ID; + + if (groupingItem != null && groupingItem.rowIndexB != -1) { + int topRowIndex = groupingItem.getTopRowIndex(this); + int bottomRowIndex = groupingItem.getBottomRowIndex(this); + int groupIdA = groupMask.get(groupingItem.rowIndexA); + int groupId = groupingItem.ungroup ? DEFAULT_GROUP_ID : (groupIdA == DEFAULT_GROUP_ID ? -1 : groupIdA); + groupMask = new ArrayList<>(groupMask); + + for (int rowIndex = topRowIndex; rowIndex <= bottomRowIndex; rowIndex++) { + groupMask.set(rowIndex, groupId); + } + + if (groupIdA != DEFAULT_GROUP_ID && !groupingItem.ungroup + && groupingItem.rowIndexA != groupingItem.rowIndexB) { + if (groupingItem.rowIndexB < groupingItem.rowIndexA) { + for (int rowIndex = topRowIndex - 1; rowIndex >= 0 + && groupMask.get(rowIndex) == groupIdA; rowIndex--) { + groupMask.set(rowIndex, DEFAULT_GROUP_ID); + } + } else if (groupingItem.rowIndexB > groupingItem.rowIndexA) { + for (int rowIndex = bottomRowIndex + 1; rowIndex < groupMask.size() + && groupMask.get(rowIndex) == groupIdA; rowIndex++) { + groupMask.set(rowIndex, DEFAULT_GROUP_ID); + } + } + } + + } + + if (this.previousPageGroupId != DEFAULT_GROUP_ID && this.previousPageGroupId == groupMask.get(0)) { + previoudGroupId = this.previousPageGroupId; + groupStartIndex = -1; + } + + for (int rowIndex = 0; rowIndex < groupMask.size(); rowIndex++) { + int groupId = groupMask.get(rowIndex); + + if (groupStartIndex != -2 && previoudGroupId != DEFAULT_GROUP_ID && previoudGroupId != groupId) { + drawGroup(Math.max(DEFAULT_GROUP_ID, previoudGroupId), groupStartIndex, rowIndex - 1); + groupStartIndex = -2; + } + + if (groupStartIndex == -2 && groupId != DEFAULT_GROUP_ID) { + groupStartIndex = rowIndex; + } + + previoudGroupId = groupId; + } + + if (groupStartIndex != -2) { + final int rowIndex = this.nextPageGroupId != DEFAULT_GROUP_ID + && this.nextPageGroupId == groupMask.get(groupMask.size() - 1) ? this.rows : getLastRowIndex(); + drawGroup(Math.max(DEFAULT_GROUP_ID, previoudGroupId), groupStartIndex, rowIndex); + } + + if (NEIClientUtils.shiftKey() && !LayoutManager.bookmarkPanel.inEditingState()) { + final int rowId = getHoveredRowIndex(true); + + if (rowId != -1) { + this.focusedGroupId = getRowGroupId(rowId); + } else { + final ItemPanelSlot focused = getSlotMouseOver(mousex, mousey); + this.focusedGroupId = focused != null ? this.metadata.get(focused.slotIndex).groupId : -1; + } + + } else { + this.focusedGroupId = -1; + } + + super.draw(mousex, mousey); + } + + private void drawGroup(int groupId, int rowIndexStart, int rowIndexEnd) { + final int halfWidth = GROUP_PANEL_WIDTH / 2; + final int heightPadding = SLOT_SIZE / 4; + final int leftPosition = marginLeft + paddingLeft - halfWidth - 1; + final int color = this.groups.get(groupId).crafting != null ? 0x6645DA75 : 0xff666666; + int width = (Math.min(rowIndexEnd, this.rows - 1) - Math.max(0, rowIndexStart) + 1) * SLOT_SIZE; + int top = marginTop + Math.max(0, rowIndexStart) * SLOT_SIZE; + + if (rowIndexStart >= 0) { + drawRect(leftPosition, marginTop + rowIndexStart * SLOT_SIZE + heightPadding, halfWidth, 1, color); + top += heightPadding + 1; + width -= heightPadding + 1; + } + + if (rowIndexEnd < this.rows) { + drawRect(leftPosition, marginTop + (rowIndexEnd + 1) * SLOT_SIZE - heightPadding, halfWidth, 1, color); + width -= heightPadding; + } + + drawRect(leftPosition, top, 1, width, color); + } + + private void removeDuplicateItems() { + final HashMap recipeCache = new HashMap<>(); + final HashMap itemsCache = new HashMap<>(); + final HashSet unique = new HashSet<>(); + ItemStackMetadata meta; + ItemStack stack; + int index = 0; + String key; + + while (index < this.metadata.size()) { + stack = this.realItems.get(index); + meta = this.metadata.get(index); + key = String.valueOf(meta.groupId); + + if (!itemsCache.containsKey(stack)) { + for (ItemStack item : itemsCache.keySet()) { + if (StackInfo.equalItemAndNBT(stack, item, true)) { + itemsCache.put(stack, itemsCache.get(item)); + break; + } + } + } + + if (!itemsCache.containsKey(stack)) { + itemsCache.put(stack, itemsCache.size()); + } + + key = key + ":" + itemsCache.get(stack); + + if (meta.recipeId != null) { + if (!recipeCache.containsKey(meta.recipeId)) { + for (BookmarkRecipeId item : recipeCache.keySet()) { + if (item.equals(meta.recipeId)) { + recipeCache.put(meta.recipeId, recipeCache.get(item)); + break; + } + } + } + + if (!recipeCache.containsKey(meta.recipeId)) { + recipeCache.put(meta.recipeId, recipeCache.size()); + } + + key = key + ":" + recipeCache.get(meta.recipeId); + } + + if (unique.contains(key)) { + this.realItems.remove(index); + this.metadata.remove(index); + } else { + unique.add(key); + index++; + } + } + + } + + private void sortGroup(int groupId) { + final int dir = getViewMode(groupId) == BookmarkViewMode.TODO_LIST ? 1 : -1; + final int size = this.metadata.size(); + ItemStackMetadata meta; int idx = 0; while (idx < size) { - ArrayList pmask = new ArrayList<>(); - int lastIdx = idx; + meta = this.metadata.get(idx); + if (meta.groupId == groupId && meta.recipeId != null) { + final HashMap sortingRank = new HashMap<>(); + final ArrayList sortedMetadata = new ArrayList<>(); + final ArrayList sortedItems = new ArrayList<>(); + final ArrayList items = new ArrayList<>(); - for (int i = 0, perPage = rows * columns; i < perPage && idx < size; i++) { - - if (isInvalidSlot(i)) { - pmask.add(null); - } else { - meta = getMetadata(idx); - isFirstColumn = (i % columns) == 0; - isIngredient = meta.recipeId != null && meta.ingredient; - - if (isFirstColumn && !isIngredient) { - pmask.add(idx++); - - todoItemsCount++; - } else if (isIngredient && (!isFirstColumn || i + 1 < perPage && isInvalidSlot(i + 1))) { - // The ingredients that do not fit on the first line are moved to the second starting with - // the second column. This condition checks whether the second column is available. If not - // available, the ingredient takes the first column - pmask.add(idx++); - } else { - pmask.add(null); + for (int index = idx; index < size; index++) { + if (this.metadata.get(index).groupId == groupId + && meta.recipeId.equals(this.metadata.get(index).recipeId)) { + sortingRank.put(index, (this.metadata.get(index).ingredient ? 1 : -1) * dir); + items.add(index); } - } + items.sort((a, b) -> sortingRank.get(a) - sortingRank.get(b)); + + for (int index : items) { + sortedItems.add(this.realItems.get(index)); + sortedMetadata.add(this.metadata.get(index)); + } + + this.realItems.removeAll(sortedItems); + this.metadata.removeAll(sortedMetadata); + + this.realItems.addAll(idx, sortedItems); + this.metadata.addAll(idx, sortedMetadata); + + idx += sortedItems.size(); + } else { + idx++; + } + } + + } + + private void sortIngredients() { + if (this.size() == 0) { + return; + } + + removeDuplicateItems(); + boolean inEditingState = LayoutManager.bookmarkPanel.inEditingState(); + + for (int groupId : this.groups.keySet()) { + BookmarkGroup group = this.groups.get(groupId); + + if (group.viewMode == BookmarkViewMode.TODO_LIST) { + sortGroup(groupId); } - if (lastIdx == idx) { - break; + if (group.crafting != null && !inEditingState) { + ArrayList groupMetadata = new ArrayList<>(); + ArrayList groupItems = new ArrayList<>(); + + for (int idx = 0; idx < this.metadata.size(); idx++) { + if (this.metadata.get(idx).groupId == groupId) { + groupItems.add(this.realItems.get(idx)); + groupMetadata.add(this.metadata.get(idx)); + } + } + + group.crafting.refresh(groupItems, groupMetadata); } - gridMask.add(pmask); } } - protected void onGridChanged() { - super.onGridChanged(); + protected void onItemsChanged() { + sortIngredients(); + onGridChanged(); + } - if (getViewMode() == BookmarkViewMode.TODO_LIST) { - sortIngredients(true); - generateToDoSpaces(); + protected void createGroup(GroupingItem groupingItem) { + final List mask = getMask(); + final int topRowIndex = groupingItem.getTopRowIndex(this); + final int bottomRowIndex = groupingItem.getBottomRowIndex(this); + final int groupIdA = getRowGroupId(groupingItem.rowIndexA); + final int startMaskIndex = topRowIndex * columns; + final int endMaskIntex = Math.min((bottomRowIndex + 1) * columns, mask.size()); + int startItemIndex = -1; + int endItemIndex = -1; + int groupId = DEFAULT_GROUP_ID; + + if (!groupingItem.ungroup) { + if (groupIdA == DEFAULT_GROUP_ID) { + for (ItemStackMetadata meta : this.metadata) { + if (meta.groupId > groupId) { + groupId = meta.groupId; + } + } + this.groups.put(++groupId, new BookmarkGroup(getViewMode(DEFAULT_GROUP_ID))); + } else { + groupId = groupIdA; + } } + + for (int index = startMaskIndex; index < endMaskIntex; index++) { + if (mask.get(index) != null) { + startItemIndex = startItemIndex == -1 ? mask.get(index) : startItemIndex; + this.metadata.get(mask.get(index)).groupId = groupId; + endItemIndex = mask.get(index); + } + } + + if (startItemIndex != -1 && endItemIndex != -1 + && groupIdA != DEFAULT_GROUP_ID + && !groupingItem.ungroup + && groupingItem.rowIndexA != groupingItem.rowIndexB) { + if (groupingItem.rowIndexB == topRowIndex) { + for (int idx = startItemIndex - 1; idx >= 0 && this.metadata.get(idx).groupId == groupIdA; idx--) { + this.metadata.get(idx).groupId = DEFAULT_GROUP_ID; + } + } else { + for (int idx = endItemIndex + 1; idx < this.metadata.size() + && this.metadata.get(idx).groupId == groupIdA; idx++) { + this.metadata.get(idx).groupId = DEFAULT_GROUP_ID; + } + } + } + + final HashSet usedSetIds = new HashSet<>(); + for (ItemStackMetadata meta : this.metadata) { + usedSetIds.add(meta.groupId); + } + + this.groups.keySet().removeIf((Integer k) -> k != DEFAULT_GROUP_ID && !usedSetIds.contains(k)); + + onItemsChanged(); + } + + protected void removeGroup(int groupId) { + + for (int i = this.metadata.size() - 1; i >= 0; i--) { + if (this.metadata.get(i).groupId == groupId) { + this.metadata.remove(i); + this.realItems.remove(i); + } + } + + if (groupId != DEFAULT_GROUP_ID) { + this.groups.remove(groupId); + } + + onItemsChanged(); } public int indexOf(ItemStack stackA, BookmarkRecipeId recipeId) { - final boolean useNBT = NEIClientConfig.useNBTInBookmarks(); + return indexOf(stackA, recipeId, DEFAULT_GROUP_ID); + } + + public int indexOf(ItemStack stackA, BookmarkRecipeId recipeId, int groupId) { for (int idx = 0; idx < realItems.size(); idx++) { - final BookmarkStackMeta meta = getMetadata(idx); - if ((recipeId == null && meta.recipeId == null - || recipeId != null && meta.recipeId != null && recipeId.equals(meta.recipeId)) - && StackInfo.equalItemAndNBT(stackA, getItem(idx), useNBT)) { + final ItemStackMetadata meta = getMetadata(idx); + if (meta.groupId == groupId + && (recipeId == null && meta.recipeId == null + || recipeId != null && meta.recipeId != null && recipeId.equals(meta.recipeId)) + && StackInfo.equalItemAndNBT(stackA, getItem(idx), true)) { return idx; } } @@ -244,57 +765,63 @@ public class BookmarkPanel extends PanelWidget { return -1; } - public BookmarkStackMeta getMetadata(int idx) { + public ItemStack getItem(int idx) { + final ItemStack stack = super.realItems.get(idx); + final ItemStackMetadata meta = this.getMetadata(idx); + final BookmarkCraftingChain crafting = this.groups.get(meta.groupId).crafting; + + if (crafting != null && crafting.calculatedItems.containsKey(stack)) { + return crafting.calculatedItems.get(stack); + } + + return stack; + } + + public ItemStackMetadata getMetadata(int idx) { return metadata.get(idx); } - public void addItem(ItemStack stackA, BookmarkStackMeta meta) { + public void addItem(ItemStack stackA, ItemStackMetadata meta) { addItem(stackA, meta, true); } - public void addItem(ItemStack stackA, BookmarkStackMeta meta, boolean animate) { - realItems.add(stackA); - metadata.add(meta); + public void addItem(ItemStack stackA, ItemStackMetadata meta, boolean animate) { + this.realItems.add(stackA); + this.metadata.add(meta); if (animate && !NEIClientConfig.shouldCacheItemRendering() && NEIClientConfig.areBookmarksAnimated()) { - animation.put(stackA, 0f); + this.animation.put(stackA, 0f); } - onGridChanged(); + onItemsChanged(); } - public void replaceItem(int idx, ItemStack stack) { - realItems.set(idx, stack); - onGridChanged(); - } - - public void removeRecipe(int idx, boolean removeFullRecipe) { - final BookmarkStackMeta meta = getMetadata(idx); + protected void removeRecipe(int idx, boolean removeFullRecipe) { + final ItemStackMetadata meta = getMetadata(idx); if (meta.recipeId != null && (removeFullRecipe || !meta.ingredient)) { - removeRecipe(meta.recipeId); + removeRecipe(meta.recipeId, meta.groupId); } else { removeItem(idx); } } - public void removeRecipe(BookmarkRecipeId recipeIdA) { + protected void removeRecipe(BookmarkRecipeId recipeIdA, int groupId) { BookmarkRecipeId recipeIdB; for (int slotIndex = metadata.size() - 1; slotIndex >= 0; slotIndex--) { - recipeIdB = getRecipeId(slotIndex); + recipeIdB = metadata.get(slotIndex).recipeId; - if (recipeIdB != null && recipeIdB.equals(recipeIdA)) { + if (recipeIdB != null && metadata.get(slotIndex).groupId == groupId && recipeIdB.equals(recipeIdA)) { removeItem(slotIndex); } } } protected boolean removeItem(int idx) { - animation.remove(getItem(idx)); realItems.remove(idx); metadata.remove(idx); - onGridChanged(); + onItemsChanged(); return true; } @@ -302,68 +829,161 @@ public class BookmarkPanel extends PanelWidget { return getMetadata(idx).recipeId; } - public BookmarkRecipeId findRecipeId(ItemStack stackA) { - final boolean useNBT = NEIClientConfig.useNBTInBookmarks(); + protected void moveItem(SortableItem sortableItem, int slotIndex) { + moveItem( + sortableItem, + slotIndex, + slotIndex < 0 ? DEFAULT_GROUP_ID : this.metadata.get(slotIndex).groupId, + slotIndex < this.realItems.indexOf(sortableItem.items.get(0))); + } - for (int idx = 0; idx < realItems.size(); idx++) { - if (StackInfo.equalItemAndNBT(stackA, getItem(idx), useNBT)) { - return getRecipeId(idx); + protected void moveItem(SortableItem sortableItem, int slotIndex, int groupId, boolean moveUp) { + if (slotIndex == -1) return; + + if (sortableItem.items.indexOf(this.realItems.get(slotIndex)) == -1) { + final ItemStack stackA = this.realItems.get(slotIndex); + + this.realItems.removeAll(sortableItem.items); + this.metadata.removeAll(sortableItem.metadata); + + slotIndex = this.realItems.indexOf(stackA) + (moveUp ? 0 : 1); + + for (ItemStackMetadata sm : sortableItem.metadata) { + sm.groupId = groupId; } + + this.realItems.addAll(slotIndex, sortableItem.items); + this.metadata.addAll(slotIndex, sortableItem.metadata); + + onItemsChanged(); + } else if (sortableItem.metadata.get(0).groupId != groupId) { + + for (ItemStackMetadata meta : sortableItem.metadata) { + meta.groupId = groupId; + } + + onItemsChanged(); } - return null; } - public void moveItem(int src, int dst) { - realItems.add(dst, realItems.remove(src)); - metadata.add(dst, metadata.remove(src)); - onGridChanged(); - } - - private boolean isPartOfFocusedRecipe(ItemPanelSlot focused, int myIdx) { - return NEIClientUtils.shiftKey() && focused != null - && LayoutManager.bookmarkPanel.sortedStackIndex == -1 - && getRecipeId(focused.slotIndex) != null - && getRecipeId(myIdx) != null - && getRecipeId(focused.slotIndex).equals(getRecipeId(myIdx)); + protected boolean shouldCacheItemRendering() { + return NEIClientConfig.shouldCacheItemRendering() && this.focusedGroupId == -1; } @Override - protected void drawSlotOutline(@Nullable ItemPanelSlot focus, int idx, Rectangle4i rect) { + protected void beforeDrawSlot(@Nullable ItemPanelSlot focus, int idx, Rectangle4i rect) { - if (LayoutManager.bookmarkPanel.sortedNamespaceIndex == LayoutManager.bookmarkPanel.activeNamespaceIndex - && LayoutManager.bookmarkPanel.sortedStackIndex == idx) { - drawRect(rect.x, rect.y, rect.w, rect.h, 0xee555555); // highlight - } else if (focus != null) { - BookmarkStackMeta meta = getMetadata(idx); - if (isPartOfFocusedRecipe(focus, idx)) { - drawRect(rect.x, rect.y, rect.w, rect.h, meta.ingredient ? 0x88b3b300 : 0x88009933); // highlight - // recipe - } else if (focus.slotIndex == idx) { - drawRect(rect.x, rect.y, rect.w, rect.h, 0xee555555); // highlight + if (LayoutManager.bookmarkPanel.sortableItem != null || !NEIClientUtils.shiftKey()) { + super.beforeDrawSlot(focus, idx, rect); + } else if (NEIClientUtils.shiftKey() && !LayoutManager.bookmarkPanel.inEditingState()) { + ItemStack stack = this.realItems.get(idx); + ItemStackMetadata meta = this.getMetadata(idx); + BookmarkGroup groupMeta = this.groups.get(meta.groupId); + + if (groupMeta.crafting != null && meta.groupId == this.focusedGroupId) { + + if (groupMeta.crafting.inputs.containsKey(stack)) { + drawRect(rect.x, rect.y, rect.w, rect.h, 0x6645DA75); // inputs + } else if (groupMeta.crafting.outputs.containsKey(stack)) { + drawRect(rect.x, rect.y, rect.w, rect.h, 0x9966CCFF); // exports + } + + } else if (focus != null && meta.recipeId != null + && meta.groupId == getMetadata(focus.slotIndex).groupId + && meta.recipeId.equals(this.getRecipeId(focus.slotIndex))) { + drawRect(rect.x, rect.y, rect.w, rect.h, meta.ingredient ? 0x6645DA75 : 0x9966CCFF); // highlight + // recipe + } else { + super.beforeDrawSlot(focus, idx, rect); + } + + } + + } + + @Override + protected void afterDrawSlot(@Nullable ItemPanelSlot focus, int idx, Rectangle4i rect) { + final ItemStackMetadata meta = this.getMetadata(idx); + + if (meta.ingredient == true || meta.recipeId == null || LayoutManager.bookmarkPanel.sortableItem != null) { + return; + } + + final ItemStack stack = this.realItems.get(idx); + final BookmarkGroup groupMeta = this.groups.get(meta.groupId); + int multiplier = 0; + + if (NEIClientUtils.shiftKey()) { + final ItemStackMetadata prevMeta = idx > 0 ? this.getMetadata(idx - 1) : null; + if (prevMeta == null || prevMeta.ingredient || !meta.recipeId.equals(prevMeta.recipeId)) { + if (groupMeta.crafting != null && meta.groupId == this.focusedGroupId + && groupMeta.crafting.multiplier.containsKey(stack)) { + multiplier = this.groups.get(meta.groupId).crafting.multiplier.get(stack); + } else if (focus != null && meta.factor > 0 + && meta.groupId == this.metadata.get(focus.slotIndex).groupId + && meta.recipeId.equals(this.metadata.get(focus.slotIndex).recipeId)) { + multiplier = StackInfo.itemStackToNBT(stack).getInteger("Count") / meta.factor; + } } } + + if (multiplier > 0) { + drawRecipeMarker( + rect.x, + rect.y, + GuiContainerManager.getFontRenderer(stack), + "x" + ReadableNumberConverter.INSTANCE.toWideReadableForm(multiplier), + 16777215); + } else if (meta.recipeId != null && !meta.ingredient && NEIClientConfig.showRecipeMarker()) { + drawRecipeMarker(rect.x, rect.y, GuiContainerManager.getFontRenderer(stack), "R", 0xA0A0A0); + } + } @Override protected void drawItem(Rectangle4i rect, int idx) { - if (LayoutManager.bookmarkPanel.sortedNamespaceIndex != LayoutManager.bookmarkPanel.activeNamespaceIndex - || LayoutManager.bookmarkPanel.sortedStackIndex != idx) { - final ItemStack stack = getItem(idx); - final BookmarkStackMeta meta = getMetadata(idx); - final String stackSize = meta.factor < 0 || meta.fluidDisplay ? "" - : ReadableNumberConverter.INSTANCE.toWideReadableForm(stack.stackSize); - if (animation.containsKey(stack) && animation.get(stack) < 1) { - final float currentScale = animation.get(stack) + SCALE_SPEED; - animation.put(stack, currentScale); - drawPoppingItem(rect, stack, stackSize, currentScale); - } else { - GuiContainerManager.drawItem(rect.x + 1, rect.y + 1, stack, true, stackSize); - if (meta.recipeId != null && !meta.ingredient && NEIClientConfig.showRecipeMarker()) { - drawRecipeMarker(rect.x, rect.y, GuiContainerManager.getFontRenderer(stack)); + + if (LayoutManager.bookmarkPanel.sortableItem == null + || LayoutManager.bookmarkPanel.sortableItem.items.get(0) != this.realItems.get(idx)) { + ItemStack realStack = this.realItems.get(idx); + final ItemStackMetadata meta = this.getMetadata(idx); + final BookmarkGroup groupMeta = this.groups.get(meta.groupId); + ItemStack drawStack = realStack; + + if (groupMeta.crafting != null && meta.groupId == this.focusedGroupId) { + + if (groupMeta.crafting.inputs.containsKey(drawStack)) { + drawStack = groupMeta.crafting.inputs.get(drawStack); + } else if (groupMeta.crafting.outputs.containsKey(drawStack)) { + drawStack = groupMeta.crafting.outputs.get(drawStack); + } else if (groupMeta.crafting.intermediate.containsKey(drawStack)) { + drawStack = groupMeta.crafting.intermediate.get(drawStack); } + + } else if (groupMeta.crafting != null && groupMeta.crafting.calculatedItems.containsKey(drawStack)) { + drawStack = groupMeta.crafting.calculatedItems.get(drawStack); } + + String stackSize = meta.fluidDisplay || drawStack.stackSize == 0 ? "" + : ReadableNumberConverter.INSTANCE.toWideReadableForm(drawStack.stackSize); + + if (this.animation.containsKey(realStack) && this.animation.get(realStack) < 1) { + final float currentScale = this.animation.get(realStack) + SCALE_SPEED; + + if (currentScale >= 1) { + this.animation.remove(realStack); + } else { + this.animation.put(realStack, currentScale); + } + + drawPoppingItem(rect, drawStack, stackSize, currentScale); + } else { + GuiContainerManager.drawItem(rect.x + 1, rect.y + 1, drawStack, true, stackSize); + } + } + } protected void drawPoppingItem(Rectangle4i rect, ItemStack stack, String stackSize, float currentScale) { @@ -379,7 +999,7 @@ public class BookmarkPanel extends PanelWidget { GL11.glScalef(1 / currentScale, 1 / currentScale, 1 / currentScale); } - protected void drawRecipeMarker(int offsetX, int offsetY, FontRenderer fontRenderer) { + protected void drawRecipeMarker(int offsetX, int offsetY, FontRenderer fontRenderer, String text, int color) { final float scaleFactor = fontRenderer.getUnicodeFlag() ? 0.85f : 0.5f; final float inverseScaleFactor = 1.0f / scaleFactor; @@ -388,11 +1008,134 @@ public class BookmarkPanel extends PanelWidget { final int X = (int) (((float) offsetX + 1.0f) * inverseScaleFactor); final int Y = (int) (((float) offsetY + 1.0f) * inverseScaleFactor); - fontRenderer.drawStringWithShadow("R", X, Y, 0xA0A0A0); + fontRenderer.drawStringWithShadow(text, X, Y, color); GL11.glScaled(inverseScaleFactor, inverseScaleFactor, inverseScaleFactor); GuiContainerManager.enable3DRender(); } + + public void update() { + + if (this.recipeTooltipRenderer != null) { + + if (this.recipeTooltipRenderer.createRecipeGui != null) { + this.recipeTooltipRenderer.createRecipeGui.run(); + this.recipeTooltipRenderer.createRecipeGui = null; + } + + if (this.recipeTooltipRenderer.gui != null) { + this.recipeTooltipRenderer.gui.updateAsTooltip(); + } + + } + + } + + public void postDrawTooltips(int mousex, int mousey, List tooltip) { + if (NEIClientConfig.getRecipeTooltipsMode() != 0) { + try { + drawRecipeTooltip(mousex, mousey, tooltip); + } catch (Exception e) { + NEIClientConfig.logger.warn("Cannot draw recipe tooltip", e); + } + } + } + + private void drawRecipeTooltip(int mousex, int mousey, List itemTooltip) { + if (!NEIClientConfig.isLoaded()) { + return; + } + + ItemPanelSlot focused = getSlotMouseOver(mousex, mousey); + + if (focused == null) { + this.recipeTooltipRenderer = null; + return; + } + + final ItemStackMetadata meta = this.metadata.get(focused.slotIndex); + final int tooltipMode = NEIClientConfig.getRecipeTooltipsMode(); + + if (meta.recipeId == null || meta.ingredient) { + this.recipeTooltipRenderer = null; + return; + } + + if (this.groups.get(meta.groupId).viewMode == BookmarkViewMode.DEFAULT && tooltipMode != 1 + && tooltipMode != 3) { + this.recipeTooltipRenderer = null; + return; + } + + if (this.groups.get(meta.groupId).viewMode == BookmarkViewMode.TODO_LIST && tooltipMode != 2 + && tooltipMode != 3) { + this.recipeTooltipRenderer = null; + return; + } + + if (this.recipeTooltipRenderer == null || this.recipeTooltipRenderer.recipeId != meta.recipeId) { + this.recipeTooltipRenderer = new RecipeTooltipRenderer(); + this.recipeTooltipRenderer.stack = this.realItems.get(focused.slotIndex); + this.recipeTooltipRenderer.recipeId = meta.recipeId; + + this.recipeTooltipRenderer.createRecipeGui = () -> { + GuiRecipe gui = GuiCraftingRecipe.createRecipeGui( + "recipeId", + false, + this.recipeTooltipRenderer.stack, + this.recipeTooltipRenderer.recipeId); + + if (gui != null) { + gui.initGui(); + gui.guiTop = 0; + gui.guiLeft = 0; + } + + this.recipeTooltipRenderer.gui = gui; + }; + } + + if (this.recipeTooltipRenderer.gui != null) { + final Minecraft mc = Minecraft.getMinecraft(); + int recipeTooltipLines = Math.max(1, itemTooltip.size()); + + GL11.glPushMatrix(); + final float tooltipYOffset; + if (mousey - marginTop > height / 2) { + tooltipYOffset = mousey - this.recipeTooltipRenderer.gui.getHeightAsWidget() + 8; + } else { + tooltipYOffset = mousey + ((recipeTooltipLines < 2) ? 1 : 3 + ((recipeTooltipLines - 1) * 10)); + } + GL11.glTranslatef(mousex, tooltipYOffset, 500); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + RenderHelper.disableStandardItemLighting(); + this.recipeTooltipRenderer.gui.drawGuiContainerBackgroundLayer(0.0f, -100, -100); + GL11.glPopAttrib(); + + if (this.recipeTooltipRenderer.gui.slotcontainer != null) { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + @SuppressWarnings("unchecked") + List slots = (List) this.recipeTooltipRenderer.gui.slotcontainer.inventorySlots; + + for (Slot slot : slots) { + if (slot != null && slot.getStack() != null) { + GuiContainerManager.drawItem(slot.xDisplayPosition, slot.yDisplayPosition, slot.getStack()); + } + } + + GL11.glPopAttrib(); + } + this.recipeTooltipRenderer.gui.drawGuiContainerForegroundLayer(-100, -100); + for (GuiButton btn : this.recipeTooltipRenderer.gui.getOverlayButtons()) { + btn.drawButton(mc, -100, -100); + } + GL11.glPopMatrix(); + } + + } + } public BookmarkPanel() { @@ -409,7 +1152,7 @@ public class BookmarkPanel extends PanelWidget { public boolean onButtonPress(boolean rightclick) { - if (rightclick) { + if (inEditingState() || rightclick) { return false; } @@ -425,7 +1168,7 @@ public class BookmarkPanel extends PanelWidget { namespaceNext = new Button("Next") { public boolean onButtonPress(boolean rightclick) { - if (rightclick) { + if (inEditingState() || rightclick) { return false; } @@ -444,7 +1187,7 @@ public class BookmarkPanel extends PanelWidget { if (rightclick) { return false; } - return pullBookmarkItems(); + return pullBookmarkItems(-1, false); } @Override @@ -454,18 +1197,22 @@ public class BookmarkPanel extends PanelWidget { }; namespaces.add(new BookmarkGrid()); - grid = namespaces.get(activeNamespaceIndex); + setNamespace(activeNamespaceIndex); } @Override public String getLabelText() { - int size = grid.size(); - if (((BookmarkGrid) grid).getViewMode() == BookmarkViewMode.TODO_LIST) { - size = ((BookmarkGrid) grid).todoItemsCount; + if (((BookmarkGrid) grid).getCraftingMode(BookmarkGrid.DEFAULT_GROUP_ID)) { + return String.format("§2[§r%d/%d§2]§r", getPage(), Math.max(1, getNumPages())); } - return String.format("(%d/%d) [%d]", getPage(), Math.max(1, getNumPages()), size); + return String.format("%d/%d", getPage(), Math.max(1, getNumPages())); + } + + public boolean inEditingState() { + return this.sortableItem != null || this.draggedStack != null + || this.groupingItem != null && this.groupingItem.rowIndexB != -1; } public void addItem(ItemStack itemStack) { @@ -475,14 +1222,12 @@ public class BookmarkPanel extends PanelWidget { public void addItem(ItemStack itemStack, boolean saveStackSize) { final BookmarkGrid BGrid = (BookmarkGrid) grid; int idx = BGrid.indexOf(itemStack, true); + if (idx != -1) { BGrid.removeItem(idx); } - addOrRemoveItem(itemStack, null, null, false, saveStackSize); - } - public int getNameSpaceSize() { - return namespaces.size(); + addOrRemoveItem(itemStack, null, null, false, saveStackSize); } public void addOrRemoveItem(ItemStack stackA) { @@ -501,7 +1246,7 @@ public class BookmarkPanel extends PanelWidget { BGrid.removeRecipe(slot.slotIndex, saveIngredients); } else { final NBTTagCompound nbTagA = StackInfo.itemStackToNBT(stackover, saveStackSize); - final ItemStack normalizedA = StackInfo.loadFromNBT(nbTagA); + final ItemStack normalizedA = StackInfo.loadFromNBT(nbTagA, saveStackSize ? nbTagA.getInteger("Count") : 0); BookmarkRecipeId recipeId = null; if (handlerName != "" && ingredients != null) { @@ -515,41 +1260,45 @@ public class BookmarkPanel extends PanelWidget { } else { if (saveIngredients && handlerName != "" && ingredients != null) { - final Map unique = new HashMap<>(); - final ArrayList sorted = new ArrayList<>(); + final Map ingredientCount = new HashMap<>(); + final Map uniqueIngredients = new LinkedHashMap<>(); - BGrid.removeRecipe(recipeId); + BGrid.removeRecipe(recipeId, BookmarkGrid.DEFAULT_GROUP_ID); for (PositionedStack stack : ingredients) { final NBTTagCompound nbTag = StackInfo.itemStackToNBT(stack.item, saveStackSize); + final String GUID = StackInfo.getItemStackGUID(stack.item); - if (unique.get(nbTag) == null) { - unique.put(nbTag, 1); - sorted.add(nbTag); + if (!uniqueIngredients.containsKey(GUID)) { + ingredientCount.put(GUID, nbTag.getInteger("Count")); + uniqueIngredients.put(GUID, nbTag); } else { - unique.put(nbTag, unique.get(nbTag) + 1); + ingredientCount.put(GUID, ingredientCount.get(GUID) + nbTag.getInteger("Count")); } + } - for (NBTTagCompound nbTag : sorted) { - final int count = nbTag.getInteger("Count") * unique.get(nbTag); - nbTag.setInteger("Count", count); + for (String GUID : uniqueIngredients.keySet()) { BGrid.addItem( - StackInfo.loadFromNBT(nbTag), - new BookmarkStackMeta( + StackInfo.loadFromNBT( + uniqueIngredients.get(GUID), + saveStackSize ? ingredientCount.get(GUID) : 0), + new ItemStackMetadata( recipeId != null ? recipeId.copy() : null, - (saveStackSize ? 1 : -1) * count, + ingredientCount.get(GUID), true, - nbTag.hasKey("gtFluidName"))); + BookmarkGrid.DEFAULT_GROUP_ID, + uniqueIngredients.get(GUID).hasKey("gtFluidName"))); } } BGrid.addItem( normalizedA, - new BookmarkStackMeta( + new ItemStackMetadata( recipeId, - (saveStackSize ? 1 : -1) * nbTagA.getInteger("Count"), + nbTagA.getInteger("Count"), false, + BookmarkGrid.DEFAULT_GROUP_ID, nbTagA.hasKey("gtFluidName"))); } } @@ -559,20 +1308,38 @@ public class BookmarkPanel extends PanelWidget { } public BookmarkRecipeId getBookmarkRecipeId(int slotIndex) { - return ((BookmarkGrid) grid).getRecipeId(slotIndex); + ItemStackMetadata meta = ((BookmarkGrid) grid).getMetadata(slotIndex); + return meta.ingredient ? null : meta.recipeId; } public BookmarkRecipeId getBookmarkRecipeId(ItemStack stackA) { - BookmarkRecipeId recipeId = ((BookmarkGrid) grid).findRecipeId(stackA); + final BookmarkGrid BGrid = (BookmarkGrid) grid; + BookmarkRecipeId recipeId = null; - if (recipeId == null) { - for (int idx = 0; idx < getNameSpaceSize() && recipeId == null; idx++) { - if (idx == activeNamespaceIndex) continue; - recipeId = namespaces.get(idx).findRecipeId(stackA); + for (int idx = 0; idx < BGrid.realItems.size(); idx++) { + if (StackInfo.equalItemAndNBT(stackA, BGrid.realItems.get(idx), true)) { + if ((recipeId = getBookmarkRecipeId(idx)) != null) { + return recipeId; + } } } - return recipeId; + return null; + } + + public int getHoveredGroupId(boolean groupPanel) { + final BookmarkGrid BGrid = (BookmarkGrid) grid; + final int overRowIndex = BGrid.getHoveredRowIndex(groupPanel); + return overRowIndex >= 0 ? BGrid.getRowGroupId(overRowIndex) : -1; + } + + public void removeGroup(int groupId) { + final BookmarkGrid BGrid = (BookmarkGrid) grid; + + if (groupId >= 0 || groupId < BGrid.groups.size()) { + BGrid.removeGroup(groupId); + saveBookmarks(); + } } protected String getNamespaceLabelText(boolean shortFormat) { @@ -596,7 +1363,7 @@ public class BookmarkPanel extends PanelWidget { if (activeNamespaceIndex != getNameSpaceSize() - 1 && grid.size() == 0) { namespaces.remove(activeNamespaceIndex); - grid = namespaces.get(activeNamespaceIndex); + setNamespace(activeNamespaceIndex); return true; } @@ -604,7 +1371,7 @@ public class BookmarkPanel extends PanelWidget { } protected boolean prevNamespace() { - if (!bookmarksIsLoaded) { + if (bookmarksState != BookmarkLoadingState.LOADED) { return false; } @@ -612,18 +1379,16 @@ public class BookmarkPanel extends PanelWidget { removeEmptyNamespaces(); if (activeNamespaceIndex == 0) { - activeNamespaceIndex = getNameSpaceSize() - 1; + setNamespace(getNameSpaceSize() - 1); } else { - activeNamespaceIndex--; + setNamespace(activeNamespaceIndex - 1); } - grid = namespaces.get(activeNamespaceIndex); - return true; } protected boolean nextNamespace() { - if (!bookmarksIsLoaded) { + if (bookmarksState != BookmarkLoadingState.LOADED) { return false; } @@ -632,14 +1397,28 @@ public class BookmarkPanel extends PanelWidget { } if (activeNamespaceIndex == fixCountOfNamespaces() - 1) { - activeNamespaceIndex = 0; + setNamespace(0); } else { - activeNamespaceIndex++; + setNamespace(activeNamespaceIndex + 1); } + return true; + } + + protected void setNamespace(int namespaceIndex) { + activeNamespaceIndex = namespaceIndex; grid = namespaces.get(activeNamespaceIndex); - return true; + if (grid.size() == 0 && activeNamespaceIndex > 0) { + ((BookmarkGrid) grid).setViewMode( + BookmarkGrid.DEFAULT_GROUP_ID, + namespaces.get(activeNamespaceIndex - 1).getViewMode(BookmarkGrid.DEFAULT_GROUP_ID)); + } + + } + + public int getNameSpaceSize() { + return namespaces.size(); } public void setBookmarkFile(String worldPath) { @@ -650,6 +1429,10 @@ public class BookmarkPanel extends PanelWidget { dir.mkdirs(); } + if (bookmarksState == BookmarkLoadingState.LOADED) { + saveBookmarks(); + } + bookmarkFile = new File(dir, "bookmarks.ini"); if (!bookmarkFile.exists()) { @@ -674,12 +1457,12 @@ public class BookmarkPanel extends PanelWidget { } } - bookmarksIsLoaded = false; + bookmarksState = null; } public void saveBookmarks() { - if (bookmarkFile == null) { + if (bookmarkFile == null || bookmarksState != BookmarkLoadingState.LOADED) { return; } @@ -689,23 +1472,37 @@ public class BookmarkPanel extends PanelWidget { final BookmarkGrid grid = namespaces.get(grpIdx); JsonObject settings = new JsonObject(); - settings.add("viewmode", new JsonPrimitive(grid.getViewMode().toString())); + JsonObject groups = new JsonObject(); settings.add("active", new JsonPrimitive(activeNamespaceIndex == grpIdx)); + + for (int groupId : grid.groups.keySet()) { + BookmarkGroup group = grid.groups.get(groupId); + JsonObject groupJson = new JsonObject(); + groupJson.add("viewmode", new JsonPrimitive(group.viewMode.toString())); + groupJson.add("crafting", new JsonPrimitive(group.crafting != null)); + groups.add(String.valueOf(groupId), groupJson); + } + + settings.add("groups", groups); strings.add("; " + NBTJson.toJson(settings)); for (int idx = 0; idx < grid.size(); idx++) { try { - final NBTTagCompound nbTag = StackInfo.itemStackToNBT(grid.getItem(idx)); + final NBTTagCompound nbTag = StackInfo.itemStackToNBT(grid.realItems.get(idx)); if (nbTag != null) { JsonObject row = new JsonObject(); - BookmarkStackMeta meta = grid.getMetadata(idx); + ItemStackMetadata meta = grid.metadata.get(idx); row.add("item", NBTJson.toJsonObject(nbTag)); row.add("factor", new JsonPrimitive(meta.factor)); row.add("ingredient", new JsonPrimitive(meta.ingredient)); + if (meta.groupId != BookmarkGrid.DEFAULT_GROUP_ID) { + row.add("groupId", new JsonPrimitive(meta.groupId)); + } + if (meta.recipeId != null) { row.add("recipeId", meta.recipeId.toJsonObject()); } @@ -728,13 +1525,14 @@ public class BookmarkPanel extends PanelWidget { public void loadBookmarksIfNeeded() { - if (bookmarksIsLoaded) { + if (bookmarksState != null || bookmarksState == BookmarkLoadingState.LOADING) { return; } - bookmarksIsLoaded = true; + bookmarksState = BookmarkLoadingState.LOADING; if (bookmarkFile == null || !bookmarkFile.exists()) { + bookmarksState = BookmarkLoadingState.LOADED; return; } @@ -747,11 +1545,11 @@ public class BookmarkPanel extends PanelWidget { return; } - namespaces.clear(); - namespaces.add(new BookmarkGrid()); - activeNamespaceIndex = -1; - final JsonParser parser = new JsonParser(); + final List namespaces = new ArrayList<>(); + namespaces.add(new BookmarkGrid()); + BookmarkGrid grid = namespaces.get(0); + int groupId = BookmarkGrid.DEFAULT_GROUP_ID; int namespaceIndex = 0; for (String itemStr : itemStrings) { @@ -765,19 +1563,35 @@ public class BookmarkPanel extends PanelWidget { if (itemStr.startsWith("; ")) { JsonObject settings = parser.parse(itemStr.substring(2)).getAsJsonObject(); - if (namespaces.get(namespaceIndex).size() > 0) { + if (grid.size() > 0) { // do not create empty namespaces - namespaces.add(new BookmarkGrid()); - namespaceIndex++; + grid = new BookmarkGrid(); + namespaces.add(grid); } if (settings.get("viewmode") != null) { - namespaces.get(namespaceIndex) - .setViewMode(BookmarkViewMode.valueOf(settings.get("viewmode").getAsString())); + grid.groups.get(BookmarkGrid.DEFAULT_GROUP_ID).viewMode = BookmarkViewMode + .valueOf(settings.get("viewmode").getAsString()); + } else if (settings.get("groups") != null && settings.get("groups") instanceof JsonObject) { + final JsonObject jsonObject = (JsonObject) settings.get("groups"); + BookmarkGroup group; + JsonObject value; + + for (Map.Entry jsonEntry : jsonObject.entrySet()) { + if (jsonEntry.getValue() instanceof JsonObject) { + value = (JsonObject) jsonEntry.getValue(); + group = new BookmarkGroup( + value.has("viewmode") + ? BookmarkViewMode.valueOf(value.get("viewmode").getAsString()) + : BookmarkViewMode.DEFAULT, + value.has("crafting") ? value.get("crafting").getAsBoolean() : false); + grid.groups.put(Integer.valueOf(jsonEntry.getKey()), group); + } + } } if (settings.get("active") != null && settings.get("active").getAsBoolean()) { - activeNamespaceIndex = namespaceIndex; + namespaceIndex = namespaces.size() - 1; } continue; @@ -800,14 +1614,17 @@ public class BookmarkPanel extends PanelWidget { ItemStack itemStack = StackInfo.loadFromNBT(itemStackNBT); if (itemStack != null) { - namespaces.get(namespaceIndex).addItem( - itemStack, - new BookmarkStackMeta( + groupId = jsonObject.has("groupId") ? jsonObject.get("groupId").getAsInt() + : BookmarkGrid.DEFAULT_GROUP_ID; + grid.realItems.add(itemStack); + grid.metadata.add( + new ItemStackMetadata( recipeId, - jsonObject.has("factor") ? jsonObject.get("factor").getAsInt() : 0, + jsonObject.has("factor") ? Math.abs(jsonObject.get("factor").getAsInt()) + : (itemStackNBT.hasKey("gtFluidName") ? 144 : 1), jsonObject.has("ingredient") ? jsonObject.get("ingredient").getAsBoolean() : false, - itemStackNBT.hasKey("gtFluidName")), - false); + grid.groups.containsKey(groupId) ? groupId : BookmarkGrid.DEFAULT_GROUP_ID, + itemStackNBT.hasKey("gtFluidName"))); } else { NEIClientConfig.logger.warn( "Failed to load bookmarked ItemStack from json string, the item no longer exists:\n{}", @@ -819,11 +1636,13 @@ public class BookmarkPanel extends PanelWidget { } } - if (activeNamespaceIndex == -1) { - activeNamespaceIndex = namespaceIndex; + for (BookmarkGrid gr : namespaces) { + gr.onItemsChanged(); } - grid = namespaces.get(activeNamespaceIndex); + this.namespaces = namespaces; + bookmarksState = BookmarkLoadingState.LOADED; + setNamespace(namespaceIndex); } @Override @@ -919,10 +1738,10 @@ public class BookmarkPanel extends PanelWidget { return null; } - final BookmarkStackMeta meta = ((BookmarkGrid) grid).getMetadata(mouseDownSlot); + final ItemStackMetadata meta = ((BookmarkGrid) grid).getMetadata(mouseDownSlot); int amount = stack.stackSize; - if (meta.factor < 0 && !meta.fluidDisplay) { + if (amount == 0 && !meta.fluidDisplay) { amount = NEIClientConfig.showItemQuantityWidget() ? NEIClientConfig.getItemQuantity() : 0; if (amount == 0) { @@ -939,62 +1758,149 @@ public class BookmarkPanel extends PanelWidget { if (button == 0 && NEIClientUtils.shiftKey() && mouseDownSlot >= 0) { ItemPanelSlot mouseOverSlot = getSlotMouseOver(mousex, mousey); - if (sortedStackIndex == -1) { - final ItemStack stack = grid.getItem(mouseDownSlot); + if (this.sortableItem == null) { - if (stack != null + if (grid.getItem(mouseDownSlot) != null && (mouseOverSlot == null || mouseOverSlot.slotIndex != mouseDownSlot || heldTime > 250)) { - sortedNamespaceIndex = activeNamespaceIndex; - sortedStackIndex = mouseDownSlot; + final BookmarkGrid BGrid = (BookmarkGrid) grid; + final ItemStackMetadata meta = BGrid.getMetadata(mouseDownSlot); + final List items = new ArrayList<>(); + final List metadata = new ArrayList<>(); + + if (meta.recipeId == null || meta.ingredient + || BGrid.getViewMode(meta.groupId) == BookmarkViewMode.DEFAULT) { + items.add(BGrid.realItems.get(this.mouseDownSlot)); + metadata.add(BGrid.metadata.get(this.mouseDownSlot)); + } else { + + for (int i = 0; i < BGrid.metadata.size(); i++) { + if (BGrid.metadata.get(i).recipeId != null && meta.groupId == BGrid.metadata.get(i).groupId + && meta.recipeId.equals(BGrid.metadata.get(i).recipeId)) { + items.add(BGrid.realItems.get(i)); + metadata.add(BGrid.metadata.get(i)); + } + } + + } + + this.sortableItem = new SortableItem(items, metadata); grid.onGridChanged(); } } else { + final BookmarkGrid BGrid = (BookmarkGrid) grid; + final ItemStackMetadata sortMeta = this.sortableItem.metadata.get(0); + final BookmarkViewMode sortViewMode = BGrid.getViewMode(sortMeta.groupId); - if (sortedNamespaceIndex != activeNamespaceIndex) { - final BookmarkGrid hoverGrid = namespaces.get(activeNamespaceIndex); - final BookmarkGrid sortedGrid = namespaces.get(sortedNamespaceIndex); + if (sortViewMode == BookmarkViewMode.TODO_LIST && !sortMeta.ingredient) { + mouseOverSlot = getSlotMouseOver(grid.marginLeft + grid.paddingLeft, mousey); - if (mouseOverSlot != null || getNextSlot(new Point(mousex, mousey)) >= 0) { - final ItemStack stack = sortedGrid.getItem(sortedStackIndex); - final BookmarkStackMeta meta = sortedGrid.getMetadata(sortedStackIndex); + if (mouseOverSlot != null) { + float ySlot = (float) (mousey - BGrid.marginTop) / BookmarkGrid.SLOT_SIZE; + int lastRowIndex = BGrid.getLastRowIndex(); + int overRowIndex = (int) ySlot; + int beforeGroupId = overRowIndex > 0 ? BGrid.getRowGroupId(overRowIndex - 1) + : BookmarkGrid.DEFAULT_GROUP_ID; + int afterGroupId = overRowIndex < lastRowIndex ? BGrid.getRowGroupId(overRowIndex + 1) + : BookmarkGrid.DEFAULT_GROUP_ID; + int overGroupId = BGrid.metadata.get(mouseOverSlot.slotIndex).groupId; + ySlot -= overRowIndex; - sortedGrid.removeItem(sortedStackIndex); - hoverGrid.addItem(stack, meta, false); + if (this.sortableItem.items.indexOf(BGrid.realItems.get(mouseOverSlot.slotIndex)) == -1 + && overGroupId == sortMeta.groupId) { - sortedNamespaceIndex = activeNamespaceIndex; - sortedStackIndex = hoverGrid.indexOf(stack, meta.recipeId); - } - - } else if (mouseOverSlot == null || mouseOverSlot.slotIndex != sortedStackIndex) { - final BookmarkGrid sortedGrid = namespaces.get(sortedNamespaceIndex); - - if (mouseOverSlot != null && sortedGrid.getViewMode() == BookmarkViewMode.DEFAULT) { - sortedGrid.moveItem(sortedStackIndex, mouseOverSlot.slotIndex); - sortedStackIndex = mouseOverSlot.slotIndex; - } else if (sortedGrid.getViewMode() == BookmarkViewMode.TODO_LIST) { - final ItemStack stack = sortedGrid.getItem(sortedStackIndex); - final BookmarkStackMeta meta = sortedGrid.getMetadata(sortedStackIndex); - final boolean isIngredient = sortedStackIndex > 0 && meta.recipeId != null - && meta.ingredient - && meta.recipeId.equals(sortedGrid.getRecipeId(sortedStackIndex - 1)); - - if (!isIngredient) { - mouseOverSlot = getSlotMouseOver(grid.marginLeft + grid.paddingLeft, mousey); - - if (mouseOverSlot != null && sortedStackIndex != mouseOverSlot.slotIndex) { - sortedGrid.moveItem(sortedStackIndex, mouseOverSlot.slotIndex); - sortedStackIndex = sortedGrid.indexOf(stack, meta.recipeId); + if (mouseOverSlot.slotIndex < BGrid.realItems.indexOf(sortableItem.items.get(0))) { + BGrid.moveItem(this.sortableItem, mouseOverSlot.slotIndex, overGroupId, true); + } else { + BGrid.moveItem( + this.sortableItem, + BGrid.getRowItemIndex(overRowIndex, false), + overGroupId, + false); } - } else if (mouseOverSlot != null && meta.recipeId != null - && sortedGrid.getMetadata(mouseOverSlot.slotIndex).ingredient - && meta.recipeId.equals(sortedGrid.getRecipeId(mouseOverSlot.slotIndex))) { - sortedGrid.moveItem(sortedStackIndex, mouseOverSlot.slotIndex); - sortedStackIndex = sortedGrid.indexOf(stack, meta.recipeId); - } + } else if (ySlot <= 0.25) { + + if (BGrid.getViewMode(beforeGroupId) == BookmarkViewMode.TODO_LIST + && !existsRecipeIdInGroupId(beforeGroupId, sortMeta.recipeId)) { + BGrid.moveItem(this.sortableItem, mouseOverSlot.slotIndex, beforeGroupId, true); + } + + } else if (ySlot > 0.25 && ySlot <= 0.5) { + + if (beforeGroupId != afterGroupId && beforeGroupId != BookmarkGrid.DEFAULT_GROUP_ID + && (afterGroupId != BookmarkGrid.DEFAULT_GROUP_ID + || BGrid.getViewMode(overGroupId) == BookmarkViewMode.DEFAULT) + && BGrid.getViewMode(BookmarkGrid.DEFAULT_GROUP_ID) == BookmarkViewMode.TODO_LIST + && !existsRecipeIdInGroupId(BookmarkGrid.DEFAULT_GROUP_ID, sortMeta.recipeId)) { + beforeGroupId = BookmarkGrid.DEFAULT_GROUP_ID; + } + + if (BGrid.getViewMode(beforeGroupId) == BookmarkViewMode.TODO_LIST + && !existsRecipeIdInGroupId(beforeGroupId, sortMeta.recipeId)) { + BGrid.moveItem(this.sortableItem, mouseOverSlot.slotIndex, beforeGroupId, true); + } + + } else if (ySlot > 0.5 && ySlot < 0.75) { + + if (beforeGroupId != afterGroupId + && (beforeGroupId != BookmarkGrid.DEFAULT_GROUP_ID + || BGrid.getViewMode(overGroupId) == BookmarkViewMode.DEFAULT) + && afterGroupId != BookmarkGrid.DEFAULT_GROUP_ID + && BGrid.getViewMode(BookmarkGrid.DEFAULT_GROUP_ID) == BookmarkViewMode.TODO_LIST + && !existsRecipeIdInGroupId(BookmarkGrid.DEFAULT_GROUP_ID, sortMeta.recipeId)) { + afterGroupId = BookmarkGrid.DEFAULT_GROUP_ID; + } + + if (BGrid.getViewMode(afterGroupId) == BookmarkViewMode.TODO_LIST + && !existsRecipeIdInGroupId(afterGroupId, sortMeta.recipeId)) { + BGrid.moveItem( + this.sortableItem, + BGrid.getRowItemIndex(overRowIndex, false), + afterGroupId, + false); + } + + } else if (ySlot >= 0.75) { + + if (BGrid.getViewMode(afterGroupId) == BookmarkViewMode.TODO_LIST + && !existsRecipeIdInGroupId(afterGroupId, sortMeta.recipeId)) { + BGrid.moveItem( + this.sortableItem, + BGrid.getRowItemIndex(overRowIndex, false), + afterGroupId, + false); + } + + } + } - } + + } else if (mouseOverSlot != null + && this.sortableItem.items.indexOf(BGrid.realItems.get(mouseOverSlot.slotIndex)) == -1) { + final ItemStackMetadata meta = BGrid.getMetadata(mouseOverSlot.slotIndex); + + if (meta.groupId == sortMeta.groupId) { + + if (sortViewMode == BookmarkViewMode.DEFAULT) { + BGrid.moveItem(this.sortableItem, mouseOverSlot.slotIndex); + } else if (sortViewMode == BookmarkViewMode.TODO_LIST && meta.recipeId != null + && meta.recipeId.equals(sortMeta.recipeId)) { + BGrid.moveItem(this.sortableItem, mouseOverSlot.slotIndex); + } + + } + + } + + } + + return; + } else if (this.groupingItem != null) { + final int overRowIndex = (mousey - grid.marginTop) / BookmarkGrid.SLOT_SIZE; + + if (this.groupingItem.rowIndexB != -1 || overRowIndex != this.groupingItem.rowIndexA || heldTime > 250) { + this.groupingItem.rowIndexB = Math.max(0, Math.min(overRowIndex, grid.getLastRowIndex())); } return; @@ -1003,20 +1909,29 @@ public class BookmarkPanel extends PanelWidget { super.mouseDragged(mousex, mousey, button, heldTime); } - private int getNextSlot(final Point point) { + private boolean existsRecipeIdInGroupId(int groupId, BookmarkRecipeId recipeId) { + if (recipeId == null) return false; + + for (ItemStackMetadata meta : ((BookmarkGrid) grid).metadata) { + if (meta.groupId == groupId && this.sortableItem.metadata.indexOf(meta) == -1 + && recipeId.equals(meta.recipeId)) { + return true; + } + } + + return false; + } + + private int getNextSlot() { final List mask = grid.getMask(); final int columns = grid.getColumns(); final int perPage = grid.getRows() * columns; - final boolean line = ((BookmarkGrid) grid).getViewMode() == BookmarkViewMode.TODO_LIST; + final boolean line = ((BookmarkGrid) grid).getViewMode(BookmarkGrid.DEFAULT_GROUP_ID) + == BookmarkViewMode.TODO_LIST; for (int i = mask.size(); i < perPage; i++) { if (!grid.isInvalidSlot(i) && (!line || (i % columns) == 0)) { - - if (point == null || grid.getSlotRect(i).contains(point.x, point.y)) { - return i; - } - - return -1; + return i; } } @@ -1026,18 +1941,14 @@ public class BookmarkPanel extends PanelWidget { @Override public void postDraw(int mousex, int mousey) { - if (sortedStackIndex != -1) { + if (this.sortableItem != null) { GuiContainerManager.drawItems.zLevel += 100; - GuiContainerManager.drawItem( - mousex - 8, - mousey - 8, - namespaces.get(sortedNamespaceIndex).getItem(sortedStackIndex).copy(), - true); + GuiContainerManager.drawItem(mousex - 8, mousey - 8, this.sortableItem.items.get(0).copy(), true); GuiContainerManager.drawItems.zLevel -= 100; } if (ItemPanels.itemPanel.draggedStack != null && this.contains(mousex, mousey)) { - final int idx = getNextSlot(null); + final int idx = getNextSlot(); if (idx >= 0) { Rectangle4i rect = grid.getSlotRect(idx); @@ -1051,17 +1962,25 @@ public class BookmarkPanel extends PanelWidget { @Override public boolean handleClickExt(int mousex, int mousey, int button) { + if (button == 0 || button == 1) { + final int overRowIndex = ((BookmarkGrid) grid).getHoveredRowIndex(true); + + if (overRowIndex != -1) { + this.groupingItem = new GroupingItem(button == 1, overRowIndex); + return true; + } + } + if (new Rectangle4i(pagePrev.x + pagePrev.w, pagePrev.y, pageNext.x - (pagePrev.x + pagePrev.w), pagePrev.h) .contains(mousex, mousey)) { final BookmarkGrid BGrid = (BookmarkGrid) grid; - if (BGrid.getViewMode() == BookmarkViewMode.DEFAULT) { - BGrid.setViewMode(BookmarkViewMode.TODO_LIST); - } else { - BGrid.setViewMode(BookmarkViewMode.DEFAULT); + if (button == 0) { + BGrid.toggleViewMode(BookmarkGrid.DEFAULT_GROUP_ID); + } else if (button == 1) { + BGrid.toggleCraftingMode(BookmarkGrid.DEFAULT_GROUP_ID); } - saveBookmarks(); return true; } else { return super.handleClickExt(mousex, mousey, button); @@ -1085,12 +2004,33 @@ public class BookmarkPanel extends PanelWidget { @Override public void mouseUp(int mousex, int mousey, int button) { - if (sortedStackIndex != -1) { - draggedStack = null; - sortedNamespaceIndex = -1; - sortedStackIndex = -1; - mouseDownSlot = -1; - grid.onGridChanged(); /* make sure grid redraws the new item */ + + if (this.sortableItem != null) { + this.sortableItem = null; + this.mouseDownSlot = -1; + grid.onItemsChanged(); /* make sure grid redraws the new item */ + saveBookmarks(); + } else if (this.groupingItem != null) { + final BookmarkGrid BGrid = (BookmarkGrid) grid; + + if (this.groupingItem.rowIndexB != -1) { + BGrid.createGroup(this.groupingItem); + } else { + int groupId = BGrid.getRowGroupId(this.groupingItem.rowIndexA); + + if (groupId != BookmarkGrid.DEFAULT_GROUP_ID) { + if (button == 0) { + BGrid.toggleViewMode(groupId); + } else if (button == 1) { + BGrid.toggleCraftingMode(groupId); + } + } + + } + + this.mouseDownSlot = -1; + this.groupingItem = null; + grid.onItemsChanged(); /* make sure grid redraws the new item */ saveBookmarks(); } else { super.mouseUp(mousex, mousey, button); @@ -1100,7 +2040,7 @@ public class BookmarkPanel extends PanelWidget { @Override public boolean onMouseWheel(int shift, int mousex, int mousey) { - if (new Rectangle4i( + if (!inEditingState() && new Rectangle4i( namespacePrev.x, namespacePrev.y, namespaceNext.x + namespaceNext.w - namespacePrev.x, @@ -1125,27 +2065,41 @@ public class BookmarkPanel extends PanelWidget { if (slot != null) { final BookmarkGrid BGrid = (BookmarkGrid) grid; - final BookmarkRecipeId recipeId = BGrid.getRecipeId(slot.slotIndex); - final boolean addFullRecipe = NEIClientUtils.shiftKey(); + final ItemStackMetadata overMeta = BGrid.getMetadata(slot.slotIndex); + final HashMap items = new HashMap<>(); + int shiftMultiplier = 1; if (NEIClientUtils.altKey()) { - shift *= slot.item.getMaxStackSize(); - } - - if (addFullRecipe) { - BookmarkStackMeta iMeta; - - for (int slotIndex = grid.size() - 1; slotIndex >= 0; slotIndex--) { - iMeta = BGrid.getMetadata(slotIndex); - - if (iMeta.recipeId != null && iMeta.recipeId.equals(recipeId) && slotIndex != slot.slotIndex) { - shiftStackSize(slotIndex, shift); - } + shiftMultiplier = NEIClientConfig.showItemQuantityWidget() ? NEIClientConfig.getItemQuantity() : 0; + if (shiftMultiplier == 0) { + shiftMultiplier = slot.item.getMaxStackSize(); } } - shiftStackSize(slot.slotIndex, shift); + if (NEIClientUtils.shiftKey()) { + ItemStackMetadata ingrMeta; + for (int slotIndex = grid.size() - 1; slotIndex >= 0; slotIndex--) { + ingrMeta = BGrid.getMetadata(slotIndex); + + if (ingrMeta.recipeId != null && ingrMeta.groupId == overMeta.groupId + && ingrMeta.recipeId.equals(overMeta.recipeId) + && slotIndex != slot.slotIndex) { + items.put(slotIndex, shiftStackSize(BGrid, slotIndex, shift, shiftMultiplier)); + } + } + + } + + items.put(slot.slotIndex, shiftStackSize(BGrid, slot.slotIndex, shift, shiftMultiplier)); + + for (int slotIndex : items.keySet()) { + if (items.get(slotIndex) != null) { + BGrid.realItems.set(slotIndex, items.get(slotIndex)); + } + } + + BGrid.onItemsChanged(); saveBookmarks(); return true; } @@ -1154,30 +2108,67 @@ public class BookmarkPanel extends PanelWidget { return super.onMouseWheel(shift, mousex, mousey); } - protected void shiftStackSize(int slotIndex, int shift) { - final BookmarkGrid BGrid = (BookmarkGrid) grid; - final NBTTagCompound nbTag = StackInfo.itemStackToNBT(BGrid.getItem(slotIndex), true); - final BookmarkStackMeta meta = BGrid.getMetadata(slotIndex); - final int factor = Math.abs(meta.factor); + private ItemStack shiftStackSize(BookmarkGrid BGrid, int slotIndex, int shift, int shiftMultiplier) { + final NBTTagCompound nbTag = StackInfo.itemStackToNBT(BGrid.getItem(slotIndex)); + final ItemStackMetadata meta = BGrid.getMetadata(slotIndex); - if (nbTag.getInteger("Count") == factor && meta.factor < 0 && shift > 0) { - meta.factor = factor; - } else { - meta.factor = nbTag.getInteger("Count") == factor && shift < 0 ? -1 * factor : factor; - if ((long) nbTag.getInteger("Count") + ((long) factor * shift) <= Integer.MAX_VALUE) { - nbTag.setInteger("Count", Math.max(nbTag.getInteger("Count") + factor * shift, factor)); - BGrid.replaceItem(slotIndex, StackInfo.loadFromNBT(nbTag)); + if (meta.factor > 0) { + final int multiplier = nbTag.getInteger("Count") / meta.factor; + final long count = ((long) (multiplier + shift * shiftMultiplier) / shiftMultiplier) * shiftMultiplier + * meta.factor; + + if (count <= Integer.MAX_VALUE) { + return StackInfo.loadFromNBT(nbTag, Math.max(count, 0)); } } + + return null; } - public boolean pullBookmarkItems() { + public boolean pullBookmarkItems(int groupId, boolean onlyIngredients) { IBookmarkContainerHandler containerHandler = BookmarkContainerInfo .getBookmarkContainerHandler(getGuiContainer()); + if (containerHandler == null) { return false; } - containerHandler.pullBookmarkItemsFromContainer(getGuiContainer(), ((BookmarkGrid) grid).realItems); + + final BookmarkGrid BGrid = (BookmarkGrid) grid; + final ArrayList items = new ArrayList<>(); + final ItemStackMap uniqueItems = new ItemStackMap<>(); + final BookmarkGroup group = groupId >= 0 ? BGrid.groups.get(groupId) : null; + ItemStackMetadata meta; + + for (int idx = 0; idx < BGrid.realItems.size(); idx++) { + meta = BGrid.metadata.get(idx); + + if (groupId == -1 || groupId == meta.groupId) { + final ItemStack stack = BGrid.getItem(idx); + + if (!onlyIngredients || meta.ingredient && (group == null || group.crafting == null + || group.crafting.inputs.containsKey(BGrid.realItems.get(idx)))) { + uniqueItems.put( + stack, + uniqueItems.getOrDefault(stack, 0L) + StackInfo.itemStackToNBT(stack).getInteger("Count")); + } + + } + } + + for (ItemStackMap.Entry entry : uniqueItems.entries()) { + if (entry.value > 0) { + items.add( + StackInfo.loadFromNBT( + StackInfo.itemStackToNBT(entry.key), + Math.min(entry.value, 9 * 4 * entry.key.getMaxStackSize()))); + } + } + + if (items.isEmpty()) { + return false; + } + + containerHandler.pullBookmarkItemsFromContainer(getGuiContainer(), items); return true; } } diff --git a/src/main/java/codechicken/nei/ClientHandler.java b/src/main/java/codechicken/nei/ClientHandler.java index aebd60e0..3b4b6cea 100644 --- a/src/main/java/codechicken/nei/ClientHandler.java +++ b/src/main/java/codechicken/nei/ClientHandler.java @@ -28,6 +28,7 @@ import net.minecraft.entity.item.EntityItem; import net.minecraft.world.World; import net.minecraftforge.client.event.RenderWorldLastEvent; import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.world.WorldEvent; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; @@ -48,6 +49,8 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; public class ClientHandler { @@ -145,6 +148,7 @@ public class ClientHandler { public static void preInit() { loadSerialHandlers(); loadHeightHackHandlers(); + loadHiddenHandlers(); ItemInfo.preInit(); StackInfo.loadGuidFilters(); } @@ -197,6 +201,30 @@ public class ClientHandler { } } + public static void loadHiddenHandlers() { + File file = NEIClientConfig.hiddenHandlersFile; + if (!file.exists()) { + try (FileWriter writer = new FileWriter(file)) { + NEIClientConfig.logger.info("Creating default hidden handlers list {}", file); + URL defaultHeightHackHandlersResource = ClientHandler.class + .getResource("/assets/nei/cfg/hiddenhandlers.cfg"); + if (defaultHeightHackHandlersResource != null) { + IOUtils.copy(defaultHeightHackHandlersResource.openStream(), writer); + } + } catch (IOException e) { + NEIClientConfig.logger.error("Failed to save default hidden handlers list to file {}", file, e); + } + } + + try (FileReader reader = new FileReader(file)) { + NEIClientConfig.logger.info("Loading hidden handlers from file {}", file); + NEIClientConfig.hiddenHandlers = IOUtils.readLines(reader).stream().filter((line) -> !line.startsWith("#")) + .collect(Collectors.toCollection(HashSet::new)); + } catch (IOException e) { + NEIClientConfig.logger.error("Failed to load hidden handlers from file {}", file, e); + } + } + public static void load() { instance = new ClientHandler(); @@ -305,6 +333,15 @@ public class ClientHandler { if (NEIClientConfig.isEnabled()) WorldOverlayRenderer.render(event.partialTicks); } + @SideOnly(Side.CLIENT) + @SubscribeEvent + public void onWorldUnload(WorldEvent.Unload event) { + Minecraft mc = Minecraft.getMinecraft(); + if (event.world == mc.theWorld) { + NEIClientConfig.unloadWorld(); + } + } + public void loadWorld(World world, boolean fromServer) { if (world != lastworld) { SMPmagneticItems.clear(); diff --git a/src/main/java/codechicken/nei/ContainerPotionCreator.java b/src/main/java/codechicken/nei/ContainerPotionCreator.java index 3f7afd0b..32a98bea 100644 --- a/src/main/java/codechicken/nei/ContainerPotionCreator.java +++ b/src/main/java/codechicken/nei/ContainerPotionCreator.java @@ -81,7 +81,6 @@ public class ContainerPotionCreator extends ContainerExtended { public void markDirty() { super.markDirty(); NEIClientConfig.global.nbt.setTag("potionStore", tag); - NEIClientConfig.global.saveNBT(); } } diff --git a/src/main/java/codechicken/nei/GuiEnchantmentModifier.java b/src/main/java/codechicken/nei/GuiEnchantmentModifier.java index cf9c429f..6a03c019 100644 --- a/src/main/java/codechicken/nei/GuiEnchantmentModifier.java +++ b/src/main/java/codechicken/nei/GuiEnchantmentModifier.java @@ -62,7 +62,6 @@ public class GuiEnchantmentModifier extends GuiContainer { public static void toggleEnchantmentValidation() { NEIClientConfig.world.nbt.setBoolean("validateenchantments", !validateEnchantments()); - NEIClientConfig.world.saveNBT(); } protected void actionPerformed(GuiButton guibutton) { diff --git a/src/main/java/codechicken/nei/ItemList.java b/src/main/java/codechicken/nei/ItemList.java index 8ca7c97a..23efae78 100644 --- a/src/main/java/codechicken/nei/ItemList.java +++ b/src/main/java/codechicken/nei/ItemList.java @@ -1,6 +1,7 @@ package codechicken.nei; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -84,6 +85,10 @@ public class ItemList { this.filters = filters; } + public AllMultiItemFilter(ItemFilter... filters) { + this(Arrays.asList(filters)); + } + public AllMultiItemFilter() { this(new LinkedList<>()); } @@ -91,7 +96,7 @@ public class ItemList { @Override public boolean matches(ItemStack item) { for (ItemFilter filter : filters) try { - if (!filter.matches(item)) return false; + if (filter != null && !filter.matches(item)) return false; } catch (Exception e) { NEIClientConfig.logger.error("Exception filtering " + item + " with " + filter, e); } @@ -254,7 +259,7 @@ public class ItemList { try { filtered = ItemList.forkJoinPool.submit( - () -> items.parallelStream().filter(PresetsWidget::matches).filter(filter::matches) + () -> items.parallelStream().filter(filter::matches) .collect(Collectors.toCollection(ArrayList::new))) .get(); } catch (InterruptedException | ExecutionException e) { diff --git a/src/main/java/codechicken/nei/ItemPanel.java b/src/main/java/codechicken/nei/ItemPanel.java index 255cc9ed..c6629658 100644 --- a/src/main/java/codechicken/nei/ItemPanel.java +++ b/src/main/java/codechicken/nei/ItemPanel.java @@ -64,18 +64,18 @@ public class ItemPanel extends PanelWidget { if (newItems != null) { realItems = newItems; newItems = null; - onGridChanged(); + onItemsChanged(); } super.refresh(gui); } @Override - protected void drawSlotOutline(@Nullable ItemPanelSlot focused, int slotIdx, Rectangle4i rect) { + protected void beforeDrawSlot(@Nullable ItemPanelSlot focused, int slotIdx, Rectangle4i rect) { if (PresetsWidget.inEditMode()) { if (!PresetsWidget.isHidden(getItem(slotIdx))) drawRect(rect.x, rect.y, rect.w, rect.h, 0xee555555); } else { - super.drawSlotOutline(focused, slotIdx, rect); + super.beforeDrawSlot(focused, slotIdx, rect); } } @@ -142,7 +142,7 @@ public class ItemPanel extends PanelWidget { } public String getLabelText() { - return String.format("(%d/%d)", getPage(), Math.max(1, getNumPages())); + return String.format("%d/%d", getPage(), Math.max(1, getNumPages())); } protected String getPositioningSettingName() { diff --git a/src/main/java/codechicken/nei/ItemStackMap.java b/src/main/java/codechicken/nei/ItemStackMap.java index 8827982e..391a8929 100644 --- a/src/main/java/codechicken/nei/ItemStackMap.java +++ b/src/main/java/codechicken/nei/ItemStackMap.java @@ -203,6 +203,11 @@ public class ItemStackMap { return map == null ? null : map.get(key); } + public T getOrDefault(ItemStack key, T defaultValue) { + T e = get(key); + return e == null ? defaultValue : e; + } + public void put(ItemStack key, T value) { if (key == null || key.getItem() == null) return; diff --git a/src/main/java/codechicken/nei/ItemStackSet.java b/src/main/java/codechicken/nei/ItemStackSet.java index 31c0543c..12b9056c 100644 --- a/src/main/java/codechicken/nei/ItemStackSet.java +++ b/src/main/java/codechicken/nei/ItemStackSet.java @@ -1,5 +1,7 @@ package codechicken.nei; +import java.util.List; + import net.minecraft.block.Block; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; @@ -27,6 +29,11 @@ public class ItemStackSet extends ItemStackMap implements ItemFilter put(item, item); } + public ItemStackSet addAll(List items) { + for (ItemStack item : items) add(item); + return this; + } + public boolean contains(ItemStack item) { return get(item) != null; } diff --git a/src/main/java/codechicken/nei/ItemsGrid.java b/src/main/java/codechicken/nei/ItemsGrid.java index 9459b59d..01e0b479 100644 --- a/src/main/java/codechicken/nei/ItemsGrid.java +++ b/src/main/java/codechicken/nei/ItemsGrid.java @@ -1,37 +1,27 @@ package codechicken.nei; import static codechicken.lib.gui.GuiDraw.drawRect; -import static codechicken.lib.gui.GuiDraw.getMousePosition; -import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import javax.annotation.Nullable; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiButton; 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.client.renderer.Tessellator; import net.minecraft.client.shader.Framebuffer; -import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; import codechicken.lib.vec.Rectangle4i; import codechicken.nei.ItemPanel.ItemPanelSlot; import codechicken.nei.api.GuiInfo; import codechicken.nei.guihook.GuiContainerManager; -import codechicken.nei.recipe.BookmarkRecipeId; -import codechicken.nei.recipe.GuiCraftingRecipe; -import codechicken.nei.recipe.GuiRecipe; import codechicken.nei.recipe.StackInfo; public class ItemsGrid { @@ -53,7 +43,7 @@ public class ItemsGrid { protected int rows; protected int columns; - protected boolean showRecipeTooltips = false; + protected List gridMask = null; protected boolean[] invalidSlotMap; @@ -79,7 +69,7 @@ public class ItemsGrid { public int indexOf(ItemStack stackA, boolean useNBT) { for (int idx = 0; idx < realItems.size(); idx++) { - if (StackInfo.equalItemAndNBT(stackA, getItem(idx), useNBT)) { + if (StackInfo.equalItemAndNBT(stackA, realItems.get(idx), useNBT)) { return idx; } } @@ -116,6 +106,10 @@ public class ItemsGrid { return columns; } + public int getLastRowIndex() { + return this.columns > 0 ? (int) Math.ceil((float) this.getMask().size() / this.columns) - 1 : 0; + } + public void setGridSize(int mleft, int mtop, int w, int h) { marginLeft = mleft; marginTop = mtop; @@ -159,10 +153,11 @@ public class ItemsGrid { protected void onGridChanged() { refreshBuffer = true; + this.gridMask = null; } - public void setShowRecipeTooltips(boolean show) { - this.showRecipeTooltips = show; + protected void onItemsChanged() { + onGridChanged(); } public void refresh(GuiContainer gui) { @@ -198,9 +193,9 @@ public class ItemsGrid { for (int r = 0; r < rows; r++) { final int idx = columns * r + c; - if (GuiInfo.hideItemPanelSlot(gui, getSlotRect(r, c)) && idx >= 0 - && idx < invalidSlotMap.length - && !invalidSlotMap[idx]) { + if (idx >= 0 && idx < invalidSlotMap.length + && !invalidSlotMap[idx] + && GuiInfo.hideItemPanelSlot(gui, getSlotRect(r, c))) { invalidSlotMap[idx] = true; validColumn = false; perPage--; @@ -238,154 +233,51 @@ public class ItemsGrid { } protected List getMask() { - List mask = new ArrayList<>(); - int idx = page * perPage; - for (int i = 0; i < rows * columns && idx < size(); i++) { - mask.add(isInvalidSlot(i) ? null : idx++); + if (this.gridMask == null) { + this.gridMask = new ArrayList<>(); + int idx = page * perPage; + + for (int i = 0; i < rows * columns && idx < size(); i++) { + this.gridMask.add(isInvalidSlot(i) ? null : idx++); + } + } - return mask; + return this.gridMask; } - private void drawSlotOutlines(int mousex, int mousey) { - ItemPanelSlot focused = getSlotMouseOver(mousex, mousey); + private void beforeDrawItems(int mousex, int mousey, @Nullable ItemPanelSlot focused) { + final List mask = getMask(); for (int i = 0; i < mask.size(); i++) { if (mask.get(i) != null) { - drawSlotOutline(focused, mask.get(i), getSlotRect(i)); + beforeDrawSlot(focused, mask.get(i), getSlotRect(i)); } } } - private int recipeTooltipSlotIdx = -1; - private int recipeTooltipLines = 2; - private Runnable recipeTooltipUpdater = null; - private GuiRecipe recipeTooltipGui = null; - - public void update() { - if (recipeTooltipUpdater != null) { - recipeTooltipUpdater.run(); - recipeTooltipUpdater = null; - } - if (recipeTooltipGui != null) { - recipeTooltipGui.updateAsTooltip(); - } - } - - private static List listOrEmptyList(final List listOrNull) { - return listOrNull == null ? Collections.emptyList() : listOrNull; - } - - private void drawRecipeTooltip(int mousex, int mousey, List itemTooltip) { - if (!NEIClientConfig.isLoaded()) { - return; - } - - final Minecraft mc = Minecraft.getMinecraft(); - ItemPanelSlot focused = getSlotMouseOver(mousex, mousey); - if (focused == null) { - recipeTooltipSlotIdx = -1; - recipeTooltipGui = null; - return; - } + private void afterDrawItems(int mousex, int mousey, @Nullable ItemPanelSlot focused) { final List mask = getMask(); - int slotIdx = -1; for (int i = 0; i < mask.size(); i++) { - if (mask.get(i) == null) { - continue; + if (mask.get(i) != null) { + afterDrawSlot(focused, mask.get(i), getSlotRect(i)); } - if (focused.slotIndex != mask.get(i)) { - continue; - } - slotIdx = i; - break; } - if (slotIdx == -1) { - recipeTooltipSlotIdx = -1; - recipeTooltipGui = null; - return; - } - - final Point mouseover = getMousePosition(); - final ItemPanelSlot panelSlot = ItemPanels.bookmarkPanel.getSlotMouseOver(mouseover.x, mouseover.y); - final BookmarkRecipeId recipeId; - if (panelSlot != null) { - recipeId = ItemPanels.bookmarkPanel.getBookmarkRecipeId(panelSlot.slotIndex); - } else { - recipeId = ItemPanels.bookmarkPanel.getBookmarkRecipeId(focused.item); - } - if (recipeId == null) { - return; - } - - if (slotIdx != recipeTooltipSlotIdx) { - recipeTooltipSlotIdx = slotIdx; - recipeTooltipGui = null; - recipeTooltipUpdater = () -> { - recipeTooltipGui = GuiCraftingRecipe.createRecipeGui("item", false, false, false, focused.item); - if (recipeTooltipGui != null) { - recipeTooltipGui.limitToOneRecipe(); - recipeTooltipGui.initGui(true); - recipeTooltipGui.guiTop = 0; - recipeTooltipGui.guiLeft = 0; - } - recipeTooltipLines = Math.max(1, itemTooltip.size()); - }; - } - if (recipeTooltipGui == null) { - return; - } - - GL11.glPushMatrix(); - final float tooltipYOffset; - if (mousey - marginTop > height / 2) { - tooltipYOffset = mousey - recipeTooltipGui.getHeightAsWidget() + 8; - } else { - tooltipYOffset = mousey + ((recipeTooltipLines < 2) ? 1 : 3 + ((recipeTooltipLines - 1) * 10)); - } - GL11.glTranslatef(mousex, tooltipYOffset, 500); - - final GuiContainer gui; - if (mc.currentScreen instanceof GuiRecipe) { - gui = ((GuiRecipe) mc.currentScreen).firstGui; - } else if (mc.currentScreen instanceof GuiContainer) { - gui = (GuiContainer) mc.currentScreen; - } else { - gui = null; - } - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - RenderHelper.disableStandardItemLighting(); - recipeTooltipGui.drawGuiContainerBackgroundLayer(0.0f, -100, -100); - GL11.glPopAttrib(); - if (recipeTooltipGui.slotcontainer != null) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - RenderHelper.enableGUIStandardItemLighting(); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - @SuppressWarnings("unchecked") - List slots = (List) recipeTooltipGui.slotcontainer.inventorySlots; - for (Slot slot : slots) { - if (slot != null && slot.getStack() != null) { - GuiContainerManager.drawItem(slot.xDisplayPosition, slot.yDisplayPosition, slot.getStack()); - } - } - GL11.glPopAttrib(); - } - recipeTooltipGui.drawGuiContainerForegroundLayer(-100, -100); - for (GuiButton btn : recipeTooltipGui.getOverlayButtons()) { - btn.drawButton(mc, -100, -100); - } - GL11.glPopMatrix(); } - protected void drawSlotOutline(@Nullable ItemPanelSlot focused, int slotIdx, Rectangle4i rect) { + public void update() {} + + protected void beforeDrawSlot(@Nullable ItemPanelSlot focused, int slotIdx, Rectangle4i rect) { if (focused != null && focused.slotIndex == slotIdx) { drawRect(rect.x, rect.y, rect.w, rect.h, 0xee555555); // highlight } } + protected void afterDrawSlot(@Nullable ItemPanelSlot focused, int slotIdx, Rectangle4i rect) {} + private void blitExistingBuffer() { Minecraft minecraft = Minecraft.getMinecraft(); GL11.glEnable(GL11.GL_BLEND); @@ -427,12 +319,18 @@ public class ItemsGrid { GuiContainerManager.disableMatrixStackLogging(); } + protected boolean shouldCacheItemRendering() { + return NEIClientConfig.shouldCacheItemRendering(); + } + public void draw(int mousex, int mousey) { if (getPerPage() == 0) { return; } - if (NEIClientConfig.shouldCacheItemRendering()) { + final ItemPanelSlot focused = getSlotMouseOver(mousex, mousey); + + if (shouldCacheItemRendering()) { if (refreshBuffer) { Minecraft minecraft = Minecraft.getMinecraft(); @@ -457,23 +355,17 @@ public class ItemsGrid { refreshBuffer = false; } - drawSlotOutlines(mousex, mousey); + beforeDrawItems(mousex, mousey, focused); blitExistingBuffer(); } else { - drawSlotOutlines(mousex, mousey); + beforeDrawItems(mousex, mousey, focused); drawItems(); } + + afterDrawItems(mousex, mousey, focused); } - public void postDrawTooltips(int mousex, int mousey, List tooltip) { - if (NEIClientConfig.showRecipeTooltips() && showRecipeTooltips) { - try { - drawRecipeTooltip(mousex, mousey, tooltip); - } catch (Exception e) { - NEIClientConfig.logger.warn("Cannot draw recipe tooltip", e); - } - } - } + public void postDrawTooltips(int mousex, int mousey, List tooltip) {} public void setVisible() { if (getItems().isEmpty() && getMessageOnEmpty() != null) { @@ -510,16 +402,20 @@ public class ItemsGrid { } public boolean contains(int px, int py) { + final Rectangle4i rect = new Rectangle4i( + marginLeft + paddingLeft, + marginTop, + columns * SLOT_SIZE, + rows * SLOT_SIZE); - if (!(new Rectangle4i(marginLeft + paddingLeft, marginTop, columns * SLOT_SIZE, height)).contains(px, py)) { + if (!rect.contains(px, py)) { return false; } final int r = (int) ((py - marginTop) / SLOT_SIZE); final int c = (int) ((px - marginLeft - paddingLeft) / SLOT_SIZE); - final int slt = columns * r + c; - return r >= rows || c >= columns || !isInvalidSlot(slt); + return !isInvalidSlot(columns * r + c); } public String getMessageOnEmpty() { diff --git a/src/main/java/codechicken/nei/LayoutManager.java b/src/main/java/codechicken/nei/LayoutManager.java index a7923d3d..f9b22077 100644 --- a/src/main/java/codechicken/nei/LayoutManager.java +++ b/src/main/java/codechicken/nei/LayoutManager.java @@ -14,7 +14,6 @@ import static codechicken.nei.NEIClientConfig.isEnabled; import static codechicken.nei.NEIClientConfig.isHidden; import static codechicken.nei.NEIClientConfig.showIDs; import static codechicken.nei.NEIClientConfig.toggleBooleanSetting; -import static codechicken.nei.NEIClientConfig.world; import static codechicken.nei.NEIClientUtils.cycleGamemode; import static codechicken.nei.NEIClientUtils.decreaseSlotStack; import static codechicken.nei.NEIClientUtils.deleteEverything; @@ -226,10 +225,6 @@ public class LayoutManager implements IContainerInputHandler, IContainerTooltipH return true; } - if (NEIClientConfig.isKeyHashDown("gui.bookmark_pull_items")) { - return bookmarkPanel.pullBookmarkItems(); - } - if (isEnabled() && !isHidden()) { for (Widget widget : controlWidgets) if (inputFocused == null) widget.lastKeyTyped(keyID, keyChar); } @@ -286,7 +281,7 @@ public class LayoutManager implements IContainerInputHandler, IContainerTooltipH @Override public void postRenderTooltips(GuiContainer gui, int mousex, int mousey, List tooltip) { - if (!isHidden() && isEnabled()) { + if (!isHidden() && isEnabled() && GuiContainerManager.shouldShowTooltip(gui)) { for (Widget widget : drawWidgets) widget.postDrawTooltips(mousex, mousey, tooltip); } } @@ -337,6 +332,8 @@ public class LayoutManager implements IContainerInputHandler, IContainerTooltipH if (gui.guiLeft - 4 < 76) visiblity.showWidgets = false; + if (!itemsLoaded) visiblity.showPresetsDropdown = false; + try { GuiInfo.readLock.lock(); GuiInfo.guiHandlers.forEach(handler -> handler.modifyVisiblity(gui, visiblity)); @@ -735,7 +732,7 @@ public class LayoutManager implements IContainerInputHandler, IContainerTooltipH @Override public boolean shouldShowTooltip(GuiContainer gui) { - return itemPanel.draggedStack == null && bookmarkPanel.sortedStackIndex == -1; + return itemPanel.draggedStack == null && !bookmarkPanel.inEditingState(); } /** Note: this method isn't actually used by this mod, but NEI add-ons might need it. */ @@ -758,17 +755,21 @@ public class LayoutManager implements IContainerInputHandler, IContainerTooltipH @Override public void renderSlotOverlay(GuiContainer window, Slot slot) { - ItemStack item = slot.getStack(); - if (world.nbt.getBoolean("searchinventories") && (item == null ? !getSearchExpression().equals("") - : !((SearchField) searchField).getFilter().matches(item))) { - GL11.glDisable(GL11.GL_LIGHTING); - GL11.glDisable(GL11.GL_DEPTH_TEST); - GL11.glTranslatef(0, 0, 150); - drawRect(slot.xDisplayPosition, slot.yDisplayPosition, 16, 16, 0x80000000); - GL11.glTranslatef(0, 0, -150); - GL11.glEnable(GL11.GL_LIGHTING); - GL11.glEnable(GL11.GL_DEPTH_TEST); + + if (SearchField.searchInventories()) { + ItemStack item = slot.getStack(); + + if (item == null ? !getSearchExpression().equals("") : !searchField.getFilter().matches(item)) { + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glTranslatef(0, 0, 150); + drawRect(slot.xDisplayPosition, slot.yDisplayPosition, 16, 16, 0x80000000); + GL11.glTranslatef(0, 0, -150); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_DEPTH_TEST); + } } + } public static void drawIcon(int x, int y, Image image) { diff --git a/src/main/java/codechicken/nei/NEIClientConfig.java b/src/main/java/codechicken/nei/NEIClientConfig.java index 2a44c51f..54c8616e 100644 --- a/src/main/java/codechicken/nei/NEIClientConfig.java +++ b/src/main/java/codechicken/nei/NEIClientConfig.java @@ -73,6 +73,7 @@ public class NEIClientConfig { public static final File serialHandlersFile = new File(configDir, "serialhandlers.cfg"); public static final File heightHackHandlersFile = new File(configDir, "heighthackhandlers.cfg"); public static final File handlerOrderingFile = new File(configDir, "handlerordering.csv"); + public static final File hiddenHandlersFile = new File(configDir, "hiddenhandlers.csv"); @Deprecated public static File bookmarkFile; @@ -84,6 +85,9 @@ public class NEIClientConfig { // We use regex here so that we can apply the height hack to entire mods with one entry. public static HashSet heightHackHandlerRegex = new HashSet<>(); + // Set of handler Name or Id of handlers that need hide. + public static HashSet hiddenHandlers = new HashSet<>(); + // Map of handler ID to sort order. // Handlers will be sorted in ascending order, so smaller numbers show up earlier. // Any handler not in the map will be assigned to 0, and negative numbers are fine. @@ -237,14 +241,14 @@ public class NEIClientConfig { tag.getTag("inventory.bookmarksEnabled").setComment("Enable/disable bookmarks").getBooleanValue(true); API.addOption(new OptionToggleButton("inventory.bookmarksEnabled", true)); - tag.getTag("inventory.useNBTInBookmarks").setComment("Use NBT in Bookmarks").getBooleanValue(true); - API.addOption(new OptionToggleButton("inventory.useNBTInBookmarks", true)); + tag.getTag("inventory.bookmarksAnimationEnabled").setComment("REI Style Animation in Bookmarks") .getBooleanValue(true); API.addOption(new OptionToggleButton("inventory.bookmarksAnimationEnabled", true)); - tag.getTag("inventory.recipeTooltipsEnabled").setComment("Show recipe tooltips in Bookmarks") - .getBooleanValue(true); - API.addOption(new OptionToggleButton("inventory.recipeTooltipsEnabled", true)); + + tag.getTag("inventory.recipeTooltipsMode").setComment("Show recipe tooltips in Bookmarks").getIntValue(1); + API.addOption(new OptionCycled("inventory.recipeTooltipsMode", 4, true)); + tag.getTag("inventory.showRecipeMarker").setComment("Show Recipe Marker").getBooleanValue(false); API.addOption(new OptionToggleButton("inventory.showRecipeMarker", true)); @@ -256,9 +260,11 @@ public class NEIClientConfig { tag.getTag("inventory.jei_style_tabs").setComment("Enable/disable JEI Style Tabs").getBooleanValue(true); API.addOption(new OptionToggleButtonBoubs("inventory.jei_style_tabs", true)); + tag.getTag("inventory.jei_style_item_presence_overlay") .setComment("Enable/disable JEI Style item presence overlay on ?-hover").getBooleanValue(true); API.addOption(new OptionToggleButton("inventory.jei_style_item_presence_overlay", true)); + tag.getTag("inventory.jei_style_recipe_catalyst").setComment("Enable/disable JEI Style Recipe Catalysts") .getBooleanValue(true); API.addOption(new OptionToggleButton("inventory.jei_style_recipe_catalyst", true)); @@ -355,6 +361,7 @@ public class NEIClientConfig { "gui.bookmark_recipe_count", Keyboard.KEY_A + NEIClientUtils.SHIFT_HASH + NEIClientUtils.CTRL_HASH); API.addHashBind("gui.bookmark_pull_items", Keyboard.KEY_V); + API.addHashBind("gui.bookmark_pull_items_ingredients", Keyboard.KEY_V + NEIClientUtils.SHIFT_HASH); API.addHashBind("gui.overlay", Keyboard.KEY_S); API.addHashBind("gui.overlay_use", Keyboard.KEY_S + NEIClientUtils.SHIFT_HASH); API.addHashBind("gui.overlay_hide", Keyboard.KEY_S + NEIClientUtils.CTRL_HASH); @@ -424,6 +431,16 @@ public class NEIClientConfig { world.saveNBT(); } + public static void unloadWorld() { + if (ItemPanels.bookmarkPanel != null) { + ItemPanels.bookmarkPanel.saveBookmarks(); + } + + if (world != null) { + world.saveNBT(); + } + } + public static int getKeyBinding(String string) { return getSetting("keys." + string).getIntValue(Keyboard.KEY_NONE); } @@ -556,16 +573,12 @@ public class NEIClientConfig { return !getBooleanSetting("inventory.bookmarksEnabled"); } - public static boolean useNBTInBookmarks() { - return getBooleanSetting("inventory.useNBTInBookmarks"); - } - public static boolean areBookmarksAnimated() { return getBooleanSetting("inventory.bookmarksAnimationEnabled"); } - public static boolean showRecipeTooltips() { - return getBooleanSetting("inventory.recipeTooltipsEnabled"); + public static int getRecipeTooltipsMode() { + return getIntSetting("inventory.recipeTooltipsMode"); } public static boolean showRecipeMarker() { @@ -689,7 +702,6 @@ public class NEIClientConfig { public static void setSearchExpression(String expression) { world.nbt.setString("search", expression); - world.saveNBT(); } public static boolean isMouseScrollTransferEnabled() { diff --git a/src/main/java/codechicken/nei/NEIClientUtils.java b/src/main/java/codechicken/nei/NEIClientUtils.java index 5af02ff9..e111ccdd 100644 --- a/src/main/java/codechicken/nei/NEIClientUtils.java +++ b/src/main/java/codechicken/nei/NEIClientUtils.java @@ -394,7 +394,6 @@ public class NEIClientUtils extends NEIServerUtils { public static void setItemQuantity(int i) { world.nbt.setInteger("quantity", i); - world.saveNBT(); } public static GuiContainer getGuiContainer() { diff --git a/src/main/java/codechicken/nei/PanelWidget.java b/src/main/java/codechicken/nei/PanelWidget.java index f0a53c1a..27b9281c 100644 --- a/src/main/java/codechicken/nei/PanelWidget.java +++ b/src/main/java/codechicken/nei/PanelWidget.java @@ -328,7 +328,7 @@ public abstract class PanelWidget extends Widget { ItemPanelSlot hoverSlot = getSlotMouseOver(mousex, mousey); if (hoverSlot != null && hoverSlot.slotIndex == mouseDownSlot && draggedStack == null) { - ItemStack item = hoverSlot.item; + ItemStack item = hoverSlot.item.copy(); if (NEIController.manager.window instanceof GuiRecipe || !NEIClientConfig.canCheatItem(item)) { diff --git a/src/main/java/codechicken/nei/PositionedStack.java b/src/main/java/codechicken/nei/PositionedStack.java index af0b062f..bcf978ec 100644 --- a/src/main/java/codechicken/nei/PositionedStack.java +++ b/src/main/java/codechicken/nei/PositionedStack.java @@ -26,8 +26,11 @@ public class PositionedStack { relx = x; rely = y; - if (genPerms) generatePermutations(); - else setPermutationToRender(0); + if (genPerms) { + generatePermutations(); + } else { + setPermutationToRender(0); + } } public PositionedStack(Object object, int x, int y) { @@ -77,10 +80,13 @@ public class PositionedStack { public void setPermutationToRender(int index) { item = items[index].copy(); - if (item.getItem() == null) item = new ItemStack(Blocks.fire); - else if (item.getItemDamage() == OreDictionary.WILDCARD_VALUE && item.getItem() != null - && item.getItem().isRepairable()) - item.setItemDamage(0); + + if (item.getItem() == null) { + item = new ItemStack(Blocks.fire); + } else if (item.getItemDamage() == OreDictionary.WILDCARD_VALUE && item.getItem() != null + && item.getItem().isRepairable()) { + item.setItemDamage(0); + } } public boolean contains(ItemStack ingredient) { diff --git a/src/main/java/codechicken/nei/PresetsWidget.java b/src/main/java/codechicken/nei/PresetsWidget.java index 55265fd4..7cd3b4e0 100644 --- a/src/main/java/codechicken/nei/PresetsWidget.java +++ b/src/main/java/codechicken/nei/PresetsWidget.java @@ -6,6 +6,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -30,39 +32,55 @@ import codechicken.core.CommonUtils; import codechicken.core.gui.GuiScrollSlot; import codechicken.lib.gui.GuiDraw; import codechicken.lib.vec.Rectangle4i; +import codechicken.nei.ItemList.ItemsLoadedCallback; import codechicken.nei.ItemPanel.ItemPanelSlot; +import codechicken.nei.api.API; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.IRecipeFilter.IRecipeFilterProvider; import codechicken.nei.api.ItemFilter; +import codechicken.nei.api.ItemFilter.ItemFilterProvider; import codechicken.nei.recipe.GuiRecipe; +import codechicken.nei.recipe.IRecipeHandler; import codechicken.nei.recipe.StackInfo; import codechicken.nei.util.NBTJson; import cpw.mods.fml.common.registry.GameRegistry; -public class PresetsWidget extends Widget { +public class PresetsWidget extends Widget implements ItemFilterProvider, ItemsLoadedCallback, ItemFilter { - public static class PresetTag implements ItemFilter { + protected static enum PresetTagState { + + WHITELIST, + BLACKLIST, + BLACKLIST_PLUS; + + public PresetTagState next() { + if (this == PresetTagState.WHITELIST) return PresetTagState.BLACKLIST; + if (this == PresetTagState.BLACKLIST) return PresetTagState.BLACKLIST_PLUS; + if (this == PresetTagState.BLACKLIST_PLUS) return PresetTagState.WHITELIST; + return this; + } + } + + public static class PresetTag { public String filename = null; public String displayName = ""; - public boolean whitelist = true; - public HashSet items; + public PresetTagState state = PresetTagState.BLACKLIST; + public ItemStackSet items; public PresetTag(String displayName) { - this(displayName, new HashSet<>(), false, null); + this(displayName, null, PresetTagState.BLACKLIST, null); } - public PresetTag(String displayName, HashSet items) { - this(displayName, items, true, null); - } - - public PresetTag(String displayName, HashSet items, boolean whitelist, String filename) { + public PresetTag(String displayName, ItemStackSet items, PresetTagState state, String filename) { this.displayName = displayName; - this.items = items != null ? items : new HashSet<>(); - this.whitelist = whitelist; + this.items = items != null ? items : new ItemStackSet(); + this.state = state; this.filename = filename; } public boolean matches(final ItemStack stack) { - return items.contains(StackInfo.getItemStackGUID(stack)) == whitelist; + return items.contains(stack) == (state == PresetTagState.WHITELIST); } public static PresetTag loadFromFile(File file) { @@ -84,14 +102,18 @@ public class PresetsWidget extends Widget { final JsonParser parser = new JsonParser(); final JsonObject metaObject = parser.parse(itemStrings.remove(0)).getAsJsonObject(); final String displayName = metaObject.get("displayName").getAsString(); - final boolean whitelist = metaObject.get("whitelist").getAsBoolean(); - final HashSet items = new HashSet<>(); + final PresetTagState state = metaObject.has("state") + ? PresetTagState.valueOf(metaObject.get("state").getAsString()) + : PresetTagState.BLACKLIST; + final ItemStackSet items = new ItemStackSet(); - for (String itemStr : itemStrings) { - items.add(itemStr); + for (ItemStack stack : ItemList.items) { + if (itemStrings.contains(StackInfo.getItemStackGUID(stack))) { + items.add(stack); + } } - return new PresetTag(displayName, items, whitelist, file.getName()); + return new PresetTag(displayName, items, state, file.getName()); } catch (Throwable th) { NEIClientConfig.logger.error("Failed to load presets ItemStack from file", file); } @@ -127,12 +149,16 @@ public class PresetsWidget extends Widget { final JsonObject row = new JsonObject(); row.add("displayName", new JsonPrimitive(displayName)); - row.add("whitelist", new JsonPrimitive(whitelist)); + row.add("state", new JsonPrimitive(state.toString())); strings.add(NBTJson.toJson(row)); try { - strings.addAll(items); + + for (ItemStack stack : items.values()) { + strings.add(StackInfo.getItemStackGUID(stack)); + } + } catch (JsonSyntaxException e) { NEIClientConfig.logger.error("Failed to stringify presets ItemStack to json string"); } @@ -146,9 +172,8 @@ public class PresetsWidget extends Widget { } } - @SuppressWarnings("unchecked") public PresetTag copy() { - return new PresetTag(displayName, (HashSet) items.clone(), whitelist, null); + return new PresetTag(displayName, new ItemStackSet().addAll(items.values()), state, null); } } @@ -290,7 +315,7 @@ public class PresetsWidget extends Widget { selected.remove(tag); tag.deleteFile(); } else if (direction.contains(mx, my)) { - tag.whitelist = !tag.whitelist; + tag.state = tag.state.next(); tag.saveToFile(); } @@ -299,6 +324,7 @@ public class PresetsWidget extends Widget { PresetsWidget.openListBox = !presets.isEmpty() && (NEIClientUtils.controlKey() || NEIClientUtils.shiftKey() || !option.contains(mx, my)); ItemList.updateFilter.restart(); + PresetsRecipeFilter.blacklist = null; } @Override @@ -319,13 +345,22 @@ public class PresetsWidget extends Widget { final String displayName = NEIClientUtils .cropText(PresetsWidget.fontRenderer, tag.displayName, option.w - 6); - final String dirName = tag.whitelist ? translate("presets.whitelist.label") - : translate("presets.blacklist.label"); + String stateLabel = translate("presets.blacklist.label"); + + if (tag.state == PresetTagState.WHITELIST) { + stateLabel = translate("presets.whitelist.label"); + } else if (tag.state == PresetTagState.BLACKLIST_PLUS) { + stateLabel = translate("presets.blacklistPlus.label"); + } // blacklist or whitelist LayoutManager.getLayoutStyle() .drawSubsetTag(null, direction.x, direction.y, direction.w, direction.h, directionState, false); - GuiDraw.drawString(dirName, direction.x + 6, direction.y + 5, directionState == 2 ? 0xFFE0E0E0 : 0xFFFFA0); + GuiDraw.drawString( + stateLabel, + direction.x + 6, + direction.y + 5, + directionState == 2 ? 0xFFE0E0E0 : 0xFFFFA0); // option name LayoutManager.getLayoutStyle() @@ -433,6 +468,70 @@ public class PresetsWidget extends Widget { } } + protected static class PresetsRecipeFilter implements IRecipeFilterProvider, IRecipeFilter { + + protected static ItemStackSet blacklist; + + public IRecipeFilter getFilter() { + + if (PresetsWidget.edit != null) { + return null; + } + + if (blacklist == null) { + blacklist = new ItemStackSet(); + + for (PresetTag tag : listbox.getSelected()) { + if (tag.state == PresetTagState.BLACKLIST_PLUS) { + blacklist.addAll(tag.items.values()); + } + } + } + + return blacklist.isEmpty() ? null : this; + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + + if (matchPositionedStack(ingredients, false)) { + return false; + } + + if (result != null && matchPositionedStack(result)) { + return true; + } + + if (!others.isEmpty() && matchPositionedStack(others, true)) { + return true; + } + + return result == null && others.isEmpty(); + } + + private boolean matchPositionedStack(List items, boolean dir) { + for (PositionedStack pStack : items) { + if (matchPositionedStack(pStack) == dir) { + return true; + } + } + + return false; + } + + private boolean matchPositionedStack(PositionedStack pStack) { + for (ItemStack stack : pStack.items) { + if (!blacklist.contains(stack)) { + return true; + } + } + + return false; + } + + } + protected static final FontRenderer fontRenderer = Minecraft.getMinecraft().fontRenderer; protected static SubsetListBox listbox = new SubsetListBox(); protected static File presetsDir; @@ -517,7 +616,7 @@ public class PresetsWidget extends Widget { public boolean onButtonPress(boolean rightclick) { if (!rightclick && edit != null) { - edit.whitelist = !edit.whitelist; + edit.state = edit.state.next(); return true; } @@ -526,20 +625,54 @@ public class PresetsWidget extends Widget { @Override public String getRenderLabel() { - return edit != null && edit.whitelist ? translate("presets.whitelist.label") - : translate("presets.blacklist.label"); + + if (edit == null || edit.state == PresetTagState.BLACKLIST) { + return translate("presets.blacklist.label"); + } else if (edit.state == PresetTagState.WHITELIST) { + return translate("presets.whitelist.label"); + } else if (edit.state == PresetTagState.BLACKLIST_PLUS) { + return translate("presets.blacklistPlus.label"); + } + + return translate("presets.blacklist.label"); } @Override public void addTooltips(List tooltip) { if (edit != null) { - tooltip.add( - edit.whitelist ? translate("presets.whitelist.tooltip") - : translate("presets.blacklist.tooltip")); + if (edit.state == PresetTagState.BLACKLIST) { + tooltip.add(translate("presets.blacklist.tooltip")); + } else if (edit.state == PresetTagState.WHITELIST) { + tooltip.add(translate("presets.whitelist.tooltip")); + } else if (edit.state == PresetTagState.BLACKLIST_PLUS) { + tooltip.add(translate("presets.blacklistPlus.tooltip")); + } } } + }; + public void itemsLoaded() { + + if (presetsDir != null) { + listbox = new SubsetListBox(presetsDir); + + edit = null; + openListBox = false; + selectedDisplayName.setText(""); + + ItemList.updateFilter.restart(); + PresetsRecipeFilter.blacklist = null; + } + + } + + public PresetsWidget() { + API.addItemFilter(this); + API.addRecipeFilter(new PresetsRecipeFilter()); + ItemList.loadCallbacks.add(this); + } + protected static void setEditedTag(final PresetTag tag) { edit = tag; openListBox = false; @@ -564,19 +697,15 @@ public class PresetsWidget extends Widget { if (edit != null) { if (mouseSelection != null && mouseSelection.items.contains(item)) { - return mouseSelection.append != edit.whitelist; + return mouseSelection.append != (edit.state == PresetTagState.WHITELIST); } return !edit.matches(item); } - if (!listbox.getSelected().isEmpty()) { - final List selected = listbox.getSelected(); - - for (int idx = 0; idx < selected.size(); idx++) { - if (!selected.get(idx).matches(item)) { - return true; - } + for (PresetTag tag : listbox.getSelected()) { + if (!tag.matches(item)) { + return true; } } @@ -619,12 +748,10 @@ public class PresetsWidget extends Widget { } protected static void hideItem(final ItemStack stack, boolean append) { - final String guid = StackInfo.getItemStackGUID(stack); - if (append) { - edit.items.add(guid); + edit.items.add(stack); } else { - edit.items.remove(guid); + edit.items.remove(stack); } } @@ -637,24 +764,44 @@ public class PresetsWidget extends Widget { } public static void loadPresets(final String worldPath) { - final File dir = new File(CommonUtils.getMinecraftDir(), "saves/NEI/" + worldPath); presetsDir = new File(CommonUtils.getMinecraftDir(), "saves/NEI/" + worldPath + "/presets"); - if (!dir.exists()) { - dir.mkdirs(); + if (!presetsDir.getParentFile().exists()) { + presetsDir.getParentFile().mkdirs(); } if (!presetsDir.exists()) { presetsDir.mkdirs(); } - listbox = new SubsetListBox(presetsDir); + if (!(new File(presetsDir, "selected.ini")).exists()) { + final File configPresets = new File(NEIClientConfig.configDir, "/presets"); - edit = null; - openListBox = false; - selectedDisplayName.setText(""); + if (configPresets.exists()) { + for (File file : configPresets.listFiles()) { + try { + InputStream src = new FileInputStream(file); + OutputStream dst = new FileOutputStream(new File(presetsDir, file.getName())); + + IOUtils.copy(src, dst); + + src.close(); + dst.close(); + } catch (IOException e) {} + } + } + } + + if (LayoutManager.itemsLoaded) { + listbox = new SubsetListBox(presetsDir); + + edit = null; + openListBox = false; + selectedDisplayName.setText(""); + ItemList.updateFilter.restart(); + PresetsRecipeFilter.blacklist = null; + } - ItemList.updateFilter.restart(); } @Override @@ -903,16 +1050,20 @@ public class PresetsWidget extends Widget { return tooltip; } - public static boolean matches(ItemStack stack) { + @Override + public ItemFilter getFilter() { + return this; + } + + @Override + public boolean matches(ItemStack stack) { if (edit != null) { return true; } - final List selected = listbox.getSelected(); - - for (int idx = 0; idx < selected.size(); idx++) { - if (!selected.get(idx).matches(stack)) { + for (PresetTag tag : listbox.getSelected()) { + if (!tag.matches(stack)) { return false; } } diff --git a/src/main/java/codechicken/nei/RecipeSearchField.java b/src/main/java/codechicken/nei/RecipeSearchField.java new file mode 100644 index 00000000..d2736290 --- /dev/null +++ b/src/main/java/codechicken/nei/RecipeSearchField.java @@ -0,0 +1,78 @@ +package codechicken.nei; + +import net.minecraft.util.EnumChatFormatting; + +import codechicken.nei.api.ItemFilter; +import codechicken.nei.util.TextHistory; + +public abstract class RecipeSearchField extends TextField { + + private static final TextHistory history = new TextHistory(); + + public RecipeSearchField(String ident) { + super(ident); + this.field.setVisible(false); + } + + public boolean isVisible() { + return this.field.getVisible(); + } + + public void setVisible(boolean visible) { + this.field.setVisible(visible); + } + + protected abstract boolean noResults(); + + @Override + public int getTextColour() { + if (!text().isEmpty() && !noResults()) { + return focused() ? 0xFFcc3300 : 0xFF993300; + } else { + return focused() ? 0xFFE0E0E0 : 0xFF909090; + } + } + + @Override + public void lastKeyTyped(int keyID, char keyChar) { + + if (focused() && NEIClientConfig.isKeyHashDown("gui.getprevioussearch")) { + handleNavigateHistory(TextHistory.Direction.PREVIOUS); + } + + if (focused() && NEIClientConfig.isKeyHashDown("gui.getnextsearch")) { + handleNavigateHistory(TextHistory.Direction.NEXT); + } + } + + @Override + public String filterText(String s) { + return EnumChatFormatting.getTextWithoutFormattingCodes(s); + } + + public ItemFilter getFilter() { + return SearchField.getFilter(text()); + } + + @Override + public void setFocus(boolean focus) { + final boolean previousFocus = field.isFocused(); + + if (previousFocus != focus) { + history.add(text()); + } + + super.setFocus(focus); + } + + private boolean handleNavigateHistory(TextHistory.Direction direction) { + if (focused()) { + return history.get(direction, text()).map(newText -> { + setText(newText); + return true; + }).orElse(false); + } + return false; + } + +} diff --git a/src/main/java/codechicken/nei/SearchField.java b/src/main/java/codechicken/nei/SearchField.java index c929d40b..3be9b750 100644 --- a/src/main/java/codechicken/nei/SearchField.java +++ b/src/main/java/codechicken/nei/SearchField.java @@ -101,7 +101,6 @@ public class SearchField extends TextField implements ItemFilterProvider { if (button == 0) { if (focused() && (System.currentTimeMillis() - lastclicktime < 400)) { // double click NEIClientConfig.world.nbt.setBoolean("searchinventories", !searchInventories()); - NEIClientConfig.world.saveNBT(); } lastclicktime = System.currentTimeMillis(); } @@ -123,11 +122,11 @@ public class SearchField extends TextField implements ItemFilterProvider { if (isVisible() && NEIClientConfig.isKeyHashDown("gui.search")) { setFocus(true); } - if (NEIClientConfig.isKeyHashDown("gui.getprevioussearch") && focused()) { + if (focused() && NEIClientConfig.isKeyHashDown("gui.getprevioussearch")) { handleNavigateHistory(TextHistory.Direction.PREVIOUS); } - if (NEIClientConfig.isKeyHashDown("gui.getnextsearch") && focused()) { + if (focused() && NEIClientConfig.isKeyHashDown("gui.getnextsearch")) { handleNavigateHistory(TextHistory.Direction.NEXT); } } @@ -151,22 +150,33 @@ public class SearchField extends TextField implements ItemFilterProvider { try { pattern = Pattern.compile(search); } catch (PatternSyntaxException ignored) {} + return pattern == null || pattern.toString().length() == 0 ? null : pattern; } @Override public ItemFilter getFilter() { - String s_filter = text().toLowerCase(); + return getFilter(text()); + } + public static ItemFilter getFilter(String s_filter) { List primary = new LinkedList<>(); List secondary = new LinkedList<>(); + s_filter = s_filter.toLowerCase(); + for (ISearchProvider p : searchProviders) { ItemFilter filter = p.getFilter(s_filter); if (filter != null) (p.isPrimary() ? primary : secondary).add(filter); } - if (!primary.isEmpty()) return new AnyMultiItemFilter(primary); - if (!secondary.isEmpty()) return new AnyMultiItemFilter(secondary); + if (!primary.isEmpty()) { + return new AnyMultiItemFilter(primary); + } + + if (!secondary.isEmpty()) { + return new AnyMultiItemFilter(secondary); + } + return new EverythingItemFilter(); } diff --git a/src/main/java/codechicken/nei/SubsetWidget.java b/src/main/java/codechicken/nei/SubsetWidget.java index 9625f874..a3824142 100644 --- a/src/main/java/codechicken/nei/SubsetWidget.java +++ b/src/main/java/codechicken/nei/SubsetWidget.java @@ -533,7 +533,6 @@ public class SubsetWidget extends Button implements ItemFilterProvider, ItemsLoa NBTTagList list = dirtyHiddenItems.getAndSet(null); if (list != null) { NEIClientConfig.world.nbt.setTag("hiddenItems", list); - NEIClientConfig.world.saveNBT(); } } diff --git a/src/main/java/codechicken/nei/api/API.java b/src/main/java/codechicken/nei/api/API.java index 8f8b0130..8d9a1b7e 100644 --- a/src/main/java/codechicken/nei/api/API.java +++ b/src/main/java/codechicken/nei/api/API.java @@ -27,11 +27,13 @@ import codechicken.nei.SearchField; import codechicken.nei.SearchField.ISearchProvider; import codechicken.nei.SubsetWidget; import codechicken.nei.SubsetWidget.SubsetTag; +import codechicken.nei.api.IRecipeFilter.IRecipeFilterProvider; import codechicken.nei.api.ItemFilter.ItemFilterProvider; import codechicken.nei.config.Option; import codechicken.nei.config.OptionKeyBind; import codechicken.nei.recipe.CatalystInfo; import codechicken.nei.recipe.GuiCraftingRecipe; +import codechicken.nei.recipe.GuiRecipe; import codechicken.nei.recipe.GuiUsageRecipe; import codechicken.nei.recipe.ICraftingHandler; import codechicken.nei.recipe.IRecipeHandler; @@ -160,7 +162,7 @@ public class API { /** * Adds an item to the item panel. Any items added using this function will override the default search pattern. - * + * * @param item an item with data */ public static void addItemListEntry(ItemStack item) { @@ -183,7 +185,7 @@ public class API { /** * Add a custom KeyBinding to be configured in the Controls menu. - * + * * @param ident An identifier for your key, eg "shoot" * @param defaultKey The default value, commonly obtained from {@link Keyboard} */ @@ -195,7 +197,7 @@ public class API { /** * Add a custom KeyBinding to be configured in the Controls menu. - * + * * @param ident An identifier for your key, eg "shoot" * @param defaultKey The default value, commonly obtained from {@link Keyboard} */ @@ -210,7 +212,7 @@ public class API { /** * Add a new Layout Style for the NEI interface - * + * * @param styleID The Unique ID to be used for storing your style in the config and cycling through avaliable styles * @param style The style to add. */ @@ -220,7 +222,7 @@ public class API { /** * Registers a new Infinite Item Handler. - * + * * @param handler The handler to be registered. */ public static void addInfiniteItemHandler(IInfiniteItemHandler handler) { @@ -229,7 +231,7 @@ public class API { /** * Registers a new Infinite Item Handler. - * + * * @param block The block to handle, null for all. * @param handler The handler to be registered. */ @@ -239,7 +241,7 @@ public class API { /** * Tells NEI not to perform any Fast Transfer operations on slots of a particular class - * + * * @param slotClass The class of slot to be exempted */ public static void addFastTransferExemptSlot(Class slotClass) { @@ -248,7 +250,7 @@ public class API { /** * Register a new text handler for the block highlight tooltip with a layout specification (HEADER, BODY or FOOTER). - * + * * @param handler The handler to be registered. * @param layout A HUDAugmenterRegistry.Layout entry. HEADER is displayed before BODY which is displayed before * FOOTER. @@ -259,16 +261,22 @@ public class API { /** * Register a mode handler for overriding NEI recipe/utility/cheat mode settings. - * + * * @param handler The handler to be registered. */ public static void registerModeHandler(INEIModeHandler handler) { NEIInfo.modeHandlers.add(handler); } + public static void addRecipeFilter(IRecipeFilterProvider filterProvider) { + synchronized (GuiRecipe.recipeFilterers) { + GuiRecipe.recipeFilterers.add(filterProvider); + } + } + /** * Register a filter provider for the item panel. - * + * * @param filterProvider The filter provider to be registered. */ public static void addItemFilter(ItemFilterProvider filterProvider) { @@ -279,7 +287,7 @@ public class API { /** * Adds a new tag to the item subset dropdown. - * + * * @param name The fully qualified name, Eg Blocks.MobSpawners. NOT case sensitive * @param filter A filter for matching items that fit in this subset */ @@ -289,7 +297,7 @@ public class API { /** * Adds a new tag to the item subset dropdown. - * + * * @param name The fully qualified name, Eg Blocks.MobSpawners. NOT case sensitive * @param items An iterable of itemstacks to be added as a subset */ @@ -315,7 +323,7 @@ public class API { /** * Adds a new sorting option to the item panel sort menu - * + * * @param name A unique id for this sort option. Will be used in the config for saving and translated in the options * gui. Note that if the translation key name.tip exists, it will be used for a tooltip */ @@ -326,7 +334,7 @@ public class API { /** * Adds an additional item list entry for an item, sorted after the rest of the items are found through the normal * process - * + * * @param item The item to add the variant for * @param variant The stack to appear in the item panel */ @@ -345,7 +353,7 @@ public class API { * Adds an association between an ingredient and what it can craft. (i.e. Furnace ItemStack -> Smelting and Fuel * Recipes) Allows players to see what ingredient they need to craft in order to make recipes from a recipe * category. - * + * * @param stack the ingredient that can craft recipes (like a furnace or crafting table) * @param handler the recipe category handled by the ingredient * @param priority higher priority comes first, default to 0 @@ -366,7 +374,7 @@ public class API { * Adds an association between an ingredient and what it can craft. (i.e. Furnace ItemStack -> Smelting and Fuel * Recipes) Allows players to see what ingredient they need to craft in order to make recipes from a recipe * category. - * + * * @param stack the ingredient that can craft recipes (like a furnace or crafting table) * @param handlerID recipe category identifier (see also {@link RecipeCatalysts#getRecipeID(IRecipeHandler)}) * @param priority higher priority comes first, default to 0 diff --git a/src/main/java/codechicken/nei/api/IRecipeFilter.java b/src/main/java/codechicken/nei/api/IRecipeFilter.java new file mode 100644 index 00000000..bf5e7d03 --- /dev/null +++ b/src/main/java/codechicken/nei/api/IRecipeFilter.java @@ -0,0 +1,18 @@ +package codechicken.nei.api; + +import java.util.List; + +import codechicken.nei.PositionedStack; +import codechicken.nei.recipe.IRecipeHandler; + +public interface IRecipeFilter { + + public static interface IRecipeFilterProvider { + + public IRecipeFilter getFilter(); + } + + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others); + +} diff --git a/src/main/java/codechicken/nei/api/ShortcutInputHandler.java b/src/main/java/codechicken/nei/api/ShortcutInputHandler.java index 2cf5d8aa..7eefce1a 100644 --- a/src/main/java/codechicken/nei/api/ShortcutInputHandler.java +++ b/src/main/java/codechicken/nei/api/ShortcutInputHandler.java @@ -1,5 +1,8 @@ package codechicken.nei.api; +import static codechicken.lib.gui.GuiDraw.getMousePosition; + +import java.awt.Point; import java.util.List; import net.minecraft.client.gui.inventory.GuiContainer; @@ -7,11 +10,13 @@ import net.minecraft.item.ItemStack; import org.lwjgl.input.Mouse; +import codechicken.nei.ItemPanel.ItemPanelSlot; import codechicken.nei.ItemPanels; import codechicken.nei.LayoutManager; import codechicken.nei.NEIClientConfig; import codechicken.nei.NEIClientUtils; import codechicken.nei.PositionedStack; +import codechicken.nei.recipe.BookmarkRecipeId; import codechicken.nei.recipe.GuiCraftingRecipe; import codechicken.nei.recipe.GuiRecipe; import codechicken.nei.recipe.GuiUsageRecipe; @@ -28,6 +33,23 @@ public abstract class ShortcutInputHandler { return hideOverlayRecipe(); } + if (NEIClientConfig.isKeyHashDown("gui.bookmark_pull_items")) { + return ItemPanels.bookmarkPanel.pullBookmarkItems(ItemPanels.bookmarkPanel.getHoveredGroupId(true), false); + } + + if (NEIClientConfig.isKeyHashDown("gui.bookmark_pull_items_ingredients")) { + return ItemPanels.bookmarkPanel.pullBookmarkItems(ItemPanels.bookmarkPanel.getHoveredGroupId(true), true); + } + + if (stackover == null && NEIClientConfig.isKeyHashDown("gui.bookmark_recipe")) { + final int groupId = ItemPanels.bookmarkPanel.getHoveredGroupId(true); + + if (groupId != -1) { + ItemPanels.bookmarkPanel.removeGroup(groupId); + return true; + } + } + if (stackover == null) { return false; } @@ -77,7 +99,6 @@ public abstract class ShortcutInputHandler { if (stackover != null) { final int button = Mouse.getEventButton(); - stackover = stackover.copy(); if (button == 0) { return GuiCraftingRecipe.openRecipeGui("item", stackover); @@ -99,14 +120,28 @@ public abstract class ShortcutInputHandler { return false; } - private static boolean openOverlayRecipe(ItemStack stack, boolean shift) { + private static boolean openOverlayRecipe(ItemStack stackover, boolean shift) { final GuiContainer gui = NEIClientUtils.getGuiContainer(); if (gui == null || gui instanceof GuiRecipe) { return false; } - return GuiCraftingRecipe.openRecipeGui("item", true, shift, stack); + final Point mouseover = getMousePosition(); + ItemPanelSlot panelSlot = ItemPanels.bookmarkPanel.getSlotMouseOver(mouseover.x, mouseover.y); + BookmarkRecipeId recipeId = null; + + if (panelSlot != null) { + recipeId = ItemPanels.bookmarkPanel.getBookmarkRecipeId(panelSlot.slotIndex); + } else { + recipeId = ItemPanels.bookmarkPanel.getBookmarkRecipeId(stackover); + } + + if (recipeId != null) { + return GuiCraftingRecipe.overlayRecipe(stackover, recipeId, shift); + } + + return false; } private static boolean saveRecipeInBookmark(ItemStack stack, boolean saveIngredients, boolean saveStackSize) { @@ -128,4 +163,5 @@ public abstract class ShortcutInputHandler { return false; } + } diff --git a/src/main/java/codechicken/nei/recipe/BookmarkRecipeId.java b/src/main/java/codechicken/nei/recipe/BookmarkRecipeId.java index b10b23c3..56bf726f 100644 --- a/src/main/java/codechicken/nei/recipe/BookmarkRecipeId.java +++ b/src/main/java/codechicken/nei/recipe/BookmarkRecipeId.java @@ -16,10 +16,6 @@ import codechicken.nei.util.NBTJson; public class BookmarkRecipeId { - public int position = -1; - - public int recipetype = -1; - public String handlerName = null; public List ingredients = new ArrayList<>(); @@ -121,43 +117,6 @@ public class BookmarkRecipeId { return arr; } - /** - * Ensures the {@link BookmarkRecipeId#recipetype} and {@link BookmarkRecipeId#position} fields point to a valid - * recipe. This is needed, because the recipe IDs are loaded lazily on first use. - * - * @param recipeHandlers The subset of recipe handlers to search for the recipe - */ - public void updateTargetRecipe(List recipeHandlers) { - if (this.recipetype == -1 || this.position == -1) { - this.recipetype = 0; - this.position = 0; - - if (this.handlerName != null) { - - for (int j = 0; j < recipeHandlers.size(); j++) { - IRecipeHandler localHandler = recipeHandlers.get(j); - HandlerInfo localHandlerInfo = GuiRecipeTab.getHandlerInfo(localHandler); - - if (localHandlerInfo.getHandlerName().equals(this.handlerName)) { - - if (!this.ingredients.isEmpty()) { - for (int i = 0; i < localHandler.numRecipes(); i++) { - - if (this.equalsIngredients(localHandler.getIngredientStacks(i))) { - this.recipetype = j; - this.position = i; - break; - } - } - } - - break; - } - } - } - } - } - public boolean equals(Object anObject) { if (this == anObject) { return true; diff --git a/src/main/java/codechicken/nei/recipe/GuiCraftingRecipe.java b/src/main/java/codechicken/nei/recipe/GuiCraftingRecipe.java index 83b90db8..1425c95e 100644 --- a/src/main/java/codechicken/nei/recipe/GuiCraftingRecipe.java +++ b/src/main/java/codechicken/nei/recipe/GuiCraftingRecipe.java @@ -6,6 +6,8 @@ import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; @@ -16,6 +18,7 @@ import codechicken.nei.ItemPanels; import codechicken.nei.NEIClientConfig; import codechicken.nei.NEIClientUtils; import codechicken.nei.PositionedStack; +import codechicken.nei.recipe.stackinfo.GTFluidStackStringifyHandler; public class GuiCraftingRecipe extends GuiRecipe { @@ -23,50 +26,77 @@ public class GuiCraftingRecipe extends GuiRecipe { public static ArrayList serialCraftingHandlers = new ArrayList<>(); public static boolean openRecipeGui(String outputId, Object... results) { - return openRecipeGui(outputId, false, false, results); + return createRecipeGui(outputId, true, results) != null; } - public static boolean openRecipeGui(String outputId, final Boolean overlay, final Boolean shift, - Object... results) { - return createRecipeGui(outputId, overlay, shift, true, results) != null; - } - - public static GuiRecipe createRecipeGui(String outputId, final Boolean overlay, final Boolean shift, - final boolean open, Object... results) { + public static GuiRecipe createRecipeGui(String outputId, boolean open, Object... results) { final Minecraft mc = NEIClientUtils.mc(); + final BookmarkRecipeId recipeId; - final BookmarkRecipeId recipeId = "item".equals(outputId) - ? getRecipeId(mc.currentScreen, (ItemStack) results[0]) - : getCurrentRecipe(mc.currentScreen); - - if (overlay && recipeId == null) return null; + if ("item".equals(outputId)) { + results = Arrays.asList(results).stream().map(rslt -> normalizeItemStack((ItemStack) rslt)).toArray(); + recipeId = getRecipeId(mc.currentScreen, (ItemStack) results[0]); + } else if ("recipeId".equals(outputId)) { + recipeId = (BookmarkRecipeId) results[1]; + } else { + recipeId = getCurrentRecipe(mc.currentScreen); + } final ArrayList handlers = getCraftingHandlers(outputId, results); if (!handlers.isEmpty()) { - GuiCraftingRecipe gui = new GuiCraftingRecipe(handlers, recipeId); + final GuiCraftingRecipe gui = new GuiCraftingRecipe(handlers, "recipeId".equals(outputId)); if (open) { mc.displayGuiScreen(gui); } - if (recipeId != null) { - gui.openTargetRecipe(gui.recipeId); - } - - if (overlay) { - gui.overlayRecipe(gui.recipeId.position, shift); - } - + gui.openTargetRecipe(recipeId); return gui; } return null; } + public static boolean overlayRecipe(ItemStack stack, BookmarkRecipeId recipeId, final Boolean shift) { + + if (stack == null || recipeId == null) { + return false; + } + + final ArrayList handlers = getCraftingHandlers("recipeId", stack, recipeId); + + if (!handlers.isEmpty()) { + final GuiCraftingRecipe gui = new GuiCraftingRecipe(handlers, true); + final int recipe = gui.openTargetRecipe(recipeId); + + if (recipe != -1) { + gui.overlayRecipe(recipe, shift); + return true; + } + + } + + return false; + } + public static ArrayList getCraftingHandlers(String outputId, Object... results) { + ArrayList craftinghandlers = GuiCraftingRecipe.craftinghandlers; + ArrayList serialCraftingHandlers = GuiCraftingRecipe.serialCraftingHandlers; + Function recipeHandlerFunction; + + if ("recipeId".equals(outputId)) { + ItemStack stack = (ItemStack) results[0]; + BookmarkRecipeId recipeId = (BookmarkRecipeId) results[1]; + craftinghandlers = filterByHandlerName(craftinghandlers, recipeId.handlerName); + serialCraftingHandlers = filterByHandlerName(serialCraftingHandlers, recipeId.handlerName); + recipeHandlerFunction = h -> h.getRecipeHandler("item", stack); + } else { + recipeHandlerFunction = h -> h.getRecipeHandler(outputId, results); + } + final RecipeHandlerQuery recipeQuery = new RecipeHandlerQuery<>( - h -> h.getRecipeHandler(outputId, results), + recipeHandlerFunction, craftinghandlers, serialCraftingHandlers, "Error while looking up crafting recipe", @@ -76,6 +106,23 @@ public class GuiCraftingRecipe extends GuiRecipe { return recipeQuery.runWithProfiling("recipe.concurrent.crafting"); } + private static ArrayList filterByHandlerName(ArrayList craftinghandlers, + String handlerName) { + return craftinghandlers.stream().filter(h -> getHandlerName(h).equals(handlerName)) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private static String getHandlerName(ICraftingHandler handler) { + return GuiRecipeTab.getHandlerInfo(handler).getHandlerName(); + } + + private static ItemStack normalizeItemStack(ItemStack stack) { + GTFluidStackStringifyHandler.replaceAE2FCFluidDrop = true; + stack = StackInfo.loadFromNBT(StackInfo.itemStackToNBT(stack)); + GTFluidStackStringifyHandler.replaceAE2FCFluidDrop = false; + return stack; + } + protected static BookmarkRecipeId getRecipeId(GuiScreen gui, ItemStack stackover) { if (gui instanceof GuiRecipe) { @@ -97,10 +144,10 @@ public class GuiCraftingRecipe extends GuiRecipe { return ItemPanels.bookmarkPanel.getBookmarkRecipeId(stackover); } - private GuiCraftingRecipe(ArrayList handlers, BookmarkRecipeId recipeId) { + private GuiCraftingRecipe(ArrayList handlers, boolean limitToOneRecipe) { super(NEIClientUtils.mc().currentScreen); + this.limitToOneRecipe = limitToOneRecipe; this.currenthandlers = handlers; - this.recipeId = recipeId; } public static void registerRecipeHandler(ICraftingHandler handler) { diff --git a/src/main/java/codechicken/nei/recipe/GuiRecipe.java b/src/main/java/codechicken/nei/recipe/GuiRecipe.java index eb0f6f8f..0e133b08 100644 --- a/src/main/java/codechicken/nei/recipe/GuiRecipe.java +++ b/src/main/java/codechicken/nei/recipe/GuiRecipe.java @@ -7,11 +7,12 @@ 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 java.util.stream.IntStream; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; @@ -21,22 +22,30 @@ 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; @@ -62,7 +71,9 @@ public abstract class GuiRecipe extends GuiContainer i private static final int buttonWidth = 13; private static final int buttonHeight = 12; - private boolean limitToOneRecipe = false; + public static final List 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) @@ -81,9 +92,9 @@ public abstract class GuiRecipe extends GuiContainer i BG_BOTTOM_HEIGHT).build(); public ArrayList currenthandlers = new ArrayList<>(); - public int page; public int recipetype; + public BookmarkRecipeId recipeId; public ContainerRecipe slotcontainer; public GuiContainer firstGui; @@ -100,11 +111,164 @@ public abstract class GuiRecipe extends GuiContainer i private final Rectangle area = new Rectangle(); private final GuiRecipeTabs recipeTabs; private final GuiRecipeCatalyst guiRecipeCatalyst; - private H handler; + private SearchRecipeHandler 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 ingredients, PositionedStack result, + List others) { + + if (matchPositionedStack(ingredients)) { + return true; + } + + if (matchPositionedStack(result)) { + return true; + } + + if (matchPositionedStack(others)) { + return true; + } + + return false; + } + + private boolean matchPositionedStack(List 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 filters; + + public AllMultiRecipeFilter(List filters) { + this.filters = filters; + } + + public AllMultiRecipeFilter(IRecipeFilter filters) { + this(Arrays.asList(filters)); + } + + public AllMultiRecipeFilter() { + this(new ArrayList<>()); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List 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 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. @@ -119,7 +283,10 @@ public abstract class GuiRecipe extends GuiContainer i this.prevGui = prevgui; this.firstGuiGeneral = prevgui; - if (prevgui instanceof GuiContainer) this.firstGui = (GuiContainer) prevgui; + + if (prevgui instanceof GuiContainer) { + this.firstGui = (GuiContainer) prevgui; + } if (prevgui instanceof IGuiContainerOverlay) { this.firstGui = ((IGuiContainerOverlay) prevgui).getFirstScreen(); @@ -164,7 +331,7 @@ public abstract class GuiRecipe extends GuiContainer i trueGuiTop = guiTop; isHeightHackApplied = NEIClientConfig.heightHackHandlerRegex.stream() - .map(pattern -> pattern.matcher(handler.getHandlerId())).anyMatch(Matcher::matches); + .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 @@ -211,16 +378,11 @@ public abstract class GuiRecipe extends GuiContainer i } } - @Override - public void initGui() { - initGui(false); - } - @SuppressWarnings("unchecked") - public void initGui(boolean asWidget) { + public void initGui() { xSize = 176; ySize = Math.min(Math.max(height - 68, 166), 370); - if (!asWidget) { + if (!this.limitToOneRecipe) { super.initGui(); } else { this.guiLeft = (this.width - this.xSize) / 2; @@ -234,11 +396,14 @@ public abstract class GuiRecipe extends GuiContainer i this.width = scaledresolution.getScaledWidth(); this.height = scaledresolution.getScaledHeight(); } - guiTop = (height - ySize) / 2 + 10; - currenthandlers = getCurrentRecipeHandlers(); // Probably don't comment me out - if (handler == null) setRecipePage(recipetype); - else initOverlayButtons(); + guiTop = (height - ySize) / 2 + 10; + + if (handler == null) { + setRecipePage(recipetype); + } else { + initOverlayButtons(); + } checkYShift(); @@ -251,6 +416,9 @@ public abstract class GuiRecipe extends GuiContainer i 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) { @@ -293,6 +461,25 @@ public abstract class GuiRecipe extends GuiContainer i 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(); } @@ -301,14 +488,20 @@ public abstract class GuiRecipe extends GuiContainer i setRecipePage(idx, 0); } - public void setRecipePage(int idx, int position) { - recipetype = idx; - if (recipetype < 0) recipetype = currenthandlers.size() - 1; - else if (recipetype >= currenthandlers.size()) recipetype = 0; + public void setRecipePage(int idx, int refIndex) { + recipetype = (currenthandlers.size() + idx) % currenthandlers.size(); - handler = currenthandlers.get(recipetype); - handlerInfo = GuiRecipeTab.getHandlerInfo(handler); - page = Math.min(Math.max(0, position), handler.numRecipes() - 1) / getRecipesPerPage(); + 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(); @@ -318,22 +511,42 @@ public abstract class GuiRecipe extends GuiContainer i return Collections.unmodifiableList(Arrays.asList(overlayButtons)); } - public void openTargetRecipe(BookmarkRecipeId recipeId) { + public int openTargetRecipe(BookmarkRecipeId recipeId) { + int refIndex = -1; + int recipetype = 0; - if (recipeId == null) { - return; + 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; + } + } } - recipeId.updateTargetRecipe(currenthandlers); - setRecipePage(recipeId.recipetype, recipeId.position); + setRecipePage(recipetype, Math.max(0, refIndex)); + + return refIndex; } public List getFocusedRecipeIngredients() { - final int recipesPerPage = getRecipesPerPage(); - for (int idx = page * recipesPerPage; idx < handler.numRecipes() && idx < (page + 1) * recipesPerPage; idx++) { - if (recipeInFocus(idx)) { - return handler.getIngredientStacks(idx); + for (int recipeIndex : getRecipeIndices()) { + if (recipeInFocus(recipeIndex)) { + return handler.original.getIngredientStacks(recipeIndex); } } @@ -341,18 +554,17 @@ public abstract class GuiRecipe extends GuiContainer i } public int prepareFocusedRecipeResultStackSize(ItemStack stackover) { - final int recipesPerPage = getRecipesPerPage(); - for (int idx = page * recipesPerPage; idx < handler.numRecipes() && idx < (page + 1) * recipesPerPage; idx++) { - if (recipeInFocus(idx)) { - final PositionedStack result = handler.getResultStack(idx); + 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 stacks = handler.getOtherStacks(idx); + final List stacks = handler.original.getOtherStacks(recipeIndex); for (PositionedStack pStack : stacks) { if (StackInfo.equalItemAndNBT(pStack.item, stackover, true)) { stackSize += pStack.item.stackSize; @@ -366,15 +578,15 @@ public abstract class GuiRecipe extends GuiContainer i return stackover.stackSize; } - protected boolean recipeInFocus(int idx) { - final PositionedStack result = handler.getResultStack(idx); - if (result != null && isMouseOver(result, idx)) { + protected boolean recipeInFocus(int recipeIndex) { + final PositionedStack result = handler.original.getResultStack(recipeIndex); + if (result != null && isMouseOver(result, recipeIndex)) { return true; } - final List stacks = handler.getOtherStacks(idx); + final List stacks = handler.original.getOtherStacks(recipeIndex); for (PositionedStack stack : stacks) { - if (isMouseOver(stack, idx)) { + if (isMouseOver(stack, recipeIndex)) { return true; } } @@ -387,29 +599,54 @@ public abstract class GuiRecipe extends GuiContainer i } public H getHandler() { - return handler; + return handler.original; } public List getRecipeIndices() { final int recipesPerPage = getRecipesPerPage(); - int minIndex = page * recipesPerPage; - int maxIndex = Math.min(handler.numRecipes(), (page + 1) * recipesPerPage); + final int minIndex = page * recipesPerPage; + final int maxIndex = Math.min(this.numRecipes(), (page + 1) * recipesPerPage); + final ArrayList range = new ArrayList<>(); - return IntStream.range(minIndex, maxIndex).boxed().collect(Collectors.toList()); + 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 (i == Keyboard.KEY_ESCAPE) // esc - { + + if (searchField.isVisible() && searchField.focused() && searchField.handleKeyPress(i, c)) { + return; + } + + if (i == Keyboard.KEY_ESCAPE) { // esc mc.displayGuiScreen(firstGuiGeneral); NEICPH.sendRequestContainer(); return; } - if (GuiContainerManager.getManager(this).lastKeyTyped(i, c)) 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 recipe : getRecipeIndices()) if (handler.keyTyped(this, c, i, recipe)) return; + for (int recipeIndex : getRecipeIndices()) { + if (handler.original.keyTyped(this, c, i, recipeIndex)) { + return; + } + } } if (i == mc.gameSettings.keyBindInventory.getKeyCode()) { @@ -429,18 +666,35 @@ public abstract class GuiRecipe extends GuiContainer i } @Override - protected void mouseClicked(int x, int y, int button) { - try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) { - for (int recipe : getRecipeIndices()) if (handler.mouseClicked(this, button, recipe)) return; + 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); + } } - if (recipeTabs.mouseClicked(x, y, button)) return; + try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) { + for (int recipeIndex : getRecipeIndices()) { + if (handler.original.mouseClicked(this, button, recipeIndex)) { + return; + } + } + } - super.mouseClicked(x, y, button); + if (recipeTabs.mouseClicked(mousex, mousey, button)) { + return; + } + + super.mouseClicked(mousex, mousey, button); } @Override - public void mouseScrolled(int i) { + 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 @@ -448,23 +702,33 @@ public abstract class GuiRecipe extends GuiContainer i // First, invoke scroll handling over recipe handler tabbar. Makes sure it is not overwritten by recipe // handler-specific scroll behavior. - if (recipeTabs.mouseScrolled(i)) return; + if (recipeTabs.mouseScrolled(scroll)) return; - for (int recipe : getRecipeIndices()) if (handler.mouseScrolled(this, i, recipe)) 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 (i < 0) nextType(); - else prevType(); + 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 (i > 0) prevPage(); - else nextPage(); + if (scroll > 0) { + prevPage(); + } else { + nextPage(); + } } } @@ -485,11 +749,16 @@ public abstract class GuiRecipe extends GuiContainer i nextPage(); return; } + if (overlayButtons != null && guibutton.id >= OVERLAY_BUTTON_ID_START && guibutton.id < OVERLAY_BUTTON_ID_START + overlayButtons.length) { - overlayRecipe( - page * getRecipesPerPage() + guibutton.id - OVERLAY_BUTTON_ID_START, - NEIClientUtils.shiftKey()); + final List 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()); + } } } @@ -501,24 +770,43 @@ public abstract class GuiRecipe extends GuiContainer i /** updateScreen() that's safe to call when this isn't the currently open GUI */ public void updateAsTooltip() { - handler.onUpdate(); + handler.original.onUpdate(); refreshPage(); } @Override public List handleTooltip(GuiContainer gui, int mousex, int mousey, List currenttip) { + try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) { - for (int i : getRecipeIndices()) currenttip = handler.handleTooltip(this, currenttip, i); + 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 handleItemTooltip(GuiContainer gui, ItemStack stack, int mousex, int mousey, List currenttip) { + try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) { - for (int i : getRecipeIndices()) currenttip = handler.handleItemTooltip(this, stack, currenttip, i); + for (int recipeIndex : getRecipeIndices()) { + currenttip = handler.original.handleItemTooltip(this, stack, currenttip, recipeIndex); + } } return currenttip; @@ -530,13 +818,11 @@ public abstract class GuiRecipe extends GuiContainer i } private void nextPage() { - page++; - if (page > (handler.numRecipes() - 1) / getRecipesPerPage()) page = 0; + changePage(1); } private void prevPage() { - page--; - if (page < 0) page = (handler.numRecipes() - 1) / getRecipesPerPage(); + changePage(-1); } protected void nextType() { @@ -547,32 +833,46 @@ public abstract class GuiRecipe extends GuiContainer i setRecipePage(--recipetype); } - protected void overlayRecipe(int recipe, final boolean shift) { - boolean moveItems = shift || !NEIClientConfig.requireShiftForOverlayRecipe(); - if (handler == null || !handler.hasOverlay(firstGui, firstGui.inventorySlots, recipe)) { - mc.displayGuiScreen(firstGui); + protected void overlayRecipe(int recipeIndex, final boolean shift) { + + if (handler == null || !handler.original.hasOverlay(firstGui, firstGui.inventorySlots, recipeIndex)) { return; } - final IRecipeOverlayRenderer renderer = handler.getOverlayRenderer(firstGui, recipe); - final IOverlayHandler overlayHandler = handler.getOverlayHandler(firstGui, recipe); - mc.displayGuiScreen(firstGui); + 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, currenthandlers.get(recipetype), recipe, moveItems); + 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 = handler.numRecipes() > recipesPerPage; - - final int numRecipes = Math.min(handler.numRecipes() - (page * recipesPerPage), recipesPerPage); + 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; @@ -580,6 +880,17 @@ public abstract class GuiRecipe extends GuiContainer i 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) { @@ -587,12 +898,13 @@ public abstract class GuiRecipe extends GuiContainer i overlay.visible = false; } } else { + final List indices = getRecipeIndices(); for (int i = 0; i < overlayButtons.length; i++) { - if (i >= numRecipes) { + if (i >= indices.size()) { overlayButtons[i].visible = false; } else { - final int curRecipe = page * recipesPerPage + i; - overlayButtons[i].visible = handler.hasOverlay(firstGui, firstGui.inventorySlots, curRecipe); + overlayButtons[i].visible = handler.original + .hasOverlay(firstGui, firstGui.inventorySlots, indices.get(i)); } } } @@ -602,9 +914,11 @@ public abstract class GuiRecipe extends GuiContainer i private void refreshSlots() { slotcontainer.inventorySlots.clear(); - final int recipesPerPage = getRecipesPerPage(); - for (int i = page * recipesPerPage; i < handler.numRecipes() && i < (page + 1) * recipesPerPage; i++) { - Point p = getRecipePosition(i); + final List 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. @@ -612,36 +926,40 @@ public abstract class GuiRecipe extends GuiContainer i p.translate(0, 16); } - List stacks = handler.getIngredientStacks(i); + TemplateRecipeHandler.disableCycledIngredients = false; + + List stacks = handler.original.getIngredientStacks(recipeIndex); for (PositionedStack stack : stacks) slotcontainer.addSlot(stack, p.x, p.y); - stacks = handler.getOtherStacks(i); + stacks = handler.original.getOtherStacks(recipeIndex); for (PositionedStack stack : stacks) slotcontainer.addSlot(stack, p.x, p.y); - PositionedStack result = handler.getResultStack(i); + PositionedStack result = handler.original.getResultStack(recipeIndex); if (result != null) slotcontainer.addSlot(result, p.x, p.y); if (!limitToOneRecipe) { - List catalysts = RecipeCatalysts.getRecipeCatalysts(handler); + List 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 itemPresenceCacheSlots; - private void updateItemPresenceCache(int recipe) { + private void updateItemPresenceCache(int recipeIndex) { if (itemPresenceCacheSlots == null) { itemPresenceCacheSlots = new ArrayList<>(); } - itemPresenceCacheRecipe = recipe; + itemPresenceCacheRecipe = recipeIndex; itemPresenceCacheSlots.clear(); - List ingredients = handler.getIngredientStacks(recipe); + List ingredients = handler.original.getIngredientStacks(recipeIndex); ArrayList invStacks = ((List) firstGui.inventorySlots.inventorySlots).stream() .filter( s -> s != null && s.getStack() != null @@ -665,42 +983,73 @@ public abstract class GuiRecipe extends GuiContainer i GuiContainerManager.enable2DRender(); final int recipesPerPage = getRecipesPerPage(); final int ySkip = limitToOneRecipe ? 25 : 0; + if (!limitToOneRecipe) { - String s = handler.getRecipeName().trim(); + String s = handler.original.getRecipeName().trim(); fontRendererObj.drawStringWithShadow(s, (xSize - fontRendererObj.getStringWidth(s)) / 2, 5, 0xffffff); - s = NEIClientUtils.translate("recipe.page", page + 1, (handler.numRecipes() - 1) / recipesPerPage + 1); - fontRendererObj.drawStringWithShadow(s, (xSize - fontRendererObj.getStringWidth(s)) / 2, 19, 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(); GL11.glPushMatrix(); GL11.glTranslatef(5, 32 - ySkip + yShift, 0); try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) { - for (int i = page * recipesPerPage; i < handler.numRecipes() && i < (page + 1) * recipesPerPage; i++) { - handler.drawForeground(i); - if (drawItemPresence && (isMouseOverOverlayButton(i - page * recipesPerPage) || limitToOneRecipe) - && firstGui != null - && firstGui.inventorySlots != null) { - List ingredients = handler.getIngredientStacks(i); - if (itemPresenceCacheRecipe != i || itemPresenceCacheSlots == null + List 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 ingredients = handler.original.getIngredientStacks(recipeIndex); + + if (itemPresenceCacheRecipe != recipeIndex || itemPresenceCacheSlots == null || itemPresenceCacheSlots.size() != ingredients.size()) { - updateItemPresenceCache(i); + 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); } } + GL11.glTranslatef(0, handlerInfo.getHeight(), 0); } + } + GL11.glPopMatrix(); } @Override public void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) { - final int recipesPerPage = getRecipesPerPage(); final int ySkip = limitToOneRecipe ? 25 : 0; GL11.glColor4f(1, 1, 1, 1); @@ -732,10 +1081,12 @@ public abstract class GuiRecipe extends GuiContainer i GL11.glPushMatrix(); GL11.glTranslatef(guiLeft + 5, guiTop - ySkip + 32 + yShift, 0); try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) { - for (int i = page * recipesPerPage; i < handler.numRecipes() && i < (page + 1) * recipesPerPage; i++) { - handler.drawBackground(i); + + for (int recipeIndex : getRecipeIndices()) { + handler.original.drawBackground(recipeIndex); GL11.glTranslatef(0, handlerInfo.getHeight(), 0); } + } GL11.glPopMatrix(); } @@ -781,10 +1132,10 @@ public abstract class GuiRecipe extends GuiContainer i return firstGuiGeneral; } - public boolean isMouseOver(PositionedStack stack, int recipe) { - Slot stackSlot = slotcontainer - .getSlotWithStack(stack, getRecipePosition(recipe).x, getRecipePosition(recipe).y); + 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; @@ -808,13 +1159,19 @@ public abstract class GuiRecipe extends GuiContainer i } private int getRecipesPerPage(HandlerInfo handlerInfo) { - if (handlerInfo != null) return Math.max( - Math.min(((ySize - (buttonHeight * 3)) / handlerInfo.getHeight()), handlerInfo.getMaxRecipesPerPage()), - 1); - else return (handler.recipiesPerPage()); + 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 recipe) { + 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. @@ -822,7 +1179,7 @@ public abstract class GuiRecipe extends GuiContainer i 5, (isHeightHackApplied ? 16 : 32) - (limitToOneRecipe ? 25 : 0) + yShift - + ((recipe % getRecipesPerPage()) * handlerInfo.getHeight())); + + ((refIndex % getRecipesPerPage()) * handlerInfo.getHeight())); } public abstract ArrayList getCurrentRecipeHandlers(); @@ -844,9 +1201,27 @@ public abstract class GuiRecipe extends GuiContainer i @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 @@ -855,11 +1230,14 @@ public abstract class GuiRecipe extends GuiContainer i protected static BookmarkRecipeId getCurrentRecipe(GuiScreen gui) { - if (gui instanceof GuiRecipe) { - GuiRecipe gRecipe = (GuiRecipe) gui; + if (gui instanceof GuiRecipe && ((GuiRecipe) gui).handler.numRecipes() > 0) { + final GuiRecipe gRecipe = (GuiRecipe) gui; + final List indices = gRecipe.getRecipeIndices(); + final int curRecipe = indices.isEmpty() ? 0 : indices.get(0); + return new BookmarkRecipeId( gRecipe.handlerInfo.getHandlerName(), - gRecipe.getHandler().getIngredientStacks(gRecipe.page * gRecipe.getRecipesPerPage())); + gRecipe.handler.original.getIngredientStacks(curRecipe)); } return null; diff --git a/src/main/java/codechicken/nei/recipe/GuiRecipeTabs.java b/src/main/java/codechicken/nei/recipe/GuiRecipeTabs.java index b7f852de..75e0797f 100644 --- a/src/main/java/codechicken/nei/recipe/GuiRecipeTabs.java +++ b/src/main/java/codechicken/nei/recipe/GuiRecipeTabs.java @@ -43,7 +43,7 @@ public class GuiRecipeTabs { categoriesPerPage = 0; numHandlers = guiRecipe.currenthandlers.size(); - for (IRecipeHandler handler : guiRecipe.currenthandlers) { + for (int i = 0; i < numHandlers; i++) { if (totalWidth + tabWidth < (guiRecipe.xSize - 4)) { totalWidth += tabWidth; categoriesPerPage++; @@ -79,9 +79,11 @@ public class GuiRecipeTabs { IRecipeHandler handler = guiRecipe.currenthandlers.get(index); int tabX = area.x + (i * tabWidth); - if (NEIClientConfig.useCreativeTabStyle()) + if (NEIClientConfig.useCreativeTabStyle()) { tabs.add(new GuiRecipeTabCreative(guiRecipe, handler, tabX, area.y)); - else tabs.add(new GuiRecipeTabJEI(guiRecipe, handler, tabX, area.y)); + } else { + tabs.add(new GuiRecipeTabJEI(guiRecipe, handler, tabX, area.y)); + } } // Maybe add buttons diff --git a/src/main/java/codechicken/nei/recipe/GuiUsageRecipe.java b/src/main/java/codechicken/nei/recipe/GuiUsageRecipe.java index 9a1b6666..4453980b 100644 --- a/src/main/java/codechicken/nei/recipe/GuiUsageRecipe.java +++ b/src/main/java/codechicken/nei/recipe/GuiUsageRecipe.java @@ -4,9 +4,11 @@ import java.util.ArrayList; import java.util.Arrays; import net.minecraft.client.Minecraft; +import net.minecraft.item.ItemStack; import codechicken.nei.NEIClientConfig; import codechicken.nei.NEIClientUtils; +import codechicken.nei.recipe.stackinfo.GTFluidStackStringifyHandler; public class GuiUsageRecipe extends GuiRecipe { @@ -14,31 +16,50 @@ public class GuiUsageRecipe extends GuiRecipe { public static ArrayList serialUsageHandlers = new ArrayList<>(); public static boolean openRecipeGui(String inputId, Object... ingredients) { - RecipeHandlerQuery recipeQuery = new RecipeHandlerQuery<>( + + if ("item".equals(inputId)) { + ingredients = Arrays.asList(ingredients).stream().map(ingr -> normalizeItemStack((ItemStack) ingr)) + .toArray(); + } + + final ArrayList handlers = getUsageHandlers(inputId, ingredients); + + if (!handlers.isEmpty()) { + final Minecraft mc = NEIClientUtils.mc(); + final BookmarkRecipeId recipeId = getCurrentRecipe(mc.currentScreen); + final GuiUsageRecipe gui = new GuiUsageRecipe(handlers); + + mc.displayGuiScreen(gui); + gui.openTargetRecipe(recipeId); + return true; + } + + return false; + } + + private GuiUsageRecipe(ArrayList handlers) { + super(NEIClientUtils.mc().currentScreen); + this.currenthandlers = handlers; + } + + public static ArrayList getUsageHandlers(String inputId, Object... ingredients) { + + final RecipeHandlerQuery recipeQuery = new RecipeHandlerQuery<>( h -> getUsageOrCatalystHandler(h, inputId, ingredients), usagehandlers, serialUsageHandlers, "Error while looking up usage recipe", "inputId: " + inputId, "ingredients: " + Arrays.toString(ingredients)); - ArrayList handlers = recipeQuery.runWithProfiling("recipe.concurrent.usage"); - if (handlers.isEmpty()) return false; - Minecraft mc = NEIClientUtils.mc(); - BookmarkRecipeId recipeId = getCurrentRecipe(mc.currentScreen); - GuiUsageRecipe gui = new GuiUsageRecipe(handlers, recipeId); - - mc.displayGuiScreen(gui); - - gui.openTargetRecipe(gui.recipeId); - - return true; + return recipeQuery.runWithProfiling("recipe.concurrent.usage"); } - private GuiUsageRecipe(ArrayList handlers, BookmarkRecipeId recipeId) { - super(NEIClientUtils.mc().currentScreen); - this.currenthandlers = handlers; - this.recipeId = recipeId; + private static ItemStack normalizeItemStack(ItemStack stack) { + GTFluidStackStringifyHandler.replaceAE2FCFluidDrop = true; + stack = StackInfo.loadFromNBT(StackInfo.itemStackToNBT(stack)); + GTFluidStackStringifyHandler.replaceAE2FCFluidDrop = false; + return stack; } public static void registerUsageHandler(IUsageHandler handler) { diff --git a/src/main/java/codechicken/nei/recipe/IRecipeHandler.java b/src/main/java/codechicken/nei/recipe/IRecipeHandler.java index 76fb41aa..cf875f4c 100644 --- a/src/main/java/codechicken/nei/recipe/IRecipeHandler.java +++ b/src/main/java/codechicken/nei/recipe/IRecipeHandler.java @@ -52,14 +52,14 @@ public interface IRecipeHandler { /** * Draw the background of this recipe handler (basically the slot layout image). - * + * * @param recipe The recipe index to draw at this position. */ void drawBackground(int recipe); /** * Draw the foreground of this recipe handler (for things like progress bars). - * + * * @param recipe The recipe index to draw at this position. */ void drawForeground(int recipe); @@ -74,11 +74,11 @@ public interface IRecipeHandler { /** * - * @param recipetype The recipe index to get items for. + * @param recipe The recipe index to get items for. * @return A list of the other {@link PositionedStack}s in this recipe relative to the top left corner of your * recipe drawing space. For example fuel in furnaces. */ - List getOtherStacks(int recipetype); + List getOtherStacks(int recipe); /** * diff --git a/src/main/java/codechicken/nei/recipe/ProfilerRecipeHandler.java b/src/main/java/codechicken/nei/recipe/ProfilerRecipeHandler.java index 4da62de1..4bab4e2e 100644 --- a/src/main/java/codechicken/nei/recipe/ProfilerRecipeHandler.java +++ b/src/main/java/codechicken/nei/recipe/ProfilerRecipeHandler.java @@ -80,7 +80,7 @@ public class ProfilerRecipeHandler implements ICraftingHandler, IUsageHandler { } @Override - public ArrayList getOtherStacks(int recipetype) { + public ArrayList getOtherStacks(int recipe) { return new ArrayList<>(); } diff --git a/src/main/java/codechicken/nei/recipe/RecipeHandlerQuery.java b/src/main/java/codechicken/nei/recipe/RecipeHandlerQuery.java index bb837a28..122603c6 100644 --- a/src/main/java/codechicken/nei/recipe/RecipeHandlerQuery.java +++ b/src/main/java/codechicken/nei/recipe/RecipeHandlerQuery.java @@ -37,6 +37,7 @@ class RecipeHandlerQuery { profiler.start(profilerSection); try { ArrayList handlers = getRecipeHandlersParallel(); + if (error) { displayRecipeLookupError(); } @@ -60,27 +61,37 @@ class RecipeHandlerQuery { } private ArrayList getSerialHandlersWithRecipes() { + return serialRecipeHandlers.stream().map(handler -> { try { - return recipeHandlerFunction.apply(handler); + return isHidden(handler) ? null : recipeHandlerFunction.apply(handler); } catch (Throwable t) { printLog(t); error = true; return null; } - }).filter(h -> h != null && h.numRecipes() > 0).collect(Collectors.toCollection(ArrayList::new)); + }).filter(h -> h != null && h.numRecipes() > 0 && SearchRecipeHandler.findFirst(h, (recipeIndex) -> true) != -1) + .collect(Collectors.toCollection(ArrayList::new)); } private ArrayList getHandlersWithRecipes() throws InterruptedException, ExecutionException { + return ItemList.forkJoinPool.submit(() -> recipeHandlers.parallelStream().map(handler -> { try { - return recipeHandlerFunction.apply(handler); + return isHidden(handler) ? null : recipeHandlerFunction.apply(handler); } catch (Throwable t) { printLog(t); error = true; return null; } - }).filter(h -> h != null && h.numRecipes() > 0).collect(Collectors.toCollection(ArrayList::new))).get(); + }).filter(h -> h != null && h.numRecipes() > 0 && SearchRecipeHandler.findFirst(h, (recipeIndex) -> true) != -1) + .collect(Collectors.toCollection(ArrayList::new))).get(); + } + + private boolean isHidden(T handler) { + final String handlerName = handler.getHandlerId(); + final String handlerId = handler.getOverlayIdentifier(); + return NEIClientConfig.hiddenHandlers.stream().anyMatch(h -> h.equals(handlerId) || h.equals(handlerName)); } private void printLog(Throwable t) { diff --git a/src/main/java/codechicken/nei/recipe/SearchRecipeHandler.java b/src/main/java/codechicken/nei/recipe/SearchRecipeHandler.java new file mode 100644 index 00000000..2887b99c --- /dev/null +++ b/src/main/java/codechicken/nei/recipe/SearchRecipeHandler.java @@ -0,0 +1,120 @@ +package codechicken.nei.recipe; + +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import codechicken.nei.ItemList; +import codechicken.nei.api.IRecipeFilter; + +class SearchRecipeHandler { + + public H original; + + private ArrayList filteredRecipes; + + private ArrayList searchRecipes; + + public SearchRecipeHandler(H handler) { + this.original = handler; + + if (this.original.numRecipes() == 0) { + this.filteredRecipes = new ArrayList<>(); + } else { + final Stream items = IntStream.range(0, this.original.numRecipes()).boxed(); + final IRecipeFilter filter = this.searchingAvailable() ? GuiRecipe.getRecipeListFilter() : null; + + if (filter == null) { + this.filteredRecipes = items.collect(Collectors.toCollection(ArrayList::new)); + } else { + this.filteredRecipes = items.filter(recipe -> mathRecipe(this.original, recipe, filter)) + .collect(Collectors.toCollection(ArrayList::new)); + } + } + } + + protected static boolean mathRecipe(IRecipeHandler handler, int recipe, IRecipeFilter filter) { + return filter.matches( + handler, + handler.getIngredientStacks(recipe), + handler.getResultStack(recipe), + handler.getOtherStacks(recipe)); + } + + public boolean searchingAvailable() { + return SearchRecipeHandler.searchingAvailable(this.original); + } + + private static boolean searchingAvailable(IRecipeHandler handler) { + return handler instanceof TemplateRecipeHandler; + } + + public static int findFirst(IRecipeHandler handler, Predicate predicate) { + final IRecipeFilter filter = searchingAvailable(handler) ? GuiRecipe.getRecipeListFilter() : null; + int refIndex = -1; + + for (int recipeIndex = 0; recipeIndex < handler.numRecipes(); recipeIndex++) { + if (filter == null || mathRecipe(handler, recipeIndex, filter)) { + refIndex++; + + if (predicate.test(recipeIndex)) { + return refIndex; + } + } + } + + return -1; + } + + public ArrayList getSearchResult(IRecipeFilter filter) { + + if (filteredRecipes.isEmpty() || !this.searchingAvailable()) { + return null; + } + + ArrayList filtered = null; + final ArrayList recipes = IntStream.range(0, filteredRecipes.size()).boxed() + .collect(Collectors.toCollection(ArrayList::new)); + + try { + filtered = ItemList.forkJoinPool.submit( + () -> recipes.parallelStream() + .filter(recipe -> mathRecipe(this.original, filteredRecipes.get(recipe), filter)) + .collect(Collectors.toCollection(ArrayList::new))) + .get(); + + filtered.sort((a, b) -> a - b); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + + } + + return filtered; + } + + public void setSearchIndices(ArrayList searchRecipes) { + this.searchRecipes = searchRecipes; + } + + public int ref(int index) { + + if (searchRecipes != null) { + index = searchRecipes.get(index); + } + + return filteredRecipes.get(index); + } + + public int numRecipes() { + + if (searchRecipes != null) { + return searchRecipes.size(); + } + + return filteredRecipes.size(); + } + +} diff --git a/src/main/java/codechicken/nei/recipe/StackInfo.java b/src/main/java/codechicken/nei/recipe/StackInfo.java index b1164aec..161e03f0 100644 --- a/src/main/java/codechicken/nei/recipe/StackInfo.java +++ b/src/main/java/codechicken/nei/recipe/StackInfo.java @@ -8,14 +8,18 @@ import java.io.InputStreamReader; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.WeakHashMap; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraftforge.fluids.FluidContainerRegistry; import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.IFluidContainerItem; import org.apache.commons.io.IOUtils; @@ -29,6 +33,13 @@ public class StackInfo { public static final ArrayList stackStringifyHandlers = new ArrayList<>(); private static final HashMap> guidfilters = new HashMap<>(); private static final WeakHashMap guidcache = new WeakHashMap<>(); + private static final LinkedHashMap fluidcache = new LinkedHashMap() { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 20; + } + }; static { stackStringifyHandlers.add(new DefaultStackStringifyHandler()); @@ -49,11 +60,24 @@ public class StackInfo { return nbTag; } + public static ItemStack loadFromNBT(NBTTagCompound nbtTag, long customCount) { + + if (nbtTag != null) { + nbtTag = (NBTTagCompound) nbtTag.copy(); + nbtTag.setInteger("Count", (int) Math.max(Math.min(customCount, Integer.MAX_VALUE), 0)); + return loadFromNBT(nbtTag); + } + + return null; + } + public static ItemStack loadFromNBT(NBTTagCompound nbtTag) { ItemStack stack = null; - for (int i = stackStringifyHandlers.size() - 1; i >= 0 && nbtTag != null && stack == null; i--) { - stack = stackStringifyHandlers.get(i).convertNBTToItemStack(nbtTag); + if (nbtTag != null) { + for (int i = stackStringifyHandlers.size() - 1; i >= 0 && stack == null; i--) { + stack = stackStringifyHandlers.get(i).convertNBTToItemStack(nbtTag); + } } return stack; @@ -76,15 +100,24 @@ public class StackInfo { } public static FluidStack getFluid(ItemStack stack) { - FluidStack fluid = null; + FluidStack fluid = fluidcache.get(stack); - for (int i = stackStringifyHandlers.size() - 1; i >= 0 && fluid == null; i--) { - fluid = stackStringifyHandlers.get(i).getFluid(stack); + if (fluid == null && !fluidcache.containsKey(stack)) { + + for (int i = stackStringifyHandlers.size() - 1; i >= 0 && fluid == null; i--) { + fluid = stackStringifyHandlers.get(i).getFluid(stack); + } + + fluidcache.put(stack, fluid); } return fluid; } + public static boolean isFluidContainer(ItemStack stack) { + return stack.getItem() instanceof IFluidContainerItem || FluidContainerRegistry.isContainer(stack); + } + public static String getItemStackGUID(ItemStack stack) { if (!guidcache.containsKey(stack)) { diff --git a/src/main/java/codechicken/nei/recipe/TemplateRecipeHandler.java b/src/main/java/codechicken/nei/recipe/TemplateRecipeHandler.java index c9b8c599..87342f9c 100644 --- a/src/main/java/codechicken/nei/recipe/TemplateRecipeHandler.java +++ b/src/main/java/codechicken/nei/recipe/TemplateRecipeHandler.java @@ -34,6 +34,8 @@ import com.google.common.base.Stopwatch; import codechicken.lib.vec.Rectangle4i; import codechicken.nei.ItemList; +import codechicken.nei.ItemList.AllMultiItemFilter; +import codechicken.nei.LayoutManager; import codechicken.nei.NEIClientConfig; import codechicken.nei.NEIClientUtils; import codechicken.nei.NEIServerUtils; @@ -42,6 +44,8 @@ import codechicken.nei.api.DefaultOverlayRenderer; import codechicken.nei.api.IOverlayHandler; import codechicken.nei.api.IRecipeOverlayRenderer; import codechicken.nei.api.IStackPositioner; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.api.ItemInfo; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.guihook.IContainerInputHandler; import codechicken.nei.guihook.IContainerTooltipHandler; @@ -174,7 +178,7 @@ public abstract class TemplateRecipeHandler implements ICraftingHandler, IUsageH /** * This will perform default cycling of ingredients, mulitItem capable - * + * * @param cycle Current cycle step * @param ingredients List of ItemStacks to cycle * @return The provided list of ingredients, with their permutations cycled to a different permutation, if one @@ -182,30 +186,53 @@ public abstract class TemplateRecipeHandler implements ICraftingHandler, IUsageH */ public List getCycledIngredients(int cycle, List ingredients) { - if (!NEIClientUtils.shiftKey()) { + if (!NEIClientUtils.shiftKey() && !disableCycledIngredients) { if (NEIClientConfig.useJEIStyledCycledIngredients()) { jeiStyledRenderPermutation(ingredients, cycle); } else { - for (int itemIndex = 0; itemIndex < ingredients.size(); itemIndex++) { - randomRenderPermutation(ingredients.get(itemIndex), cycle + itemIndex); - } + randomRenderPermutation(ingredients, cycle); } } return ingredients; } - public void randomRenderPermutation(PositionedStack stack, long cycle) { - Random rand = new Random(cycle + offset); - stack.setPermutationToRender(Math.abs(rand.nextInt()) % stack.items.length); + protected void randomRenderPermutation(List stacks, long cycle) { + final ItemFilter filter = getItemFilter(); + final int randCycle = Math.abs(new Random(cycle + offset).nextInt()); + + for (PositionedStack stack : stacks) { + if (stack.items.length <= 1) continue; + ArrayList filtered = getFilteredIngredientAlternatives(stack, filter); + if (filtered.isEmpty()) { + stack.setPermutationToRender((int) (randCycle % stack.items.length)); + } else { + stack.setPermutationToRender(filtered.get((int) (randCycle % filtered.size()))); + } + } + } - public void jeiStyledRenderPermutation(List stacks, long cycle) { + protected void jeiStyledRenderPermutation(List stacks, long cycle) { + final ItemFilter filter = getItemFilter(); + for (PositionedStack stack : stacks) { - stack.setPermutationToRender((int) (cycle % stack.items.length)); + if (stack.items.length <= 1) continue; + ArrayList filtered = getFilteredIngredientAlternatives(stack, filter); + if (filtered.isEmpty()) { + stack.setPermutationToRender((int) (cycle % stack.items.length)); + } else { + stack.setPermutationToRender(filtered.get((int) (cycle % filtered.size()))); + } } } + @Deprecated + protected void randomRenderPermutation(PositionedStack stack, long cycle) { + Random rand = new Random(cycle + offset); + stack.setPermutationToRender((int) (Math.abs(rand.nextInt()) % stack.items.length)); + } + public void setIngredientPermutation(Collection ingredients, ItemStack ingredient) { for (PositionedStack stack : ingredients) { for (int i = 0; i < stack.items.length; i++) { @@ -396,11 +423,32 @@ public abstract class TemplateRecipeHandler implements ICraftingHandler, IUsageH */ public LinkedList transferRects = new LinkedList<>(); + public static boolean disableCycledIngredients = true; + public TemplateRecipeHandler() { loadTransferRects(); RecipeTransferRectHandler.registerRectsToGuis(getRecipeTransferRectGuis(), transferRects); } + protected static ItemFilter getItemFilter() { + return new AllMultiItemFilter( + item -> !ItemInfo.hiddenItems.contains(item), + LayoutManager.presetsPanel != null ? LayoutManager.presetsPanel.getFilter() : null, + GuiRecipe.searchField != null ? GuiRecipe.searchField.getFilter() : null); + } + + protected static ArrayList getFilteredIngredientAlternatives(PositionedStack stack, ItemFilter filter) { + final ArrayList filtered = new ArrayList<>(); + + for (int i = 0; i < stack.items.length; i++) { + if (filter.matches(stack.items[i])) { + filtered.add(i); + } + } + + return filtered; + } + /** * Add all RecipeTransferRects to the transferRects list during this call. Afterward they may be added to the input * handler for the corresponding guis from getRecipeTransferRectGuis @@ -541,7 +589,8 @@ public abstract class TemplateRecipeHandler implements ICraftingHandler, IUsageH public TemplateRecipeHandler newInstance() { try { - // Use the non parallel version since we might already be using the forkJoinPool and might have no threads + // Use the non parallel version since we might already be using the forkJoinPool + // and might have no threads // available // This will ideally have been pre-cached elsewhere and will be a NOOP findFuelsOnce(); @@ -680,8 +729,12 @@ public abstract class TemplateRecipeHandler implements ICraftingHandler, IUsageH @Override public boolean mouseClicked(GuiRecipe gui, int button, int recipe) { - if (button == 0) return transferRect(gui, recipe, false); - else if (button == 1) return transferRect(gui, recipe, true); + + if (button == 0) { + return transferRect(gui, recipe, false); + } else if (button == 1) { + return transferRect(gui, recipe, true); + } return false; } @@ -690,29 +743,58 @@ public abstract class TemplateRecipeHandler implements ICraftingHandler, IUsageH public boolean mouseScrolled(GuiRecipe gui, int scroll, int recipe) { if (!NEIClientUtils.shiftKey()) return false; - final Point offset = gui.getRecipePosition(recipe); final Point pos = getMousePosition(); + final Point offset = gui.getRecipePosition(recipe); final Point relMouse = new Point(pos.x - gui.guiLeft - offset.x, pos.y - gui.guiTop - offset.y); + final PositionedStack overStack = getIngredientMouseOver(relMouse.x, relMouse.y, recipe); - for (PositionedStack pStack : getIngredientStacks(recipe)) { - if ((new Rectangle4i(pStack.relx, pStack.rely, 18, 18)).contains(relMouse.x, relMouse.y)) { - int index = pStack.items.length + scroll; + if (overStack != null && overStack.items.length > 1) { + final ItemFilter filter = getItemFilter(); - for (int i = 0; i < pStack.items.length; i++) { - if (NEIServerUtils.areStacksSameTypeCraftingWithNBT(pStack.items[i], pStack.item)) { - index = index + i; - break; - } + if (NEIClientConfig.useJEIStyledCycledIngredients()) { + ItemStack stack = overStack.item; + for (PositionedStack pStack : getIngredientStacks(recipe)) { + shiftPermutationToRender(pStack, stack, scroll, filter); } - - pStack.setPermutationToRender(index % pStack.items.length); - return true; + } else { + shiftPermutationToRender(overStack, overStack.item, scroll, filter); } + + return true; } return false; } + private void shiftPermutationToRender(PositionedStack pStack, ItemStack stack, int scroll, ItemFilter filter) { + + for (int index = 0; index < pStack.items.length; index++) { + if (NEIServerUtils.areStacksSameTypeCraftingWithNBT(pStack.items[index], stack)) { + ArrayList filtered = getFilteredIngredientAlternatives(pStack, filter); + + if (filtered.isEmpty()) { + pStack.setPermutationToRender((int) ((pStack.items.length + scroll + index) % pStack.items.length)); + } else { + pStack.setPermutationToRender( + filtered.get((int) ((filtered.size() + scroll + index) % filtered.size()))); + } + + break; + } + } + } + + private PositionedStack getIngredientMouseOver(int mousex, int mousey, int recipe) { + + for (PositionedStack pStack : getIngredientStacks(recipe)) { + if ((new Rectangle4i(pStack.relx, pStack.rely, 18, 18)).contains(mousex, mousey)) { + return pStack; + } + } + + return null; + } + private boolean transferRect(GuiRecipe gui, int recipe, boolean usage) { Point offset = gui.getRecipePosition(recipe); return transferRect(gui, transferRects, offset.x, offset.y, usage); diff --git a/src/main/java/codechicken/nei/recipe/stackinfo/DefaultStackStringifyHandler.java b/src/main/java/codechicken/nei/recipe/stackinfo/DefaultStackStringifyHandler.java index 4b1b3607..37975c10 100644 --- a/src/main/java/codechicken/nei/recipe/stackinfo/DefaultStackStringifyHandler.java +++ b/src/main/java/codechicken/nei/recipe/stackinfo/DefaultStackStringifyHandler.java @@ -21,7 +21,7 @@ public class DefaultStackStringifyHandler implements IStackStringifyHandler { final NBTTagCompound nbTag = new NBTTagCompound(); nbTag.setString("strId", strId); - nbTag.setInteger("Count", Math.max(saveStackSize ? stack.stackSize : 1, 1)); + nbTag.setInteger("Count", saveStackSize ? stack.stackSize : 1); nbTag.setShort("Damage", (short) stack.getItemDamage()); if (stack.hasTagCompound()) { diff --git a/src/main/java/codechicken/nei/recipe/stackinfo/GTFluidStackStringifyHandler.java b/src/main/java/codechicken/nei/recipe/stackinfo/GTFluidStackStringifyHandler.java index abcd72b2..605fc835 100644 --- a/src/main/java/codechicken/nei/recipe/stackinfo/GTFluidStackStringifyHandler.java +++ b/src/main/java/codechicken/nei/recipe/stackinfo/GTFluidStackStringifyHandler.java @@ -2,6 +2,7 @@ package codechicken.nei.recipe.stackinfo; import java.lang.reflect.Method; +import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.fluids.Fluid; @@ -9,16 +10,18 @@ import net.minecraftforge.fluids.FluidRegistry; import net.minecraftforge.fluids.FluidStack; import codechicken.nei.api.IStackStringifyHandler; +import cpw.mods.fml.common.registry.GameRegistry; public class GTFluidStackStringifyHandler implements IStackStringifyHandler { - public static Class GTDisplayFluid = null; - public static Method getFluidDisplayStack = null; - public static Method getFluidFromDisplayStack = null; + protected static Class GTDisplayFluid = null; + protected static Method getFluidDisplayStack = null; + protected static Method getFluidFromDisplayStack = null; + public static boolean replaceAE2FCFluidDrop = false; static { try { - final Class gtUtility = Class.forName("gregtech.api.util.GT_Utility"); + final Class gtUtility = Class.forName("gregtech.api.util.GT_Utility"); GTDisplayFluid = Class.forName("gregtech.common.items.GT_FluidDisplayItem"); getFluidFromDisplayStack = gtUtility.getMethod("getFluidFromDisplayStack", ItemStack.class); @@ -31,13 +34,13 @@ public class GTFluidStackStringifyHandler implements IStackStringifyHandler { public NBTTagCompound convertItemStackToNBT(ItemStack stack, boolean saveStackSize) { - if (GTDisplayFluid != null && GTDisplayFluid.isInstance(stack.getItem())) { + if (replaceAE2FCFluidDrop || stack.getItem() != GameRegistry.findItem("ae2fc", "fluid_drop")) { final FluidStack fluidStack = getFluid(stack); if (fluidStack != null) { final NBTTagCompound nbTag = new NBTTagCompound(); nbTag.setString("gtFluidName", fluidStack.getFluid().getName()); - nbTag.setInteger("Count", saveStackSize ? fluidStack.amount : 1); + nbTag.setInteger("Count", saveStackSize ? fluidStack.amount : 144); return nbTag; } } @@ -66,17 +69,33 @@ public class GTFluidStackStringifyHandler implements IStackStringifyHandler { } public FluidStack getFluid(ItemStack stack) { + final Item item = stack.getItem(); - if (getFluidFromDisplayStack != null && GTDisplayFluid != null && GTDisplayFluid.isInstance(stack.getItem())) { - try { + try { + if (getFluidFromDisplayStack != null && GTDisplayFluid != null && GTDisplayFluid.isInstance(item)) { final Object obj = getFluidFromDisplayStack.invoke(null, stack); if (obj != null) { return (FluidStack) obj; } - } catch (Exception e) {} - } + } else if (item == GameRegistry.findItem("ae2fc", "fluid_packet")) { + NBTTagCompound nbtTag = stack.getTagCompound(); + + return FluidStack.loadFluidStackFromNBT((NBTTagCompound) nbtTag.getTag("FluidStack")); + } else if (item == GameRegistry.findItem("ae2fc", "fluid_drop")) { + NBTTagCompound nbtTag = stack.getTagCompound(); + Fluid fluid = FluidRegistry.getFluid(nbtTag.getString("Fluid").toLowerCase()); + + if (fluid != null) { + FluidStack fluidStack = new FluidStack(fluid, stack.stackSize); + if (nbtTag.hasKey("FluidTag")) { + fluidStack.tag = nbtTag.getCompoundTag("FluidTag"); + } + return fluidStack; + } + } + } catch (Exception e) {} return null; } diff --git a/src/main/resources/assets/nei/lang/en_US.lang b/src/main/resources/assets/nei/lang/en_US.lang index 4b7e9417..367e7d4a 100644 --- a/src/main/resources/assets/nei/lang/en_US.lang +++ b/src/main/resources/assets/nei/lang/en_US.lang @@ -112,6 +112,7 @@ nei.options.keys.gui.bookmark=Add Bookmark nei.options.keys.gui.bookmark_recipe=Add Bookmark with Recipe nei.options.keys.gui.bookmark_count=Add Bookmark with Count nei.options.keys.gui.bookmark_pull_items=Pull all Bookmarked Items +nei.options.keys.gui.bookmark_pull_items_ingredients=Pull all Bookmarked Ingredients nei.options.keys.gui.bookmark_recipe_count=Add Bookmark with Recipe & Count nei.options.keys.gui.hide_bookmarks=Toggle Bookmark Panel Visibility nei.options.keys.gui.overlay=Overlay Recipe @@ -155,12 +156,11 @@ nei.options.inventory.jei_style_recipe_catalyst.false=No nei.options.inventory.saveCurrentRecipeInBookmarksEnabled=Save Current Recipe in Bookmarks nei.options.inventory.saveCurrentRecipeInBookmarksEnabled.true=Enabled nei.options.inventory.saveCurrentRecipeInBookmarksEnabled.false=Disabled -nei.options.inventory.useNBTInBookmarks=Use NBT in Bookmarks -nei.options.inventory.useNBTInBookmarks.true=Yes -nei.options.inventory.useNBTInBookmarks.false=No -nei.options.inventory.recipeTooltipsEnabled=Show recipe tooltips in Bookmarks -nei.options.inventory.recipeTooltipsEnabled.true=Yes -nei.options.inventory.recipeTooltipsEnabled.false=No +nei.options.inventory.recipeTooltipsMode=Show recipe tooltips in Bookmarks +nei.options.inventory.recipeTooltipsMode.0=None +nei.options.inventory.recipeTooltipsMode.1=In Horizontal View +nei.options.inventory.recipeTooltipsMode.2=In Vertical View +nei.options.inventory.recipeTooltipsMode.3=Always nei.options.inventory.bookmarksAnimationEnabled=REI Style Animation in Bookmarks nei.options.inventory.bookmarksAnimationEnabled.true=Yes nei.options.inventory.bookmarksAnimationEnabled.false=No @@ -348,6 +348,8 @@ nei.presets.blacklist.label=B nei.presets.blacklist.tooltip=Blacklist nei.presets.whitelist.label=W nei.presets.whitelist.tooltip=Whitelist +nei.presets.blacklistPlus.label=B+ +nei.presets.blacklistPlus.tooltip=Blacklist Plus nei.presets.textfield.tooltip.1=Enter - save nei.presets.textfield.tooltip.2=Escape - cancel nei.presets.label.tooltip=Double click to edit diff --git a/src/main/resources/assets/nei/lang/zh_CN.lang b/src/main/resources/assets/nei/lang/zh_CN.lang index 682c1f18..e43edea7 100644 --- a/src/main/resources/assets/nei/lang/zh_CN.lang +++ b/src/main/resources/assets/nei/lang/zh_CN.lang @@ -147,9 +147,6 @@ nei.options.inventory.jei_style_recipe_catalyst.false=否 nei.options.inventory.saveCurrentRecipeInBookmarksEnabled=保存书签中的当前合成表 nei.options.inventory.saveCurrentRecipeInBookmarksEnabled.true=启用 nei.options.inventory.saveCurrentRecipeInBookmarksEnabled.false=禁用 -nei.options.inventory.useNBTInBookmarks=在书签中启用NBT -nei.options.inventory.useNBTInBookmarks.true=是 -nei.options.inventory.useNBTInBookmarks.false=否 nei.options.inventory.bookmarksAnimationEnabled=REI风格书签动画 nei.options.inventory.bookmarksAnimationEnabled.true=是 nei.options.inventory.bookmarksAnimationEnabled.false=否 @@ -271,7 +268,7 @@ nei.recipe.furnace=烧制 nei.recipe.fuel=燃料 nei.recipe.fuel.smeltCount=可冶炼%s个物品 # Deprecated -nei.recipe.fuel.required=需要%s个 +nei.recipe.fuel.required=需要%s个 # Deprecated nei.recipe.fuel.processed=可烧制%s个 # Deprecated diff --git a/src/main/resources/assets/nei/textures/nei_sprites.png b/src/main/resources/assets/nei/textures/nei_sprites.png index ff51bbd91827ee4ed5510c35674fec2ce528d6f0..ebf78d37adf3270b8bfdc54673adbc92e1eb83a1 100644 GIT binary patch literal 35117 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 IW%K_30k|C!PXGV_ literal 37711 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