Compare commits

..
33 Commits
Author SHA1 Message Date
5faed21218 Revert missing item count rendering changes from #1044 (#1091)
Co-authored-by: Martin Robertz <dream-master@gmx.net>
Co-authored-by: slprime <31038811+slprime@users.noreply.github.com>
2026-10-26 00:21:23 +02:00
Worive aa9da2e0a0 Fix NEI initialization aborting while logging plugin errors (#1093) 2026-10-25 17:10:38 +02:00
slprime e9a3a494d5 Add Mouse Binding (#1092) 2026-10-25 02:49:38 +03:00
slprime ebe83f970f Fix group drag between bookmark namespaces (#1090) 2026-10-24 01:09:00 +03:00
slprime ad9beb7f90 Fill crafting grid with one recipe when Ctrl is held (#1089) 2026-10-23 23:44:16 +03:00
slprime 68bdb51c6b Update tree camera and scrollbar dragging every frame (#1087) 2026-10-22 23:58:41 +03:00
slprime e4e1964dec Fix NPE in BookmarksGridSlot when dragging a group across namespaces (#1086) 2026-10-22 16:22:23 +03:00
slprime 560618024f Autocrafting Optimization (#1084) 2026-10-21 01:24:40 +03:00
slprime e2a2d2a9df Batch 9-slice quads in DrawableResource (#1082) 2026-10-19 15:47:52 +03:00
slprime 7163630cb1 Move 9-slice setup into DrawableBuilder (#1081) 2026-10-16 23:18:28 +03:00
RessedandMartin Robertz ab168ad6ba Make StrictIdentifierFilter (<modid:name:meta>) resolve item on first match (#1080)
Co-authored-by: Martin Robertz <dream-master@gmx.net>
2026-10-16 20:42:59 +03:00
Martin Robertz e13e3483f0 update+deps+bs 2026-10-16 10:20:51 +02:00
slprime de26547b64 Treat item aliases as translation keys (#1079) 2026-10-15 20:19:58 +03:00
slprime c49f8808fd Fix NPE in item panel getMask (#1078) 2026-10-15 18:59:55 +03:00
slprime 1c6cc8b693 Unify hovered recipe lookup into HoveredRecipe (#1076) 2026-10-12 22:03:07 +03:00
slprime 43ae1b613a Add recipe origins to handler debug widget (#1072) 2026-10-12 13:43:22 +03:00
Wilhelm Schuster 402c4325a6 Update java version in GitHub issue template (#1074) 2026-10-11 23:33:54 +02:00
slprime 550e85c456 Fix -@mod and !@mod search syntax in Extended mode (#1071) 2026-10-10 14:58:17 +03:00
slprime 1673079cbb ItemZoom: Add side priority option (#1070) 2026-10-10 14:36:28 +03:00
slprime e6174c446a Add options for vertical line count in chain tooltips (#1068) 2026-10-09 21:14:11 +03:00
slprime 869b4571b0 Add recipe cycle freeze option (#1067) 2026-10-09 19:47:38 +03:00
slprime 35f5400ecb Fix Crafting Tree NPE (#1064) 2026-10-06 19:57:10 +03:00
micvog 1e2801c891 Fix crash when returning to the crafting tree after its bookmark group was removed (#1063) 2026-10-06 19:46:37 +03:00
Ressedandslprime 1b3b495fec Fix crash while dragging bookmark group to another grid (#1062)
Co-authored-by: slprime <31038811+slprime@users.noreply.github.com>
2026-10-05 20:07:52 +03:00
slprime 2844ea35bc Fix Old Handlers Hack (#1061) 2026-10-04 22:07:41 +03:00
slprime c020ecbf1f Add Custom Renderer API for Fluids (#1059) 2026-10-02 01:22:58 +03:00
slprime a105a60a1a Fix Subset Width Calculation (#1058) 2026-10-01 23:16:35 +03:00
slprime 3a4a6734dc Fix NPE on GT fluid display without NBT (#1057) 2026-09-29 22:30:43 +03:00
slprime 11f9194de9 Fix EnderIo Tank (#1056) 2026-09-29 20:00:29 +03:00
slprimeandgithub-actions[bot] a685efb3b7 Recipe Group Optimization (#1054)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-28 00:19:29 +03:00
danyadevandMartin Robertz 9f3697bb67 fix cycling for fluid permutations (#1048)
Co-authored-by: Martin Robertz <dream-master@gmx.net>
2026-09-27 18:21:56 +03:00
slprime 18abb880f8 Add widget size to recipe event (#1053) 2026-09-27 14:13:27 +03:00
slprime 5868187410 Save Tree State (#1052) 2026-09-27 11:00:27 +03:00
89 changed files with 2807 additions and 916 deletions

No files matched your search

+2 -4
View File
@@ -33,12 +33,10 @@ body:
description: What Java version are you using? It's worth mentioning that if you play on Java9+ you should try update to latest minor release (e.g. prefer Java 17.0.6 over 17.0.2) of that version.
options:
- Java 8
- Java 9
- Java 11
- Java 17
- Java 19
- Java 20
- Java 21
- java 25
- java 26
- Other (Please Specify)
validations:
required: true
+4 -4
View File
@@ -5,8 +5,8 @@ configurations.create('gtnhlibEmbed') {
}
dependencies {
api('com.github.GTNewHorizons:CodeChickenCore:1.4.21:dev')
devOnlyNonPublishable('com.github.GTNewHorizons:GTNHLib:0.11.49:dev')
api('com.github.GTNewHorizons:CodeChickenCore:1.4.22:dev')
devOnlyNonPublishable('com.github.GTNewHorizons:GTNHLib:0.11.54:dev')
shadowImplementation('org.apache.commons:commons-csv:1.10.0')
@@ -23,8 +23,8 @@ dependencies {
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
testRuntimeOnly("org.lwjgl.lwjgl:lwjgl:2.9.3");
compileOnly("com.github.GTNewHorizons:GTNHLib:0.11.49") { transitive = false }
gtnhlibEmbed("com.github.GTNewHorizons:GTNHLib:0.11.49") { transitive = false }
compileOnly("com.github.GTNewHorizons:GTNHLib:0.11.54") { transitive = false }
gtnhlibEmbed("com.github.GTNewHorizons:GTNHLib:0.11.54") { transitive = false }
}
def embedArchiveOperations = gtnhGradle.archiveOperations
+1 -1
View File
@@ -17,7 +17,7 @@ pluginManagement {
}
plugins {
id 'com.gtnewhorizons.gtnhsettingsconvention' version '2.0.31'
id 'com.gtnewhorizons.gtnhsettingsconvention' version '2.0.34'
}
@@ -572,9 +572,10 @@ public class BookmarkPanel extends PanelWidget<BookmarkGrid> {
return;
} else if (this.sortableGroup != null) {
final int overRowIndex = grid.getRowIndex(mousey);
final int overRowIndex = Math.min(grid.getRowIndex(mousey), grid.getRows() - 1);
if (overRowIndex >= 0 && this.sortableGroup.groupId != this.grid.getRowGroupId(overRowIndex)) {
if (overRowIndex >= 0 && !isOverNamespaceControls(mousex, mousey)
&& !this.sortableGroup.matches(this.grid, this.grid.getRowGroupId(overRowIndex))) {
this.grid.moveGroup(this.sortableGroup, overRowIndex);
}
@@ -1022,14 +1023,18 @@ public class BookmarkPanel extends PanelWidget<BookmarkGrid> {
this.mouseDownSlot = -1;
}
@Override
public boolean onMouseWheel(int shift, int mousex, int mousey) {
if ((!inEditingState() || this.sortableGroup != null) && new Rectangle4i(
private boolean isOverNamespaceControls(int mousex, int mousey) {
return new Rectangle4i(
namespacePrev.x,
namespacePrev.y,
namespaceNext.x + namespaceNext.w - namespacePrev.x,
namespacePrev.h).contains(mousex, mousey)) {
namespacePrev.h).contains(mousex, mousey);
}
@Override
public boolean onMouseWheel(int shift, int mousex, int mousey) {
if ((!inEditingState() || this.sortableGroup != null) && isOverNamespaceControls(mousex, mousey)) {
if (shift > 0) {
this.storage.prevNamespace();
@@ -1105,10 +1110,10 @@ public class BookmarkPanel extends PanelWidget<BookmarkGrid> {
}
if (super.onMouseWheel(shift, mousex, mousey)) {
final int overRowIndex = grid.getRowIndex(mousey);
final int overRowIndex = Math.min(grid.getRowIndex(mousey), grid.getRows() - 1);
if (this.sortableGroup != null && overRowIndex >= 0
&& this.sortableGroup.groupId != this.grid.getRowGroupId(overRowIndex)) {
&& !this.sortableGroup.matches(this.grid, this.grid.getRowGroupId(overRowIndex))) {
this.grid.moveGroup(this.sortableGroup, overRowIndex);
}
@@ -316,6 +316,10 @@ public class FavoriteRecipes {
load();
}
public static boolean hasManual() {
return manualFavorites.size() > 0;
}
public static boolean containsManual(ItemStack stack) {
return getManualFavorite(stack) != null;
}
+18 -22
View File
@@ -7,40 +7,36 @@ import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
import codechicken.nei.drawable.DrawableBuilder;
import codechicken.nei.drawable.DrawableResource;
public class GuiNEIButton extends GuiButton {
protected static final ResourceLocation guiTex = new ResourceLocation("textures/gui/widgets.png");
private static final DrawableResource[] BACKGROUNDS = new DrawableResource[3];
static {
for (int state = 0; state < BACKGROUNDS.length; state++) {
BACKGROUNDS[state] = new DrawableBuilder(guiTex, 0, 46 + state * 20, 200, 20).setSlices(2, 2, 2, 2).build();
}
}
public GuiNEIButton(int i, int j, int k, int l, int i1, String s) {
super(i, j, k, l, i1, s);
}
protected DrawableResource getBackground(int mouseX, int mouseY, boolean mouseOver) {
return BACKGROUNDS[getHoverState(mouseOver)];
}
public void drawButton(Minecraft minecraft, int x, int y) {
if (!visible) return;
minecraft.renderEngine.bindTexture(guiTex);
GL11.glColor4f(1, 1, 1, 1);
boolean mouseOver = x >= xPosition && y >= yPosition && x < xPosition + width && y < yPosition + height;
int k = getHoverState(mouseOver);
drawTexturedModalRect(xPosition, yPosition, 0, 46 + k * 20, width / 2, height / 2); // top left
drawTexturedModalRect(xPosition + width / 2, yPosition, 200 - width / 2, 46 + k * 20, width / 2, height / 2); // top
// right
drawTexturedModalRect(
xPosition,
yPosition + height / 2,
0,
46 + k * 20 + 20 - height / 2,
width / 2,
height / 2); // bottom left
drawTexturedModalRect(
xPosition + width / 2,
yPosition + height / 2,
200 - width / 2,
46 + k * 20 + 20 - height / 2,
width / 2,
height / 2); // bottom right
mouseDragged(minecraft, x, y);
GL11.glColor4f(1, 1, 1, 1);
getBackground(x, y, mouseOver).draw(xPosition, yPosition, width, height);
mouseDragged(minecraft, x, y);
drawContent(minecraft, x, y, mouseOver);
}
@@ -55,7 +51,7 @@ public class GuiNEIButton extends GuiButton {
return ColorUtils.buttonLabelNormal.getColor();
}
protected void drawContent(Minecraft minecraft, int y, int x, boolean mouseOver) {
protected void drawContent(Minecraft minecraft, int x, int y, boolean mouseOver) {
FontRenderer fontrenderer = minecraft.fontRenderer;
int color = getTextColour(mouseOver);
+2 -1
View File
@@ -11,6 +11,7 @@ import java.util.regex.Pattern;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.StatCollector;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
@@ -103,7 +104,7 @@ public class ItemList {
}
for (String alias : ItemInfo.getAliases(item)) {
if (this.pattern.matcher(alias).find()) {
if (this.pattern.matcher(StatCollector.translateToLocal(alias)).find()) {
return true;
}
}
+1 -7
View File
@@ -75,13 +75,7 @@ public class ItemPanel extends PanelWidget<ItemsPanelGrid> {
}
public static void updateItemList(ArrayList<ItemStack> newItems) {
final ItemsPanelGrid grid = ItemPanels.itemPanel.getGrid();
if (grid.rawItems.size() != newItems.size()) {
grid.setPage(0);
}
grid.setItems(newItems);
ItemPanels.itemPanel.getGrid().setItems(newItems);
ItemPanels.itemPanel.realItems = newItems;
}
+29 -2
View File
@@ -111,8 +111,14 @@ public class ItemZoom extends Widget implements IContainerInputHandler {
this.xPosition = (availableAreaWidth - this.scale * SLOT_SIZE) / 2;
this.availableAreaWidth = (int) (availableAreaWidth / screenScale);
if (availableAreaWidth / screenScale >= mouse.x) {
this.xPosition += gui.width * screenScale - availableAreaWidth;
final float rightSideShift = gui.width * screenScale - availableAreaWidth;
final boolean leftSideFree = availableAreaWidth / screenScale < mouse.x;
final boolean rightSideFree = mouse.x < gui.width - availableAreaWidth / screenScale;
final boolean showOnRight = leftSideFree && rightSideFree ? !preferLeftSide() : !leftSideFree;
if (showOnRight) {
this.xPosition += rightSideShift;
}
try {
@@ -128,6 +134,22 @@ public class ItemZoom extends Widget implements IContainerInputHandler {
}
}
private boolean preferLeftSide() {
switch (NEIClientConfig.getIntSetting("inventory.itemzoom.preferredSide")) {
case 1:
return true;
case 2:
return false;
}
final int leftCount = NEIClientConfig.isBookmarkPanelHidden()
|| ItemPanels.bookmarkPanel.getGrid().getPerPage() == 0 ? 0 : ItemPanels.bookmarkPanel.getGrid().size();
final int rightCount = ItemPanels.itemPanel.getGrid().getPerPage() == 0 ? 0
: ItemPanels.itemPanel.getGrid().size();
return leftCount <= rightCount;
}
private float getPointSize(GuiContainer gui) {
final float screenScale = 1.0f / getScreenScale();
final float availableAreaWidth = (gui.width - (gui.xSize + gui.width) / 2) * screenScale;
@@ -197,6 +219,11 @@ public class ItemZoom extends Widget implements IContainerInputHandler {
@Override
public boolean lastKeyTyped(GuiContainer gui, char keyChar, int keyID) {
return false;
}
@Override
public boolean onHotkeyPressed(GuiContainer gui) {
if (KeyManager.isHashDown("itemzoom.toggle", NEIClientUtils.SHIFT_HASH)) {
NEIClientConfig.getSetting("inventory.itemzoom.enabled")
@@ -89,6 +89,10 @@ public class ItemsPanelGrid extends ItemsGrid<ItemsPanelGrid.ItemsPanelGridSlot,
public void refresh(GuiContainer gui) {
if (this.newItems != null) {
if (this.rawItems.size() != this.newItems.size()) {
this.page = 0;
}
this.groupItems.clear();
this.itemToGroup.clear();
this.forceExpand = false;
@@ -167,7 +167,7 @@ public class ItemsTooltipLineHandler implements ITooltipLineHandler {
protected int activeStackIndex = -1;
protected Dimension size = new Dimension();
protected final boolean showNames;
protected int maxNamedRows = Integer.MAX_VALUE;
protected int length = 0;
protected int rows = 0;
@@ -175,29 +175,25 @@ public class ItemsTooltipLineHandler implements ITooltipLineHandler {
protected int gridColumns = 0;
protected int gridCount = 0;
public static ItemsTooltipLineHandler grid(String label, List<ItemStack> items, int maxRows) {
return new ItemsTooltipLineHandler(label, items, true, maxRows, false);
}
public static ItemsTooltipLineHandler list(String label, List<ItemStack> items, int maxRows) {
return new ItemsTooltipLineHandler(label, items, true, maxRows, true);
public static ItemsTooltipLineHandler of(String label, List<ItemStack> items, int maxRows, int maxNamedRows) {
return new ItemsTooltipLineHandler(label, items, true, maxRows, maxNamedRows);
}
public ItemsTooltipLineHandler(String label, List<ItemStack> items) {
this(label, items, true, DEFAULT_MAX_ROWS, false);
this(label, items, true, DEFAULT_MAX_ROWS, 0);
}
public ItemsTooltipLineHandler(String label, List<ItemStack> items, boolean saveStackSize, int maxRows) {
this(label, items, saveStackSize, maxRows, false);
this(label, items, saveStackSize, maxRows, 0);
}
protected ItemsTooltipLineHandler(String label, List<ItemStack> items, boolean saveStackSize, int maxRows,
boolean showNames) {
int maxNamedRows) {
this.label = label;
this.items = groupingItemStacks(items);
this.amountRenderer = saveStackSize ? new TotalAmountRenderer() : new NoAmountRenderer();
this.length = this.items.size();
this.showNames = showNames;
this.maxNamedRows = maxNamedRows;
setMaxRows(maxRows);
}
@@ -214,7 +210,7 @@ public class ItemsTooltipLineHandler implements ITooltipLineHandler {
this.names.clear();
this.nameRows = 0;
if (!this.showNames || !layoutWithNames(maxRows)) {
if (!layoutWithNames(maxRows)) {
layoutAsGrid(maxRows);
}
@@ -228,7 +224,7 @@ public class ItemsTooltipLineHandler implements ITooltipLineHandler {
}
protected boolean layoutWithNames(int maxRows) {
final int maxNames = this.length <= maxRows ? this.length : maxRows - 1;
final int maxNames = Math.min(this.maxNamedRows, this.length <= maxRows ? this.length : maxRows - 1);
final int[] lineWidth = new int[maxNames + 1];
lineWidth[0] = labelWidth();
+46 -4
View File
@@ -4,6 +4,7 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.BooleanSupplier;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.KeyBinding;
@@ -29,6 +30,7 @@ public class KeyManager {
private static final Map<String, KeyBinding> keyBindings = new HashMap<>();
private static final Map<String, String> keyAliases = new HashMap<>();
private static volatile boolean pendingOptionsReload = false;
private static int clickedButton = -1;
static {
// Aliases for legacy keybinds
@@ -73,7 +75,8 @@ public class KeyManager {
final int keyCode = binding.getKeyCode();
if (keyCode < 0) {
return Mouse.isButtonDown(keyCode + 100);
final int button = keyCode + 100;
return Mouse.isButtonDown(button) || button == clickedButton;
} else if (keyCode > Keyboard.KEY_NONE && keyCode < Keyboard.KEYBOARD_SIZE) {
return Keyboard.isKeyDown(keyCode);
}
@@ -82,6 +85,40 @@ public class KeyManager {
return false;
}
/**
* Whether any NEI key binding is assigned to the given mouse button.
*/
public static boolean isMouseBound(int button) {
for (KeyBinding binding : keyBindings.values()) {
if (binding.getKeyCode() == button - 100) {
return true;
}
}
return false;
}
/**
* Runs a hotkey handler for a click of the given mouse button if the button is bound to any NEI key binding. The
* button is reported as down by {@link #isKeyDown} for the duration of the call, because the LWJGL button state can
* already be released when a quick click is processed.
*
* @return true if the click was consumed by the handler
*/
public static boolean handleMouseHotkey(int button, BooleanSupplier handler) {
if (!isMouseBound(button)) {
return false;
}
final int previous = clickedButton;
clickedButton = button;
try {
return handler.getAsBoolean();
} finally {
clickedButton = previous;
}
}
public static boolean isHashDown(String ident) {
return isHashDown(ident, 0);
}
@@ -107,18 +144,23 @@ public class KeyManager {
public static String getKeyName(String ident, int meta) {
final int keyCode = getKeyCode(ident);
return keyCode == Keyboard.KEY_NONE ? null : NEIKeyboardUtils.getKeyName(keyCode + meta);
return keyCode == Keyboard.KEY_NONE ? null : NEIKeyboardUtils.getKeyName(keyCode, meta);
}
public static String getKeyName(String ident, int meta, int mouseBind) {
final int keyCode = getKeyCode(ident);
return keyCode == Keyboard.KEY_NONE && mouseBind == NEIMouseUtils.MOUSE_BTN_NONE ? null
: getKeyName(keyCode + meta, mouseBind);
: getKeyName(keyCode, meta, mouseBind);
}
public static String getKeyName(int keyBind, int mouseBind) {
final int keyCode = NEIKeyboardUtils.unhash(keyBind);
return getKeyName(keyCode, keyBind - keyCode, mouseBind);
}
private static String getKeyName(int keyCode, int hash, int mouseBind) {
StringJoiner keyText = new StringJoiner(" + ");
String keyHash = keyBind == Keyboard.KEY_NONE ? "" : NEIKeyboardUtils.getKeyName(keyBind);
String keyHash = keyCode == Keyboard.KEY_NONE && hash == 0 ? "" : NEIKeyboardUtils.getKeyName(keyCode, hash);
String mouseHash = mouseBind == NEIMouseUtils.MOUSE_BTN_NONE ? "" : NEIMouseUtils.getKeyName(mouseBind);
if (!keyHash.isEmpty()) {
@@ -220,6 +220,20 @@ public class LayoutManager implements IContainerInputHandler, IContainerTooltipH
@Override
public boolean lastKeyTyped(GuiContainer gui, char keyChar, int keyID) {
if (isEnabled() && !isHidden()) {
for (Widget widget : controlWidgets) {
if (inputFocused == null) {
widget.lastKeyTyped(keyID, keyChar);
}
}
}
return false;
}
@Override
public boolean onHotkeyPressed(GuiContainer gui) {
if (KeyManager.isKeyDown("gui.hide")) {
toggleBooleanSetting("inventory.hidden");
return true;
@@ -234,8 +248,12 @@ public class LayoutManager implements IContainerInputHandler, IContainerTooltipH
return true;
}
if (isEnabled() && !isHidden()) {
for (Widget widget : controlWidgets) if (inputFocused == null) widget.lastKeyTyped(keyID, keyChar);
if (isEnabled() && !isHidden() && inputFocused == null) {
for (Widget widget : controlWidgets) {
if (widget.onHotkeyPressed()) {
return true;
}
}
}
return false;
@@ -61,6 +61,9 @@ public class NEICPH implements IClientPacketHandler {
case S2C.CACHE_BOOKMARK_DATA:
handleCacheBookmarkData(packet);
break;
case S2C.SYNC:
ServerSync.onReply(packet.readInt());
break;
}
}
@@ -37,7 +37,6 @@ import codechicken.lib.config.ConfigTag;
import codechicken.lib.config.ConfigTagParent;
import codechicken.nei.api.API;
import codechicken.nei.api.GuiInfo;
import codechicken.nei.api.IConfigureNEI;
import codechicken.nei.api.ItemInfo;
import codechicken.nei.api.NEIInfo;
import codechicken.nei.config.ConfigSet;
@@ -200,6 +199,11 @@ public class NEIClientConfig {
tag.getTag("inventory.bookmarks.showRecipeMarkerMode").setComment("Show Recipe Marker").getIntValue(0);
API.addOption(new OptionCycled("inventory.bookmarks.showRecipeMarkerMode", 3, true));
tag.getTag("inventory.bookmarks.maxVerticalTooltipRows")
.setComment("Max rows in vertical crafting chain tooltip lists (0 - disable, always use grid)")
.getIntValue(12);
API.addOption(new OptionIntegerField("inventory.bookmarks.maxVerticalTooltipRows", 0, 50));
tag.getTag("inventory.bookmarks.recipeMarkerColor").setComment("Color of the icon marker")
.getHexValue(0xADADAD);
API.addOption(
@@ -224,6 +228,10 @@ public class NEIClientConfig {
.getBooleanValue(false);
API.addOption(new OptionToggleButton("inventory.bookmarks.bookmarkRecipeWithCount", true));
tag.getTag("inventory.bookmarks.craftingTreeZoomWithoutModifier")
.setComment("Zoom Crafting Tree with mouse wheel without holding Ctrl").getBooleanValue(false);
API.addOption(new OptionToggleButton("inventory.bookmarks.craftingTreeZoomWithoutModifier", true));
tag.getTag("inventory.guirecipe.jeiStyleTabs").setComment("Enable/disable JEI Style Tabs")
.getBooleanValue(true);
API.addOption(new OptionToggleButtonBoubs("inventory.guirecipe.jeiStyleTabs", true));
@@ -263,6 +271,11 @@ public class NEIClientConfig {
.getBooleanValue(true);
API.addOption(new OptionToggleButton("inventory.guirecipe.scrollPages", true));
tag.getTag("inventory.guirecipe.freezeCycleOnHover")
.setComment("Stop cycling the recipe while hovering over an ingredient or a button")
.getBooleanValue(false);
API.addOption(new OptionToggleButton("inventory.guirecipe.freezeCycleOnHover", true));
tag.getTag("inventory.guirecipe.profile").getBooleanValue(false);
API.addOption(new OptionToggleButton("inventory.guirecipe.profile", true));
@@ -368,6 +381,11 @@ public class NEIClientConfig {
tag.getTag("inventory.itemzoom.onlySolid").getBooleanValue(false);
API.addOption(new OptionToggleButton("inventory.itemzoom.onlySolid", true));
tag.getTag("inventory.itemzoom.preferredSide")
.setComment("Which side to prefer showing the zoomed item on, when both sides are free.")
.getIntValue(0);
API.addOption(new OptionCycled("inventory.itemzoom.preferredSide", 3, true));
tag.getTag("inventory.itemzoom.helpText")
.setComment("Display name \"Item Zoom\" and the hotkey to toggle this mod below the zoomed item.")
.getBooleanValue(true);
@@ -1011,16 +1029,7 @@ public class NEIClientConfig {
? NEIClientConfig.pluginsList.parallelStream()
: NEIClientConfig.pluginsList.stream();
stream.forEach(clazz -> {
try {
IConfigureNEI config = (IConfigureNEI) clazz.getConstructor().newInstance();
config.loadConfig();
NEIModContainer.plugins.add(config);
logger.debug("Loaded {}", clazz.getName());
} catch (Throwable e) {
logger.error("Failed to Load {}", clazz.getName(), e);
}
});
PluginLoader.load(stream, NEIModContainer.plugins, logger);
KeyManager.requestOptionsReload();
RecipeCatalysts.loadCatalystInfo();
@@ -1102,6 +1111,10 @@ public class NEIClientConfig {
return getIntSetting("inventory.bookmarks.showRecipeMarkerMode");
}
public static int getMaxVerticalTooltipRows() {
return getIntSetting("inventory.bookmarks.maxVerticalTooltipRows");
}
public static boolean showItemQuantityWidget() {
return getBooleanSetting("inventory.showItemQuantityWidget");
}
@@ -1150,6 +1163,10 @@ public class NEIClientConfig {
return getBooleanSetting("inventory.bookmarks.ignorePotionOverlap");
}
public static boolean craftingTreeZoomWithoutModifier() {
return getBooleanSetting("inventory.bookmarks.craftingTreeZoomWithoutModifier");
}
public static boolean optimizeGuiOverlapComputation() {
return getBooleanSetting("inventory.optimize_gui_overlap_computation");
}
@@ -1158,6 +1175,10 @@ public class NEIClientConfig {
return getBooleanSetting("inventory.guirecipe.cycledIngredientsTooltip");
}
public static boolean freezeCycleOnHover() {
return getBooleanSetting("inventory.guirecipe.freezeCycleOnHover");
}
public static boolean requireShiftForOverlayRecipe() {
return getBooleanSetting("inventory.guirecipe.shiftOverlayRecipe");
}
@@ -92,9 +92,17 @@ public class NEISPH implements IServerPacketHandler {
case C2S.SEND_CHAT_ITEM_LINK:
handleSendChatItemLink(sender, packet);
break;
case C2S.SYNC:
handleSync(sender, packet.readInt());
break;
}
}
private void handleSync(EntityPlayerMP sender, int id) {
sender.sendContainerAndContentsToPlayer(sender.openContainer, sender.openContainer.getInventory());
new PacketCustom(channel, S2C.SYNC).writeInt(id).sendToPlayer(sender);
}
private void handleSendChatItemLink(EntityPlayerMP sender, PacketCustom packet) {
final NBTTagCompound payloadTag = packet.readNBTTagCompound();
NEIServerUtils.sendChatItemLink(sender, payloadTag);
@@ -24,6 +24,7 @@ public final class PacketIDs {
public static final int REQUEST_POTION_GUI = 24;
public static final int SET_DUMMY_SLOT = 25;
public static final int SEND_CHAT_ITEM_LINK = 26;
public static final int SYNC = 27;
}
public static final class S2C {
@@ -38,5 +39,6 @@ public final class PacketIDs {
public static final int SET_CREATIVE_PLUS_MODE = 23;
public static final int OPEN_POTION_GUI = 24;
public static final int CACHE_BOOKMARK_DATA = 25;
public static final int SYNC = 26;
}
}
@@ -305,7 +305,7 @@ public abstract class PanelWidget<T extends ItemsGrid<? extends ItemsGridSlot, ?
}
@Override
public boolean handleKeyPress(int keyID, char keyChar) {
public boolean onHotkeyPressed() {
final Point mouse = GuiDraw.getMousePosition();
if (!contains(mouse.x, mouse.y)) return false;
@@ -0,0 +1,38 @@
package codechicken.nei;
import java.util.Collection;
import java.util.stream.Stream;
import org.apache.logging.log4j.Logger;
import codechicken.nei.api.IConfigureNEI;
final class PluginLoader {
private PluginLoader() {}
static void load(Stream<Class<?>> plugins, Collection<IConfigureNEI> loadedPlugins, Logger logger) {
final ClassLoader classLoader = IConfigureNEI.class.getClassLoader();
plugins.forEach(clazz -> {
final Thread thread = Thread.currentThread();
final ClassLoader previousClassLoader = thread.getContextClassLoader();
// Common-pool workers may use the system loader, which cannot see FML-only classes. Keep FML's loader
// through error logging too: old Log4j can otherwise throw while resolving a plugin's stack frames.
try {
thread.setContextClassLoader(classLoader);
try {
IConfigureNEI config = (IConfigureNEI) clazz.getConstructor().newInstance();
config.loadConfig();
loadedPlugins.add(config);
logger.debug("Loaded {}", clazz.getName());
} catch (Throwable e) {
logger.error("Failed to Load {}", clazz.getName(), e);
}
} finally {
thread.setContextClassLoader(previousClassLoader);
}
});
}
}
@@ -23,6 +23,11 @@ public class PopupInputHandler implements IContainerInputHandler {
@Override
public boolean lastKeyTyped(GuiContainer gui, char keyChar, int keyID) {
return false;
}
@Override
public boolean onHotkeyPressed(GuiContainer gui) {
if (KeyManager.isHashDown("gui.enchant") && canPerformAction("enchant")) {
NEICPH.sendOpenEnchantmentWindow();
@@ -163,7 +163,7 @@ public class PositionedStack implements Cloneable {
}
public List<ItemStack> getFilteredPermutations(ItemFilter additionalFilter) {
List<ItemStack> items = Arrays.asList(this.items);
List<ItemStack> items = new ArrayList<>(Arrays.asList(this.items));
items = filteringPermutations(items, item -> !ItemInfo.isHidden(item));
items = filteringPermutations(items, PresetsList.getItemFilter());
@@ -361,7 +361,7 @@ public class PositionedStack implements Cloneable {
return;
}
FluidDrawer.drawFluid(
FluidDrawer.drawTank(
this.relx,
this.rely,
this.width,
@@ -48,15 +48,19 @@ public abstract class RecipeSearchField extends TextField implements ItemFilterP
}
@Override
public void lastKeyTyped(int keyID, char keyChar) {
public boolean onHotkeyPressed() {
if (focused() && KeyManager.isKeyDown("gui.getprevioussearch")) {
handleNavigateHistory(TextHistory.Direction.PREVIOUS);
return true;
}
if (focused() && KeyManager.isKeyDown("gui.getnextsearch")) {
handleNavigateHistory(TextHistory.Direction.NEXT);
return true;
}
return false;
}
@Override
@@ -227,18 +227,24 @@ public class SearchField extends TextField implements ItemFilterProvider {
}
@Override
public void lastKeyTyped(int keyID, char keyChar) {
public boolean onHotkeyPressed() {
if (isVisible() && KeyManager.isHashDown("gui.search")) {
setFocus(true);
return true;
}
if (focused() && KeyManager.isKeyDown("gui.getprevioussearch")) {
handleNavigateHistory(TextHistory.Direction.PREVIOUS);
return true;
}
if (focused() && KeyManager.isKeyDown("gui.getnextsearch")) {
handleNavigateHistory(TextHistory.Direction.NEXT);
return true;
}
return false;
}
@Override
@@ -445,7 +445,7 @@ public class SearchTokenParser {
final char ch = input.charAt(index);
if (!insideQuotes && delimiters.indexOf(ch) >= 0
&& (!space || index == 0 || input.charAt(index - 1) == ' ')) {
&& (!space || index == 0 || input.charAt(index - 1) == ' ' || lastEnd == index)) {
if (lastEnd == index) {
token += String.valueOf(ch);
@@ -0,0 +1,36 @@
package codechicken.nei;
import codechicken.lib.packet.PacketCustom;
import codechicken.nei.PacketIDs.C2S;
/**
* Waits until the server has processed everything the client sent before. The server handles slot clicks and NEI
* packets in the order they came and answers after it has sent the slot updates, so once the answer is here the client
* container holds what the server computed.
*/
public final class ServerSync {
private static int lastId;
private static volatile int awaiting;
private ServerSync() {}
public static void request() {
if (!NEIClientConfig.hasSMPCounterPart()) {
awaiting = 0;
return;
}
awaiting = ++lastId;
new PacketCustom(NEICPH.channel, C2S.SYNC).writeInt(awaiting).sendToServer();
}
public static boolean pending() {
return awaiting != 0;
}
static void onReply(int id) {
if (id == awaiting) awaiting = 0;
}
}
@@ -160,7 +160,7 @@ public class SubsetWidget extends Button implements ItemFilterProvider {
// 1 - partitial hidden
// 2 - enabled
public int state = 2;
public int calculatedWidth;
public volatile int calculatedWidth = -1;
public final List<ItemStack> items;
public final List<SubsetTag> children = new ArrayList<>();
protected final SubsetSlot slot = new SubsetSlot();
@@ -206,7 +206,7 @@ public class SubsetWidget extends Button implements ItemFilterProvider {
this.state = 2;
this.items.clear();
this.children.clear();
this.calculatedWidth = 0;
this.calculatedWidth = -1;
}
public void updateVisiblity(int mx, int my) {
@@ -261,6 +261,10 @@ public class SubsetWidget extends Button implements ItemFilterProvider {
}
public void resize(Rectangle4i screen, Rectangle4i parent, boolean dropRight) {
if (calculatedWidth < 0) {
calculatedWidth = this.children.stream().mapToInt(SubsetTag::nameWidth).max().orElse(0);
}
int height = Math.min(slot.contentHeight(), screen.h);
int width = Math.max(calculatedWidth + 2, this.items.isEmpty() ? 0 : 16 + MARGIN * 2);
int scrollbarWidth = slot.scrollbarDim().width;
@@ -686,12 +690,12 @@ public class SubsetWidget extends Button implements ItemFilterProvider {
for (SubsetTag tag : list) {
tag.children.sort(Comparator.comparing(SubsetTag::displayName));
tag.calculatedWidth = tag.children.stream().mapToInt(SubsetTag::nameWidth).max().orElse(0);
tag.calculatedWidth = -1;
}
root.children.removeIf(child -> child.children.isEmpty() && child.items.isEmpty());
root.children.sort(Comparator.comparing(SubsetTag::displayName));
root.calculatedWidth = root.children.stream().mapToInt(SubsetTag::nameWidth).max().orElse(0);
root.calculatedWidth = -1;
calculateVisibility(root);
} catch (Throwable e) {
@@ -29,6 +29,10 @@ public abstract class Widget {
public void lastKeyTyped(int keyID, char keyChar) {}
public boolean onHotkeyPressed() {
return false;
}
public boolean handleClickExt(int mx, int my, int button) {
return false;
}
@@ -162,6 +162,19 @@ public class WidgetContainer extends Widget {
}
@Override
public boolean onHotkeyPressed() {
for (int index = this.widgets.size() - 1; index >= 0; index--) {
final Widget widget = this.widgets.get(index);
if (widget.onHotkeyPressed()) {
return true;
}
}
return false;
}
@Override
public boolean isFocused() {
return this.isFocused;
@@ -12,6 +12,7 @@ import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.inventory.Slot;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraftforge.fluids.Fluid;
import org.lwjgl.input.Keyboard;
@@ -33,6 +34,7 @@ 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.item.FluidDrawer;
import codechicken.nei.recipe.CatalystInfo;
import codechicken.nei.recipe.GuiCraftingRecipe;
import codechicken.nei.recipe.GuiRecipe;
@@ -178,6 +180,8 @@ public class API {
/**
* Add an alias name that this item can be found by in the search field, in addition to its display name.
*
* @param aliases translation keys, localized at search time
*/
public static void setAliases(ItemStack item, String... aliases) {
final List<String> list = Arrays.asList(aliases).stream().filter(Objects::nonNull).map(String::trim)
@@ -375,6 +379,16 @@ public class API {
StackInfo.stackStringifyHandlers.add(handler);
}
/**
* Replaces the default rendering of a fluid in NEI fluid display slots and fluid tanks.
*
* @param fluid the fluid to render
* @param renderer the renderer, or null to restore the default rendering
*/
public static void registerFluidRenderer(Fluid fluid, IFluidRenderer renderer) {
FluidDrawer.registerFluidRenderer(fluid, renderer);
}
/**
* 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
@@ -0,0 +1,17 @@
package codechicken.nei.api;
import net.minecraftforge.fluids.Fluid;
/**
* Custom renderer for a fluid, used by NEI fluid display slots and fluid tanks in recipes.
* <p>
* Register with {@link API#registerFluidRenderer(Fluid, IFluidRenderer)}.
*/
public interface IFluidRenderer {
/**
* Renders one 16x16 tile of the fluid. Partial tiles of a tank are clipped by NEI. The amount overlay is drawn by
* NEI.
*/
void renderFluid(Fluid fluid, int x, int y);
}
@@ -19,7 +19,6 @@ import codechicken.nei.BookmarkContainerInfo;
import codechicken.nei.FavoriteRecipes;
import codechicken.nei.ItemPanels;
import codechicken.nei.ItemQuantityField;
import codechicken.nei.ItemsGrid.ItemsGridSlot;
import codechicken.nei.KeyManager;
import codechicken.nei.LayoutManager;
import codechicken.nei.NEIClientConfig;
@@ -38,6 +37,7 @@ import codechicken.nei.recipe.GuiCraftingRecipe;
import codechicken.nei.recipe.GuiFavoriteButton;
import codechicken.nei.recipe.GuiRecipe;
import codechicken.nei.recipe.GuiUsageRecipe;
import codechicken.nei.recipe.HoveredRecipe;
import codechicken.nei.recipe.Recipe;
import codechicken.nei.recipe.Recipe.RecipeId;
import codechicken.nei.recipe.RecipeHandlerRef;
@@ -415,41 +415,37 @@ public abstract class ShortcutInputHandler {
private static Recipe getFocusedRecipe(ItemStack stackover, int mousex, int mousey, boolean useFavorites) {
final GuiContainer gui = NEIClientUtils.getGuiContainer();
Recipe recipe = null;
if (useFavorites) {
ItemsGridSlot itemSlot = ItemPanels.itemPanel.getSlotMouseOver(mousex, mousey);
if (itemSlot == null) {
itemSlot = ItemPanels.itemPanel.historyPanel.getSlotMouseOver(mousex, mousey);
}
if (itemSlot == null) {
itemSlot = ItemPanels.itemPanel.craftablesPanel.getSlotMouseOver(mousex, mousey);
}
if (itemSlot != null) {
final RecipeId recipeId = itemSlot.getRecipeId();
if (recipeId != null && (recipe = Recipe.of(recipeId)) != null) {
recipe.getIngredients().stream().forEach(ingr -> {
final List<ItemStack> permutations = ingr.getPermutations();
for (int index = 0; index < permutations.size(); index++) {
if (FavoriteRecipes.contains(permutations.get(index))) {
ingr.setActiveIndex(index);
break;
}
}
});
}
if (gui instanceof GuiRecipe<?>guiRecipe) {
final Recipe focusedRecipe = guiRecipe.getFocusedRecipe();
if (focusedRecipe != null) {
return focusedRecipe;
}
}
if (gui instanceof GuiRecipe<?>guiRecipe) {
recipe = guiRecipe.getFocusedRecipe();
if (!useFavorites) {
return null;
}
final HoveredRecipe hovered = HoveredRecipe.of(gui, stackover);
if (hovered == null || hovered.source != HoveredRecipe.Source.ITEM_PANEL) {
return null;
}
final Recipe recipe = Recipe.of(hovered.recipeId);
if (recipe != null) {
recipe.getIngredients().stream().forEach(ingr -> {
final List<ItemStack> permutations = ingr.getPermutations();
for (int index = 0; index < permutations.size(); index++) {
if (FavoriteRecipes.contains(permutations.get(index))) {
ingr.setActiveIndex(index);
break;
}
}
});
}
return recipe;
@@ -529,7 +525,9 @@ public abstract class ShortcutInputHandler {
}
final BookmarksGridSlot slot = ItemPanels.bookmarkPanel.getSlotMouseOver(mousex, mousey);
final RecipeId recipeId = getHotkeyRecipeId(gui, mousex, mousey, stack, slot);
final HoveredRecipe hovered = HoveredRecipe.of(gui, stack);
final RecipeId recipeId = hovered == null || hovered.source == HoveredRecipe.Source.OTHER
|| hovered.source == HoveredRecipe.Source.BOOKMARK && hovered.favorite ? null : hovered.recipeId;
if (slot != null) {
final BookmarkGroup group = slot.getGroup();
@@ -729,36 +727,4 @@ public abstract class ShortcutInputHandler {
}
}
private static RecipeId getHotkeyRecipeId(GuiContainer gui, int mousex, int mousey, ItemStack stack,
BookmarksGridSlot slot) {
if (slot != null) {
return slot.getType() == BookmarkItemType.INGREDIENT ? null : slot.getRecipeId();
}
ItemsGridSlot itemSlot = ItemPanels.itemPanel.getSlotMouseOver(mousex, mousey);
if (itemSlot == null) {
itemSlot = ItemPanels.itemPanel.historyPanel.getSlotMouseOver(mousex, mousey);
}
if (itemSlot == null) {
itemSlot = ItemPanels.itemPanel.craftablesPanel.getSlotMouseOver(mousex, mousey);
}
if (itemSlot != null) {
return itemSlot.getRecipeId();
}
if (gui instanceof GuiRecipe<?>guiRecipe) {
final Recipe focusedRecipe = guiRecipe.getFocusedRecipe();
if (focusedRecipe != null) {
return focusedRecipe.getRecipeId();
}
}
return null;
}
}
@@ -14,6 +14,7 @@ import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.NEW;
import java.util.Map;
@@ -22,11 +23,14 @@ import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import codechicken.lib.asm.ASMBlock;
@@ -98,6 +102,48 @@ public class NEITransformer implements IClassTransformer {
}
});
if (Boolean.getBoolean("nei.recipeOrigins")) {
// Replace the plain ArrayList CraftingManager's constructor builds its recipe list with, so every recipe
// added anywhere (including vanilla's own, added in that same constructor) is observed for
// codechicken.nei.recipe.debug.RecipeOrigins. Must be done at classload time, since by the time any mod
// code runs vanilla has already populated the list. Capturing a stack trace per recipe is slow, so this is
// opt-in via -Dnei.recipeOrigins=true.
transformer.add(
new MethodTransformer(
new ObfMapping("net/minecraft/item/crafting/CraftingManager", "<init>", "()V")) {
@Override
public void transform(MethodNode mv) {
ASMHelper.logger.debug("NEI: Hooking CraftingManager recipe list for origin tracking");
TypeInsnNode newInsn = null;
MethodInsnNode initInsn = null;
// only the first ArrayList construction, the recipe list field
for (AbstractInsnNode insn : mv.instructions.toArray()) {
if (newInsn == null) {
if (insn.getOpcode() == NEW
&& "java/util/ArrayList".equals(((TypeInsnNode) insn).desc)) {
newInsn = (TypeInsnNode) insn;
}
} else if (insn.getOpcode() == INVOKESPECIAL
&& "java/util/ArrayList".equals(((MethodInsnNode) insn).owner)
&& "<init>".equals(((MethodInsnNode) insn).name)) {
initInsn = (MethodInsnNode) insn;
break;
}
}
if (newInsn == null || initInsn == null) {
ASMHelper.logger.warn("NEI: CraftingManager recipe list not found, origins disabled");
return;
}
newInsn.desc = initInsn.owner = "codechicken/nei/recipe/debug/RecipeOrigins$TrackingList";
}
});
}
// put glint alpha into the buffer correctly for exporting
transformer.add(
new MethodReplacer(
@@ -452,8 +452,9 @@ public class BookmarkGrid extends ItemsGrid<BookmarksGridSlot, BookmarkGrid.Book
if (!this.isEmpty() && this.gridGenerator.maxAbsoluteSlotIndex > 0) {
final GroupingItem groupingItem = ItemPanels.bookmarkPanel.groupingItem;
final int sortableGroupId = ItemPanels.bookmarkPanel.sortableGroup != null
? ItemPanels.bookmarkPanel.sortableGroup.groupId
: -3;
&& ItemPanels.bookmarkPanel.sortableGroup.grid == this
? ItemPanels.bookmarkPanel.sortableGroup.groupId
: -3;
final int rowShift = this.page * this.rows;
Map<Integer, Integer> groups = this.gridGenerator.rowToGroupId;
@@ -795,10 +796,10 @@ public class BookmarkGrid extends ItemsGrid<BookmarksGridSlot, BookmarkGrid.Book
this.bookmarkItems.add(item);
}
sortableGroup.grid = this;
sortableGroup.groupId = groupId;
sortableGroup.update(this, groupId);
this.onItemsChanged();
this.getMask();// force generate grid
overGroupId = getRowGroupId(overRowIndex);
}
if (sortableGroup.groupId == overGroupId) {
@@ -1284,7 +1285,7 @@ public class BookmarkGrid extends ItemsGrid<BookmarksGridSlot, BookmarkGrid.Book
for (BookmarksGridSlot slot : getMask()) {
Rectangle4i rect = getSlotRect(slot.slotIndex);
if (sortableGroup != null && slot.getGroupId() == sortableGroup.groupId) {
if (sortableGroup != null && sortableGroup.matches(this, slot.getGroupId())) {
NEIClientUtils.drawRect(rect.x, rect.y, rect.w, rect.h, DRAG_COLOR);
} else if (sortableItem != null && sortableItem.items.contains(slot.itemIndex)) {
NEIClientUtils.drawRect(rect.x, rect.y, rect.w, rect.h, DRAG_COLOR);
@@ -1313,7 +1314,7 @@ public class BookmarkGrid extends ItemsGrid<BookmarksGridSlot, BookmarkGrid.Book
final SortableItem sortableItem = LayoutManager.bookmarkPanel.sortableItem;
for (BookmarksGridSlot slot : getMask()) {
if ((sortableGroup == null || slot.getGroupId() != sortableGroup.groupId)
if ((sortableGroup == null || !sortableGroup.matches(this, slot.getGroupId()))
&& (sortableItem == null || !sortableItem.items.contains(slot.itemIndex))) {
slot.afterDraw(getSlotRect(slot.slotIndex), mouseContext);
}
@@ -1330,7 +1331,7 @@ public class BookmarkGrid extends ItemsGrid<BookmarksGridSlot, BookmarkGrid.Book
for (BookmarksGridSlot slot : getMask()) {
if ((sortableGroup == null || slot.getGroupId() != sortableGroup.groupId)
if ((sortableGroup == null || !sortableGroup.matches(this, slot.getGroupId()))
&& (sortableItem == null || !sortableItem.items.contains(slot.itemIndex))) {
final BookmarkItem item = slot.getBookmarkItem();
@@ -340,7 +340,8 @@ public class BookmarkGridGenerator {
0,
chainItem.getItem(),
null,
group);
group,
this.grid);
} else {
return new BookmarksGridSlot(
slotIndex,
@@ -350,7 +351,8 @@ public class BookmarkGridGenerator {
chainItem.getCalculatedMultiplier(),
chainItem.getItem(),
chainItem.calculatedType,
group);
group,
this.grid);
}
}
@@ -144,7 +144,7 @@ public class BookmarkItem {
private static synchronized String getItemGUID(ItemStack stack) {
final FluidStack fluidStack = StackInfo.getFluid(stack);
if (fluidStack != null) {
if (fluidStack != null && (fluidStack.amount > 0 || !StackInfo.isFluidContainer(stack))) {
return fluidStack.getFluid().getName() + ":" + fluidStack.tag;
} else {
@@ -23,6 +23,7 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import codechicken.nei.BookmarkPanel.BookmarkViewMode;
import codechicken.nei.ItemPanels;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.recipe.Recipe.RecipeId;
import codechicken.nei.util.NBTJson;
@@ -79,10 +80,21 @@ public class BookmarkStorage {
public int getNamespaceSize() {
if (this.namespaces.get(this.namespaces.size() - 1).size() > 0) {
this.namespaces.add(new BookmarkGrid());
} else if (this.activeNamespaceIndex == this.namespaces.size() - 2 && getActiveGrid().size() == 0) {
this.namespaces.remove(this.namespaces.size() - 1);
final int lastIndex = this.namespaces.size() - 1;
final BookmarkGrid lastGrid = this.namespaces.get(lastIndex);
final SortableGroup sortableGroup = ItemPanels.bookmarkPanel.sortableGroup;
if (lastGrid.size() > 0) {
if (sortableGroup == null || sortableGroup.grid != lastGrid || !sortableGroup.aloneInGrid) {
this.namespaces.add(new BookmarkGrid());
}
} else if (lastIndex > 0 && this.activeNamespaceIndex != lastIndex) {
final BookmarkGrid prevGrid = this.namespaces.get(lastIndex - 1);
if (sortableGroup != null && sortableGroup.grid == prevGrid && sortableGroup.aloneInGrid
|| prevGrid == getActiveGrid() && prevGrid.size() == 0) {
this.namespaces.remove(lastIndex);
}
}
return this.namespaces.size();
@@ -198,6 +210,11 @@ public class BookmarkStorage {
final BookmarkItem item = BookmarkItem.of(parser.parse(itemStr).getAsJsonObject());
if (item != null) {
if (!grid.groups.containsKey(item.groupId)) {
item.groupId = BookmarkGrid.DEFAULT_GROUP_ID;
}
grid.addItem(item, false);
} else {
wasError = true;
@@ -8,7 +8,6 @@ import org.lwjgl.opengl.GL11;
import codechicken.lib.vec.Rectangle4i;
import codechicken.nei.BookmarkPanel.BookmarkViewMode;
import codechicken.nei.ItemPanels;
import codechicken.nei.ItemsGrid.ItemsGridSlot;
import codechicken.nei.ItemsGrid.MouseContext;
import codechicken.nei.NEIClientConfig;
@@ -65,7 +64,8 @@ public class BookmarksGridSlot extends ItemsGridSlot {
protected final int rowIndex;
public BookmarksGridSlot(int slotIndex, int itemIndex, long realMultiplier, long shiftAmount,
long calculatedMultiplier, BookmarkItem bookmarkItem, CalculatedType calculatedType, BookmarkGroup group) {
long calculatedMultiplier, BookmarkItem bookmarkItem, CalculatedType calculatedType, BookmarkGroup group,
BookmarkGrid grid) {
super(
slotIndex,
itemIndex,
@@ -96,7 +96,6 @@ public class BookmarksGridSlot extends ItemsGridSlot {
: CalculatedType.RESULT;
}
final BookmarkGrid grid = ItemPanels.bookmarkPanel.getGrid();
final BookmarkItem prevItem = grid.getCalculatedItem(
grid.gridGenerator.slotToItem.getOrDefault(grid.gridGenerator.itemToSlot.get(this.itemIndex) - 1, -1));
this.isFirstOutput = this.itemIndex >= 0 && this.bookmarkItem.type == BookmarkItemType.RESULT
@@ -13,13 +13,23 @@ public class SortableGroup {
public int groupId;
public BookmarkGrid grid;
public boolean aloneInGrid;
public int shiftX = 0;
public int shiftY = 0;
public SortableGroup(BookmarkGrid grid, int groupId) {
update(grid, groupId);
}
public void update(BookmarkGrid grid, int groupId) {
this.grid = grid;
this.groupId = groupId;
this.aloneInGrid = grid.bookmarkItems.stream().allMatch(item -> item.groupId == groupId);
}
public boolean matches(BookmarkGrid grid, int groupId) {
return this.grid == grid && this.groupId == groupId;
}
public void postDraw(int mousex, int mousey) {
@@ -2,6 +2,8 @@ package codechicken.nei.bookmark.tree;
import java.awt.geom.Point2D;
import org.lwjgl.input.Mouse;
class Camera {
private static final float ZOOM_MIN = 0.1f;
@@ -39,23 +41,34 @@ class Camera {
&& y < this.widgetY + this.widgetH;
}
public void startDrag(int mouseX, int mouseY) {
public void startDrag(Point2D mouse) {
this.dragging = true;
this.lastDragX = mouseX;
this.lastDragY = mouseY;
this.lastDragX = (float) mouse.getX();
this.lastDragY = (float) mouse.getY();
}
public void drag(int mouseX, int mouseY) {
public void drag(Point2D mouse) {
if (!this.dragging) {
return;
}
final float mouseX = (float) mouse.getX();
final float mouseY = (float) mouse.getY();
this.scrollX -= (mouseX - this.lastDragX) / this.zoomLevel;
this.scrollY -= (mouseY - this.lastDragY) / this.zoomLevel;
this.lastDragX = mouseX;
this.lastDragY = mouseY;
}
public void update(Point2D mouse, int treeWidth, int treeHeight) {
if (this.dragging && Mouse.isButtonDown(0)) {
drag(mouse);
}
clampToBounds(treeWidth, treeHeight);
}
public void endDrag() {
this.dragging = false;
}
@@ -79,7 +92,6 @@ class Camera {
} else {
this.scrollY -= direction * this.widgetH / (SCROLL_STEP_DIVISOR * this.zoomLevel);
}
}
public void centerOn(float worldX, float worldY) {
@@ -142,7 +142,11 @@ class CraftingTreeCanvas {
public int treeWidth = 0;
public int treeHeight = 0;
private final Camera camera = new Camera();
private final Camera camera;
public CraftingTreeCanvas(Camera camera) {
this.camera = camera;
}
public void setViewport(int x, int y, int w, int h) {
this.camera.setViewport(x, y, w, h);
@@ -165,12 +169,12 @@ class CraftingTreeCanvas {
: -FIT_MARGIN;
}
public void startDrag(int mouseX, int mouseY) {
this.camera.startDrag(mouseX, mouseY);
public void startDrag(Point2D mouse) {
this.camera.startDrag(mouse);
}
public void drag(int mouseX, int mouseY) {
this.camera.drag(mouseX, mouseY);
public void drag(Point2D mouse) {
this.camera.drag(mouse);
}
public void endDrag() {
@@ -255,7 +259,7 @@ class CraftingTreeCanvas {
public void draw(Point2D mouse, Set<BookmarkItem> collapsedItems, SearchResult searchResult,
boolean useInventorySnapshot, ItemTreeSlot hoveredNode, ItemTreeSlot deleteHoverNode) {
this.camera.clampToBounds(this.treeWidth, this.treeHeight);
this.camera.update(mouse, this.treeWidth, this.treeHeight);
final float zScrollX = this.camera.scrollX * this.camera.zoomLevel;
final float zScrollY = this.camera.scrollY * this.camera.zoomLevel;
@@ -2,9 +2,12 @@ package codechicken.nei.bookmark.tree;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.bookmark.BookmarkGroup;
import codechicken.nei.bookmark.BookmarkItem;
class CraftingTreeState {
@@ -25,6 +28,8 @@ class CraftingTreeState {
HANDLERS
}
private static final Map<BookmarkGroup, CraftingTreeState> GROUP_STATES = new WeakHashMap<>();
private static final String KEY_STATS_VISIBLE = "craftingTree.statsVisible";
private static final String KEY_STATS_SECTIONS = "craftingTree.statsSections";
private static final String KEY_USE_INVENTORY_SNAPSHOT = "craftingTree.useInventorySnapshot";
@@ -35,6 +40,8 @@ class CraftingTreeState {
public boolean useInventorySnapshot = false;
public CollapseMode collapseMode = CollapseMode.EXPANDED;
public boolean initialized;
public final Camera camera = new Camera();
public final Set<BookmarkItem> collapsedItems = new HashSet<>();
public ItemTreeSlot replaceRecipeNode;
@@ -65,6 +72,9 @@ class CraftingTreeState {
? collapseModes[savedCollapseMode]
: CollapseMode.EXPANDED;
this.replaceRecipeNode = null;
this.searchItemResult = null;
this.searchStatsResult = null;
this.camera.endDrag();
}
public void toggleStatsPanelVisible() {
@@ -98,6 +108,10 @@ class CraftingTreeState {
NEIClientConfig.world.nbt.setInteger(KEY_COLLAPSE_MODE, this.collapseMode.ordinal());
}
public static CraftingTreeState of(BookmarkGroup group) {
return GROUP_STATES.computeIfAbsent(group, g -> new CraftingTreeState());
}
public SearchResult activeSearchResult() {
return this.searchItemResult != null ? this.searchItemResult : this.searchStatsResult;
}
@@ -39,6 +39,7 @@ import codechicken.lib.gui.GuiDraw;
import codechicken.nei.KeyManager;
import codechicken.nei.LRUCache;
import codechicken.nei.NEICPH;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.NEIClientUtils;
import codechicken.nei.PositionedStack;
import codechicken.nei.SearchField.GuiSearchField;
@@ -57,6 +58,7 @@ import codechicken.nei.drawable.DrawableResource;
import codechicken.nei.guihook.IContainerTooltipHandler;
import codechicken.nei.guihook.IGuiClientSide;
import codechicken.nei.guihook.IGuiHandleMouseWheel;
import codechicken.nei.guihook.IGuiHotkeyListener;
import codechicken.nei.recipe.AcceptsFollowingTooltipLineHandler;
import codechicken.nei.recipe.AutoCraftingManager;
import codechicken.nei.recipe.GuiCraftingRecipe;
@@ -76,7 +78,7 @@ import codechicken.nei.util.SlotInaccessible;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGuiClientSide, IGuiHandleMouseWheel,
IContainerTooltipHandler, IGuiContainerOverlay {
IGuiHotkeyListener, IContainerTooltipHandler, IGuiContainerOverlay {
public static class EventHandler {
@@ -101,24 +103,23 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
}
}
private static final int MARGIN = 4;
private static final int PADDING = 9;
private static final int PADDING_TOP = 22;
private static final DrawableResource BG_TEXTURE = new DrawableBuilder(
"nei:textures/gui/craftingtree.png",
0,
0,
64,
64).build();
64).setSlices(PADDING, PADDING, PADDING_TOP, PADDING).build();
private static final DateTimeFormatter SCREENSHOT_DATE_FORMAT = DateTimeFormatter
.ofPattern("yyyy-MM-dd_HH.mm.ss", Locale.ROOT);
private static final int MARGIN = 4;
private static final int PADDING = 9;
private static final int PADDING_TOP = 22;
private final LRUCache<String, String> handlerTitleCache = new LRUCache<>(100);
private GuiScreen prevGui;
private boolean isFirstScreen = true;
private final BookmarkGrid grid;
private final int groupId;
@@ -127,7 +128,7 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
private final StatsPanel statsPanel = new StatsPanel();
private final StatsSectionsMenu statsSectionsMenu = new StatsSectionsMenu();
private final CraftingTreeCanvas canvas = new CraftingTreeCanvas();
private final CraftingTreeCanvas canvas;
private boolean statsPanelVisible;
private GuiSearchField searchField;
@@ -136,7 +137,7 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
private AcceptsFollowingTooltipLineHandler acceptsFollowingTooltipLineHandler;
private RecipeItemsTooltipLineHandler recipeItemsTooltipLineHandler;
private final CraftingTreeState uiState = new CraftingTreeState();
private final CraftingTreeState uiState;
private final CraftingTreeToolbar toolbar;
private ItemTreeSlot hoveredNode;
@@ -148,6 +149,8 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
this.grid = grid;
this.groupId = groupId;
this.uiState = CraftingTreeState.of(grid.getGroup(groupId));
this.canvas = new CraftingTreeCanvas(this.uiState.camera);
this.toolbar = new CraftingTreeToolbar(this.uiState);
}
@@ -160,7 +163,7 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
final ItemTreeSlot replaceNode = this.uiState.replaceRecipeNode;
this.uiState.replaceRecipeNode = null;
if (recipe != null) {
if (recipe != null && this.grid.getGroup(this.groupId) != null) {
final RecipeId recipeId = recipe.getRecipeId();
boolean changed = false;
@@ -188,6 +191,13 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
@Override
public void initGui() {
// the group can be removed from the bookmark panel while a recipe screen opened from this tree is shown
if (this.grid.getGroup(this.groupId) == null) {
this.mc.displayGuiScreen(this.prevGui);
return;
}
this.xSize = this.width;
this.ySize = this.height;
@@ -284,7 +294,7 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
this.canvas.graph.rebuild(this.math);
if (this.isFirstScreen) {
if (!this.uiState.initialized) {
updateCollapsedItems();
} else {
this.canvas.recalculateCoordinates(this.collapsedRecipes, this.uiState.collapsedItems, anchor);
@@ -292,8 +302,8 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
rebuildStatsPanel();
if (this.isFirstScreen) {
this.isFirstScreen = false;
if (!this.uiState.initialized) {
this.uiState.initialized = true;
this.canvas.fitToView();
}
}
@@ -329,15 +339,7 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
this.hoveredNode = canHover && hoveredBadgeHandler == null ? this.canvas.getNodeMouseOver(getMousePosition())
: null;
BG_TEXTURE.draw(
MARGIN,
MARGIN,
this.width - MARGIN * 2,
this.height - MARGIN * 2,
PADDING,
PADDING,
PADDING_TOP,
PADDING);
BG_TEXTURE.draw(MARGIN, MARGIN, this.width - MARGIN * 2, this.height - MARGIN * 2);
if (this.math.outputRecipes.isEmpty()) {
drawCenteredString(
@@ -756,7 +758,7 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
return true;
}
this.canvas.startDrag(mouseX, mouseY);
this.canvas.startDrag(getMousePosition());
}
return false;
@@ -777,7 +779,7 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
}
this.canvas.drag(mouseX, mouseY);
this.canvas.drag(getMousePosition());
}
@Override
@@ -842,7 +844,7 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
(int) mouse.getY(),
direction,
NEIClientUtils.shiftKey(),
NEIClientUtils.controlKey());
NEIClientUtils.controlKey() || NEIClientConfig.craftingTreeZoomWithoutModifier());
}
private boolean handleItemWheel(int wheel, Point2D mouse) {
@@ -904,6 +906,18 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
}
}
if (keyCode == this.mc.gameSettings.keyBindInventory.getKeyCode()) {
this.mc.displayGuiScreen(this.prevGui);
NEICPH.sendRequestContainer();
return;
}
super.keyTyped(typedChar, keyCode);
}
@Override
public boolean onHotkeyPressed() {
if (this.statsPanelVisible && KeyManager.isKeyDown("recipe.recipe")) {
final Point2D mouse = getMousePosition();
final Widget widget = this.statsPanel.getWidgetUnderMouse((int) mouse.getX(), (int) mouse.getY());
@@ -911,11 +925,11 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
if (widget != null && widget instanceof StatsPanel.ItemStatsSlot itemStatsSlot
&& itemStatsSlot.recipeId != null) {
GuiCraftingRecipe.openRecipeGui("recipeId", itemStatsSlot.recipeId.getResult(), itemStatsSlot.recipeId);
return;
return true;
}
}
super.keyTyped(typedChar, keyCode);
return false;
}
private String getHandlerTitle(String handlerName) {
@@ -242,7 +242,7 @@ class StatsPanel extends ScrollContainer {
26,
84,
46,
16).build();
16).setSlices(0, 0, 2, 0).build();
private static final DrawableResource SLOT_DEFAULT = new DrawableBuilder(
"nei:textures/gui/craftingtree.png",
@@ -290,8 +290,8 @@ class StatsPanel extends ScrollContainer {
public StatsPanel() {
final ScrollBar scrollbar = new ScrollBar().setTrackWidth(4)
.setThumbTexture(new DrawableBuilder("nei:textures/gui/craftingtree.png", 26, 100, 24, 4).build(), 0, 0)
.setTrackTexture(new DrawableBuilder("nei:textures/gui/craftingtree.png", 26, 104, 24, 4).build(), 0, 0)
.setThumbTexture(new DrawableBuilder("nei:textures/gui/craftingtree.png", 26, 100, 24, 4).build())
.setTrackTexture(new DrawableBuilder("nei:textures/gui/craftingtree.png", 26, 104, 24, 4).build())
.setTrackPadding(1, -5, 1, 0).setOverflowType(ScrollBar.OverflowType.AUTO);
setHorizontalScroll(scrollbar);
@@ -589,7 +589,7 @@ class StatsPanel extends ScrollContainer {
RenderHelper.enableGUIStandardItemLighting();
GL11.glDisable(GL11.GL_LIGHTING);
BG_TEXTURE.draw(this.x, this.y, this.w, this.h, 0, 0, 2, 0);
BG_TEXTURE.draw(this.x, this.y, this.w, this.h);
super.draw(mouseX, mouseY);
@@ -74,7 +74,11 @@ public class IMCHandler {
tag.getInteger("imageX"),
tag.getInteger("imageY"),
tag.getInteger("imageWidth"),
tag.getInteger("imageHeight"));
tag.getInteger("imageHeight"),
tag.hasKey("imageTextureWidth") ? tag.getInteger("imageTextureWidth")
: HandlerInfo.DEFAULT_IMAGE_TEXTURE_SIZE,
tag.hasKey("imageTextureHeight") ? tag.getInteger("imageTextureHeight")
: HandlerInfo.DEFAULT_IMAGE_TEXTURE_SIZE);
}
if (!info.hasImageOrItem()) {
final String itemName = tag.getString("itemName");
@@ -79,19 +79,24 @@ public class RightPanel extends GuiWidget {
}
@Override
public void lastKeyTyped(int keyID, char keyChar) {
public boolean onHotkeyPressed() {
if (!focused() && KeyManager.isHashDown("gui.search")) {
setFocus(true);
return true;
}
if (focused() && KeyManager.isKeyDown("gui.getprevioussearch")) {
handleNavigateHistory(TextHistory.Direction.PREVIOUS);
return true;
}
if (focused() && KeyManager.isKeyDown("gui.getnextsearch")) {
handleNavigateHistory(TextHistory.Direction.NEXT);
return true;
}
return false;
}
@Override
@@ -15,13 +15,21 @@ public class DrawableBuilder {
private int paddingBottom = 0;
private int paddingLeft = 0;
private int paddingRight = 0;
private int sliceLeft = 0;
private int sliceRight = 0;
private int sliceTop = 0;
private int sliceBottom = 0;
public DrawableBuilder(String resourceLocation, int u, int v, int width, int height) {
this(new ResourceLocation(resourceLocation), u, v, width, height);
}
public DrawableBuilder(ResourceLocation resource, int u, int v, int width, int height) {
this.u = u;
this.v = v;
this.width = width;
this.height = height;
this.resourceLocation = new ResourceLocation(resourceLocation);
this.resourceLocation = resource;
}
public DrawableBuilder setTextureSize(int width, int height) {
@@ -38,6 +46,14 @@ public class DrawableBuilder {
return this;
}
public DrawableBuilder setSlices(int sliceLeft, int sliceRight, int sliceTop, int sliceBottom) {
this.sliceLeft = sliceLeft;
this.sliceRight = sliceRight;
this.sliceTop = sliceTop;
this.sliceBottom = sliceBottom;
return this;
}
public DrawableBuilder trim(int trimTop, int trimBottom, int trimLeft, int trimRight) {
this.u += trimLeft;
this.v += trimTop;
@@ -58,6 +74,10 @@ public class DrawableBuilder {
paddingLeft,
paddingRight,
textureWidth,
textureHeight);
textureHeight,
sliceLeft,
sliceRight,
sliceTop,
sliceBottom);
}
}
@@ -1,7 +1,7 @@
package codechicken.nei.drawable;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
@@ -19,9 +19,42 @@ public class DrawableResource extends Image {
private final int paddingLeft;
private final int paddingRight;
private final int sliceLeft;
private final int sliceRight;
private final int sliceTop;
private final int sliceBottom;
public DrawableResource(ResourceLocation resourceLocation, int u, int v, int width, int height, int paddingTop,
int paddingBottom, int paddingLeft, int paddingRight, int textureWidth, int textureHeight) {
this(
resourceLocation,
u,
v,
width,
height,
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
textureWidth,
textureHeight,
0,
0,
0,
0);
}
// negative padding lets the texture overflow the box it is drawn into
public DrawableResource(ResourceLocation resourceLocation, int u, int v, int width, int height, int paddingTop,
int paddingBottom, int paddingLeft, int paddingRight, int textureWidth, int textureHeight, int sliceLeft,
int sliceRight, int sliceTop, int sliceBottom) {
super(u, v, width, height);
this.sliceLeft = sliceLeft;
this.sliceRight = sliceRight;
this.sliceTop = sliceTop;
this.sliceBottom = sliceBottom;
this.resourceLocation = resourceLocation;
this.textureWidth = textureWidth;
this.textureHeight = textureHeight;
@@ -40,6 +73,14 @@ public class DrawableResource extends Image {
return height + paddingTop + paddingBottom;
}
public int getTextureWidth() {
return this.textureWidth;
}
public int getTextureHeight() {
return this.textureHeight;
}
public void bindTexture() {
Minecraft.getMinecraft().getTextureManager().bindTexture(this.resourceLocation);
}
@@ -55,18 +96,28 @@ public class DrawableResource extends Image {
GL11.glEnable(GL11.GL_TEXTURE_2D);
bindTexture();
drawModalRectWithCustomSizedTexture(xOffset + maskLeft, yOffset + maskTop, maskLeft, maskTop, width, height);
final Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
addQuad(tessellator, xOffset + maskLeft, yOffset + maskTop, maskLeft, maskTop, width, height);
tessellator.draw();
if (!is2DTexture) GL11.glDisable(GL11.GL_TEXTURE_2D);
}
public void draw(int xOffset, int yOffset, int width, int height, int sliceLeft, int sliceRight, int sliceTop,
int sliceBottom) {
final int textureWidth = getWidth();
final int textureHeight = getHeight();
final int middleWidth = textureWidth - sliceLeft - sliceRight;
final int middleHeight = textureHeight - sliceTop - sliceBottom;
final int tileWidth = width - sliceLeft - sliceRight;
final int tileHeight = height - sliceTop - sliceBottom;
public void draw(int xOffset, int yOffset, int width, int height) {
if (width == getWidth() && height == getHeight()) {
draw(xOffset, yOffset);
return;
}
final int sliceLeft = this.sliceLeft;
final int sliceRight = this.sliceRight;
final int sliceTop = this.sliceTop;
final int sliceBottom = this.sliceBottom;
final int middleWidth = this.width - sliceLeft - sliceRight;
final int middleHeight = this.height - sliceTop - sliceBottom;
final int tileWidth = width - this.paddingLeft - this.paddingRight - sliceLeft - sliceRight;
final int tileHeight = height - this.paddingTop - this.paddingBottom - sliceTop - sliceBottom;
if (middleWidth > 0 && middleHeight > 0 && tileWidth > 0 && tileHeight > 0) {
final boolean is2DTexture = GL11.glGetBoolean(GL11.GL_TEXTURE_2D);
@@ -78,14 +129,30 @@ public class DrawableResource extends Image {
GL11.glEnable(GL11.GL_TEXTURE_2D);
bindTexture();
// middle parts
final Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
for (int tileW = 0; tileW <= tileWidthCount; tileW++) {
final int tileWidthSize = (tileW == tileWidthCount) ? remainderWidth : middleWidth;
final int x = xOffset + sliceLeft + tileW * middleWidth;
// border top
addQuad(tessellator, x, yOffset, sliceLeft, 0, tileWidthSize, sliceTop);
// border bottom
addQuad(
tessellator,
x,
yOffset + sliceTop + tileHeight,
sliceLeft,
sliceTop + middleHeight,
tileWidthSize,
sliceBottom);
// middle parts
for (int tileH = 0; tileH <= tileHeightCount; tileH++) {
final int tileHeightSize = (tileH == tileHeightCount) ? remainderHeight : middleHeight;
drawModalRectWithCustomSizedTexture(
addQuad(
tessellator,
x,
yOffset + sliceTop + tileH * middleHeight,
sliceLeft,
@@ -95,114 +162,75 @@ public class DrawableResource extends Image {
}
}
// border top
if (sliceTop > 0) {
for (int tileW = 0; tileW <= tileWidthCount; tileW++) {
final int tileWidthSize = (tileW == tileWidthCount) ? remainderWidth : middleWidth;
drawModalRectWithCustomSizedTexture(
xOffset + sliceLeft + tileW * middleWidth,
yOffset,
sliceLeft,
0,
tileWidthSize,
sliceTop);
}
}
for (int tileH = 0; tileH <= tileHeightCount; tileH++) {
final int tileHeightSize = (tileH == tileHeightCount) ? remainderHeight : middleHeight;
final int y = yOffset + sliceTop + tileH * middleHeight;
// border bottom
if (sliceBottom > 0) {
for (int tileW = 0; tileW <= tileWidthCount; tileW++) {
final int tileWidthSize = (tileW == tileWidthCount) ? remainderWidth : middleWidth;
drawModalRectWithCustomSizedTexture(
xOffset + sliceLeft + tileW * middleWidth,
yOffset + sliceTop + tileHeight,
sliceLeft,
sliceTop + middleHeight,
tileWidthSize,
sliceBottom);
}
}
// border left
if (sliceLeft > 0) {
for (int tileH = 0; tileH <= tileHeightCount; tileH++) {
final int tileHeightSize = (tileH == tileHeightCount) ? remainderHeight : middleHeight;
drawModalRectWithCustomSizedTexture(
xOffset,
yOffset + sliceTop + tileH * middleHeight,
0,
sliceTop,
sliceLeft,
tileHeightSize);
}
}
// border right
if (sliceRight > 0) {
for (int tileH = 0; tileH <= tileHeightCount; tileH++) {
final int tileHeightSize = (tileH == tileHeightCount) ? remainderHeight : middleHeight;
drawModalRectWithCustomSizedTexture(
xOffset + sliceLeft + tileWidth,
yOffset + sliceTop + tileH * middleHeight,
sliceLeft + middleWidth,
sliceTop,
sliceRight,
tileHeightSize);
}
// border left
addQuad(tessellator, xOffset, y, 0, sliceTop, sliceLeft, tileHeightSize);
// border right
addQuad(
tessellator,
xOffset + sliceLeft + tileWidth,
y,
sliceLeft + middleWidth,
sliceTop,
sliceRight,
tileHeightSize);
}
// angle left-top
if (sliceLeft > 0 && sliceTop > 0) {
drawModalRectWithCustomSizedTexture(xOffset, yOffset, 0, 0, sliceLeft, sliceTop);
}
addQuad(tessellator, xOffset, yOffset, 0, 0, sliceLeft, sliceTop);
// angle right-top
if (sliceRight > 0 && sliceTop > 0) {
drawModalRectWithCustomSizedTexture(
xOffset + sliceLeft + tileWidth,
yOffset,
sliceLeft + middleWidth,
0,
sliceRight,
sliceTop);
}
addQuad(
tessellator,
xOffset + sliceLeft + tileWidth,
yOffset,
sliceLeft + middleWidth,
0,
sliceRight,
sliceTop);
// angle left-bottom
if (sliceLeft > 0 && sliceBottom > 0) {
drawModalRectWithCustomSizedTexture(
xOffset,
yOffset + sliceTop + tileHeight,
0,
sliceTop + middleHeight,
sliceLeft,
sliceBottom);
}
addQuad(
tessellator,
xOffset,
yOffset + sliceTop + tileHeight,
0,
sliceTop + middleHeight,
sliceLeft,
sliceBottom);
// angle right-bottom
if (sliceRight > 0 && sliceBottom > 0) {
drawModalRectWithCustomSizedTexture(
xOffset + sliceLeft + tileWidth,
yOffset + sliceTop + tileHeight,
sliceLeft + middleWidth,
sliceTop + middleHeight,
sliceRight,
sliceBottom);
}
addQuad(
tessellator,
xOffset + sliceLeft + tileWidth,
yOffset + sliceTop + tileHeight,
sliceLeft + middleWidth,
sliceTop + middleHeight,
sliceRight,
sliceBottom);
tessellator.draw();
if (!is2DTexture) GL11.glDisable(GL11.GL_TEXTURE_2D);
}
}
private void drawModalRectWithCustomSizedTexture(int xOffset, int yOffset, int u, int v, int width, int height) {
Gui.func_146110_a(
xOffset + this.paddingLeft,
yOffset + this.paddingTop,
this.x + u,
this.y + v,
width,
height,
this.textureWidth,
this.textureHeight);
private void addQuad(Tessellator tessellator, int xOffset, int yOffset, int u, int v, int width, int height) {
if (width <= 0 || height <= 0) return;
final float scaleU = 1.0F / this.textureWidth;
final float scaleV = 1.0F / this.textureHeight;
final int x = xOffset + this.paddingLeft;
final int y = yOffset + this.paddingTop;
final float u0 = (this.x + u) * scaleU;
final float u1 = (this.x + u + width) * scaleU;
final float v0 = (this.y + v) * scaleV;
final float v1 = (this.y + v + height) * scaleV;
tessellator.addVertexWithUV(x, y + height, 0.0D, u0, v1);
tessellator.addVertexWithUV(x + width, y + height, 0.0D, u1, v1);
tessellator.addVertexWithUV(x + width, y, 0.0D, u1, v0);
tessellator.addVertexWithUV(x, y, 0.0D, u0, v0);
}
}
@@ -449,6 +449,7 @@ public class GuiContainerManager {
}
private int clickHandled = 0;
private int hotkeyHandled = 0;
public final List<IContainerTooltipHandler> instanceTooltipHandlers;
public GuiContainerManager(GuiContainer screen) {
@@ -483,6 +484,7 @@ public class GuiContainerManager {
public void load() {
clickHandled = 0;
hotkeyHandled = 0;
for (IContainerObjectHandler objectHandler : objectHandlers) objectHandler.load(window);
}
@@ -502,6 +504,46 @@ public class GuiContainerManager {
for (IContainerInputHandler inputhander : inputHandlers)
if (inputhander.lastKeyTyped(window, keyChar, keyID)) return true;
return onHotkeyPressed();
}
public boolean onHotkeyPressed() {
if (window instanceof IGuiHotkeyListener && ((IGuiHotkeyListener) window).onHotkeyPressed()) {
return true;
}
for (IContainerInputHandler inputhander : inputHandlers) {
if (inputhander.onHotkeyPressed(window)) {
return true;
}
}
return window instanceof IGuiHotkeyListener && ((IGuiHotkeyListener) window).onScreenHotkeyPressed();
}
public boolean handleMouseHotkey() {
final int button = Mouse.getEventButton();
if (button < 0) {
return false;
}
if (!Mouse.getEventButtonState()) {
// the press has been consumed, so the release must not reach the screen either
if ((hotkeyHandled & 1 << button) == 0) {
return false;
}
hotkeyHandled &= ~(1 << button);
return true;
}
if (KeyManager.handleMouseHotkey(button, this::onHotkeyPressed)) {
hotkeyHandled |= 1 << button;
return true;
}
return false;
}
@@ -36,6 +36,18 @@ public interface IContainerInputHandler {
*/
public boolean lastKeyTyped(GuiContainer gui, char keyChar, int keyID);
/**
* Called when a key or a mouse button is pressed and the event has not been handled by the first pass. Use this for
* key bindings that work globally and check them with {@link codechicken.nei.KeyManager#isKeyDown}, so they work
* the same for keyboard keys and mouse buttons.
*
* @param gui An instance of the currentscreen
* @return true to terminate further processing of this event.
*/
default boolean onHotkeyPressed(GuiContainer gui) {
return false;
}
/**
* Called when the mouse is clicked in the gui
*
@@ -0,0 +1,27 @@
package codechicken.nei.guihook;
/**
* Interface for GuiContainers to receive NEI compatible hotkey events from both keyboard keys and mouse buttons
*/
public interface IGuiHotkeyListener {
/**
* Called before the input handlers when a key or a mouse button is pressed, check key bindings with
* {@link codechicken.nei.KeyManager#isKeyDown}
*
* @return true to terminate further processing of this event.
*/
default boolean onHotkeyPressed() {
return false;
}
/**
* Called after the input handlers for screen-wide hotkeys that do not depend on the hovered object, so they never
* take priority over more specific actions
*
* @return true to terminate further processing of this event.
*/
default boolean onScreenHotkeyPressed() {
return false;
}
}
@@ -2,15 +2,14 @@ package codechicken.nei.item;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.item.ItemStack;
import net.minecraft.util.IIcon;
import net.minecraftforge.client.IItemRenderer;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidRegistry;
import org.lwjgl.opengl.GL11;
import codechicken.nei.item.FluidDrawer.FillDirection;
import codechicken.nei.util.ReadableNumberConverter;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -34,37 +33,13 @@ public class FluidDisplayRenderer implements IItemRenderer {
return;
}
renderIcon(item, fluidDisplay);
renderIcon(item);
renderAmountOverlay(item, fluidDisplay);
}
private void renderIcon(ItemStack item, ItemFluidDisplay fluidDisplay) {
final IIcon icon = fluidDisplay.getIconIndex(item);
if (icon == null) {
return;
}
final int color = fluidDisplay.getColorFromItemStack(item, 0);
final float red = (color >> 16 & 0xFF) / 255F;
final float green = (color >> 8 & 0xFF) / 255F;
final float blue = (color & 0xFF) / 255F;
Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.locationBlocksTexture);
GL11.glColor4f(red, green, blue, 1F);
GL11.glEnable(GL11.GL_BLEND);
OpenGlHelper
.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
final Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
tessellator.addVertexWithUV(0, 16, 0, icon.getMinU(), icon.getMaxV());
tessellator.addVertexWithUV(16, 16, 0, icon.getMaxU(), icon.getMaxV());
tessellator.addVertexWithUV(16, 0, 0, icon.getMaxU(), icon.getMinV());
tessellator.addVertexWithUV(0, 0, 0, icon.getMinU(), icon.getMinV());
tessellator.draw();
GL11.glColor4f(1F, 1F, 1F, 1F);
GL11.glDisable(GL11.GL_BLEND);
private void renderIcon(ItemStack item) {
final Fluid fluid = FluidRegistry.getFluid(item.getItemDamage());
FluidDrawer.drawFluidArea(fluid != null ? fluid : FluidRegistry.WATER, 0, 0, 16, 16, false, FillDirection.UP);
}
private void renderAmountOverlay(ItemStack item, ItemFluidDisplay fluidDisplay) {
@@ -1,14 +1,21 @@
package codechicken.nei.item;
import java.util.IdentityHashMap;
import java.util.Map;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.util.IIcon;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidRegistry;
import net.minecraftforge.fluids.FluidStack;
import org.lwjgl.opengl.GL11;
import codechicken.nei.api.IFluidRenderer;
import codechicken.nei.scroll.GuiHelper;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -27,28 +34,32 @@ public class FluidDrawer {
return this == LEFT || this == RIGHT;
}
public FillDirection resolve(FluidStack fluidStack) {
public FillDirection resolve(Fluid fluid) {
if (this != AUTO) return this;
return fluidStack.getFluid().isGaseous(fluidStack) ? DOWN : UP;
return fluid.isGaseous() ? DOWN : UP;
}
}
private static final Map<Fluid, IFluidRenderer> fluidRenderers = new IdentityHashMap<>();
private FluidDrawer() {}
public static void drawFluid(int x, int y, int width, int height, int capacity, FluidStack fluidStack,
boolean flowing, FillDirection direction) {
final Fluid fluid = fluidStack.getFluid();
IIcon icon = flowing ? fluid.getFlowingIcon() : null;
if (icon == null) {
icon = fluid.getIcon(fluidStack);
public static void registerFluidRenderer(Fluid fluid, IFluidRenderer renderer) {
if (renderer == null) {
fluidRenderers.remove(fluid);
} else {
fluidRenderers.put(fluid, renderer);
}
}
if (icon == null || fluidStack.amount <= 0) {
public static void drawTank(int x, int y, int width, int height, int capacity, FluidStack fluidStack,
boolean flowing, FillDirection direction) {
if (fluidStack.amount <= 0) {
return;
}
direction = direction.resolve(fluidStack);
final Fluid fluid = fluidStack.getFluid();
direction = direction.resolve(fluid);
final int tankSize = direction.isHorizontal() ? width : height;
final int tankCapacity = capacity > 0 ? capacity : fluidStack.amount;
@@ -59,38 +70,109 @@ public class FluidDrawer {
final int fillHeight = direction.isHorizontal() ? height : Math.min(fillSize, height);
final int fillX = direction == FillDirection.LEFT ? x + width - fillWidth : x;
final int fillY = direction == FillDirection.DOWN ? y : y + height - fillHeight;
GL11.glDisable(GL11.GL_LIGHTING);
drawFluidArea(fluid, fillX, fillY, fillWidth, fillHeight, flowing, direction);
GL11.glEnable(GL11.GL_LIGHTING);
}
/**
* Fills the area with fluid tiles. The tiles start from the side the fluid fills from, so a partial tile ends up at
* the fluid surface.
*/
public static void drawFluidArea(Fluid fluid, int x, int y, int width, int height, boolean flowing,
FillDirection direction) {
direction = direction.resolve(fluid);
final boolean anchorRight = direction == FillDirection.LEFT;
final boolean anchorBottom = direction != FillDirection.DOWN;
final IFluidRenderer renderer = fluidRenderers.get(fluid);
final int color = fluid.getColor(fluidStack);
if (renderer == null) {
drawDefaultFluid(fluid, x, y, width, height, flowing, anchorRight, anchorBottom);
} else {
drawCustomFluid(renderer, fluid, x, y, width, height, anchorRight, anchorBottom);
}
}
private static void drawCustomFluid(IFluidRenderer renderer, Fluid fluid, int x, int y, int width, int height,
boolean anchorRight, boolean anchorBottom) {
GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT);
GL11.glDisable(GL11.GL_LIGHTING);
final boolean clip = width % 16 != 0 || height % 16 != 0;
if (clip) {
GuiHelper.pushScissorFrame(x, y, width, height);
}
try {
for (int tx = 0; tx < width; tx += 16) {
final int tileX = anchorRight ? x + width - tx - 16 : x + tx;
for (int ty = 0; ty < height; ty += 16) {
final int tileY = anchorBottom ? y + height - ty - 16 : y + ty;
renderer.renderFluid(fluid, tileX, tileY);
}
}
} finally {
if (clip) {
GuiHelper.popScissorFrame();
}
GL11.glPopAttrib();
GL11.glColor4f(1F, 1F, 1F, 1F);
}
}
private static void drawDefaultFluid(Fluid fluid, int x, int y, int width, int height, boolean flowing,
boolean anchorRight, boolean anchorBottom) {
IIcon icon = flowing ? fluid.getFlowingIcon() : null;
if (icon == null) {
icon = fluid.getStillIcon();
}
if (icon == null) {
icon = FluidRegistry.WATER.getStillIcon();
}
if (icon == null) {
return;
}
final int color = fluid.getColor();
final float red = (color >> 16 & 0xFF) / 255F;
final float green = (color >> 8 & 0xFF) / 255F;
final float blue = (color & 0xFF) / 255F;
Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.locationBlocksTexture);
GL11.glColor3f(red, green, blue);
GL11.glColor4f(red, green, blue, 1F);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDisable(GL11.GL_LIGHTING);
OpenGlHelper
.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
final double iconMinU = icon.getMinU();
final double iconMaxU = icon.getMaxU();
final double iconMinV = icon.getMinV();
final double iconMaxV = icon.getMaxV();
final Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
for (int tx = 0; tx < fillWidth; tx += 16) {
final int tileWidth = Math.min(16, fillWidth - tx);
for (int tx = 0; tx < width; tx += 16) {
final int tileWidth = Math.min(16, width - tx);
// a cropped tile keeps the texture edge that stays seamless against the neighbouring tile
final double uSize = (icon.getMaxU() - icon.getMinU()) * tileWidth / 16D;
final double uMin = anchorRight ? icon.getMaxU() - uSize : icon.getMinU();
final double uMax = anchorRight ? icon.getMaxU() : icon.getMinU() + uSize;
final int leftX = anchorRight ? fillX + fillWidth - tx - tileWidth : fillX + tx;
final double uSize = (iconMaxU - iconMinU) * tileWidth / 16D;
final double uMin = anchorRight ? iconMaxU - uSize : iconMinU;
final double uMax = anchorRight ? iconMaxU : iconMinU + uSize;
final int leftX = anchorRight ? x + width - tx - tileWidth : x + tx;
final int rightX = leftX + tileWidth;
for (int ty = 0; ty < fillHeight; ty += 16) {
final int tileHeight = Math.min(16, fillHeight - ty);
final double vSize = (icon.getMaxV() - icon.getMinV()) * tileHeight / 16D;
final double vMin = anchorBottom ? icon.getMaxV() - vSize : icon.getMinV();
final double vMax = anchorBottom ? icon.getMaxV() : icon.getMinV() + vSize;
final int bottomY = anchorBottom ? fillY + fillHeight - ty : fillY + ty + tileHeight;
for (int ty = 0; ty < height; ty += 16) {
final int tileHeight = Math.min(16, height - ty);
final double vSize = (iconMaxV - iconMinV) * tileHeight / 16D;
final double vMin = anchorBottom ? iconMaxV - vSize : iconMinV;
final double vMax = anchorBottom ? iconMaxV : iconMinV + vSize;
final int bottomY = anchorBottom ? y + height - ty : y + ty + tileHeight;
final int topY = bottomY - tileHeight;
tessellator.addVertexWithUV(leftX, bottomY, 0, uMin, vMax);
@@ -102,7 +184,6 @@ public class FluidDrawer {
tessellator.draw();
GL11.glDisable(GL11.GL_BLEND);
GL11.glEnable(GL11.GL_LIGHTING);
GL11.glColor4f(1F, 1F, 1F, 1F);
}
}
@@ -41,6 +41,7 @@ public class ItemFluidDisplay extends Item implements IFluidContainerItem {
public ItemFluidDisplay() {
setUnlocalizedName("nei.fluidDisplay");
setMaxStackSize(1);
setHasSubtypes(true);
INSTANCE = this;
}
@@ -72,7 +73,7 @@ public class ItemFluidDisplay extends Item implements IFluidContainerItem {
@Override
public FluidStack getFluid(ItemStack stack) {
if (stack == null || !(stack.getItem() instanceof ItemFluidDisplay) || !stack.hasTagCompound()) {
if (stack == null || !(stack.getItem() instanceof ItemFluidDisplay)) {
return null;
}
@@ -85,7 +86,7 @@ public class ItemFluidDisplay extends Item implements IFluidContainerItem {
final NBTTagCompound nbTag = stack.getTagCompound();
final FluidStack fluidStack = new FluidStack(
fluid,
(int) Math.min(nbTag.getLong("neiFluidDisplayAmount"), Integer.MAX_VALUE));
(int) Math.min(nbTag != null ? nbTag.getLong("neiFluidDisplayAmount") : 0, Integer.MAX_VALUE));
return fluidStack;
}
@@ -5,13 +5,18 @@ import java.util.List;
import java.util.Map;
import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.Slot;
import net.minecraft.inventory.SlotCrafting;
import net.minecraft.item.ItemStack;
import codechicken.nei.FastTransferManager;
import codechicken.nei.ItemPanels;
import codechicken.nei.ItemStackAmount;
import codechicken.nei.NEIClientUtils;
import codechicken.nei.NEIServerUtils;
import codechicken.nei.RestartableTask;
import codechicken.nei.ServerSync;
import codechicken.nei.bookmark.BookmarkItem;
import codechicken.nei.recipe.Recipe.RecipeId;
import codechicken.nei.recipe.chain.RecipeChainIterator;
@@ -26,6 +31,7 @@ public class AutoCraftingManager {
@Override
public void execute() {
final GuiContainer guiContainer = NEIClientUtils.getGuiContainer();
final ItemStack[] layout = snapshotLayout(guiContainer);
final List<BookmarkItem> initialItems = prepareInitialItems(math, getInventoryItems(guiContainer));
boolean processed = false;
boolean changed = false;
@@ -69,6 +75,11 @@ public class AutoCraftingManager {
} while (changed && !interrupted(guiContainer));
if (!interrupted(guiContainer)) {
restoreLayout(guiContainer, layout);
ServerSync.request();
}
} finally {
StackInfo.pauseItemDamageSound(false);
}
@@ -134,4 +145,70 @@ public class AutoCraftingManager {
return inventory;
}
private static ItemStack[] snapshotLayout(GuiContainer guiContainer) {
final List<Slot> slots = guiContainer.inventorySlots.inventorySlots;
final ItemStack[] layout = new ItemStack[slots.size()];
for (int index = 0; index < slots.size(); index++) {
final Slot slot = slots.get(index);
if (slot.getHasStack() && !(slot instanceof SlotCrafting)) {
layout[index] = slot.getStack().copy();
}
}
return layout;
}
private static void restoreLayout(GuiContainer guiContainer, ItemStack[] layout) {
final List<Slot> slots = guiContainer.inventorySlots.inventorySlots;
final EntityPlayer player = guiContainer.mc.thePlayer;
for (int index = 0; index < slots.size() && player.inventory.getItemStack() == null
&& guiContainer.mc.currentScreen == guiContainer; index++) {
final ItemStack expected = layout[index];
final Slot target = slots.get(index);
if (expected == null || inPlace(slots, layout, index)
|| !target.isItemValid(expected)
|| target.getHasStack() && !target.canTakeStack(player)) {
continue;
}
final Slot source = findMisplaced(slots, layout, expected, player);
if (source == null) continue;
FastTransferManager.clickSlot(guiContainer, source.slotNumber);
FastTransferManager.clickSlot(guiContainer, target.slotNumber);
if (player.inventory.getItemStack() != null) {
FastTransferManager.clickSlot(guiContainer, source.slotNumber);
}
}
FastTransferManager.dropHeldItem(guiContainer);
}
private static boolean inPlace(List<Slot> slots, ItemStack[] layout, int index) {
final ItemStack stack = slots.get(index).getStack();
return stack != null && layout[index] != null
&& NEIServerUtils.areStacksSameTypeCraftingWithNBT(layout[index], stack);
}
private static Slot findMisplaced(List<Slot> slots, ItemStack[] layout, ItemStack expected, EntityPlayer player) {
for (int index = 0; index < slots.size(); index++) {
final Slot slot = slots.get(index);
if (slot.getHasStack() && !(slot instanceof SlotCrafting)
&& !inPlace(slots, layout, index)
&& slot.canTakeStack(player)
&& NEIServerUtils.areStacksSameTypeCraftingWithNBT(expected, slot.getStack())) {
return slot;
}
}
return null;
}
}
@@ -1,6 +1,7 @@
package codechicken.nei.recipe;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -20,10 +21,13 @@ import codechicken.lib.inventory.InventoryUtils;
import codechicken.nei.FastTransferManager;
import codechicken.nei.NEIClientUtils;
import codechicken.nei.PositionedStack;
import codechicken.nei.ServerSync;
import codechicken.nei.api.IOverlayHandler;
public class DefaultOverlayHandler implements IOverlayHandler {
private static final long SYNC_TIMEOUT_MILLIS = 5000;
public static class DistributedIngred {
public DistributedIngred(ItemStack item) {
@@ -113,7 +117,8 @@ public class DefaultOverlayHandler implements IOverlayHandler {
}
boolean craft = false;
while (multiplier > 0) {
while (multiplier > 0 && firstGui.mc.currentScreen == firstGui
&& presenceOverlay(firstGui, handler, recipeIndex).stream().allMatch(s -> s.isPresent())) {
final int transfer = transferRecipe(firstGui, handler, recipeIndex, multiplier);
if (transfer <= 0) {
@@ -122,6 +127,10 @@ public class DefaultOverlayHandler implements IOverlayHandler {
multiplier -= transfer;
if (!craftingSlot.getHasStack() && !awaitServer(firstGui)) {
break;
}
if (craftingSlot.getHasStack() && craftingSlot.canTakeStack(firstGui.mc.thePlayer)) {
FastTransferManager.clickSlot(firstGui, craftingSlot.slotNumber, 0, 1);
@@ -139,9 +148,30 @@ public class DefaultOverlayHandler implements IOverlayHandler {
return craft;
}
private boolean awaitServer(GuiContainer gui) {
final long deadline = System.currentTimeMillis() + SYNC_TIMEOUT_MILLIS;
ServerSync.request();
while (ServerSync.pending()) {
if (gui.mc.currentScreen != gui || System.currentTimeMillis() > deadline) {
return false;
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
return false;
}
}
return true;
}
protected boolean clearIngredients(GuiContainer gui, IRecipeHandler handler) {
for (Slot slot : getCraftMatrixSlots(gui, handler)) {
if (gui.mc.currentScreen != gui) return false;
if (slot.canTakeStack(gui.mc.thePlayer)) {
FastTransferManager.clickSlot(gui, slot.slotNumber, 0, 1);
if (slot.getHasStack()) return false;
@@ -206,7 +236,9 @@ public class DefaultOverlayHandler implements IOverlayHandler {
protected void moveIngredients(GuiContainer gui, List<IngredientDistribution> assignedIngredients, int multiplier) {
final Slot craftingSlot = getCraftingResultSlot(gui);
for (Slot slot : gui.inventorySlots.inventorySlots) {
for (Slot slot : getSourceSlots(gui)) {
if (gui.mc.currentScreen != gui) return;
if (slot == craftingSlot || !slot.getHasStack()
|| !canMoveFrom(slot, gui)
|| !slot.canTakeStack(gui.mc.thePlayer))
@@ -240,6 +272,10 @@ public class DefaultOverlayHandler implements IOverlayHandler {
FastTransferManager.clickSlot(gui, slot.slotNumber);
}
// A right click with an empty or foreign cursor would take half of the dest stack
final ItemStack held = gui.mc.thePlayer.inventory.getItemStack();
if (held == null || !canStack(distrib.permutation, held)) break;
FastTransferManager.clickSlot(gui, dest.slotNumber, 1);
stackSize--;
}
@@ -370,7 +406,7 @@ public class DefaultOverlayHandler implements IOverlayHandler {
}
protected void findInventoryQuantities(GuiContainer gui, List<DistributedIngred> ingredStacks) {
for (Slot slot : gui.inventorySlots.inventorySlots) /* work out how much we have to go round */ {
for (Slot slot : getSourceSlots(gui)) /* work out how much we have to go round */ {
if (slot.getHasStack() && canMoveFrom(slot, gui)) {
final ItemStack pstack = slot.getStack();
final DistributedIngred istack = findIngred(ingredStacks, pstack);
@@ -408,6 +444,25 @@ public class DefaultOverlayHandler implements IOverlayHandler {
}
}
protected List<Slot> getSourceSlots(GuiContainer gui) {
final List<Slot> slots = new ArrayList<>(gui.inventorySlots.inventorySlots);
slots.sort(Comparator.comparingLong(slot -> getRemainingDurability(slot.getStack())));
return slots;
}
private static long getRemainingDurability(ItemStack stack) {
if (stack == null || stack.getMaxStackSize() != 1) return Long.MAX_VALUE;
final NBTTagCompound tagCompound = stack.getTagCompound();
if (tagCompound != null && tagCompound.hasKey("GT.ToolStats")) {
final NBTTagCompound toolStats = tagCompound.getCompoundTag("GT.ToolStats");
return toolStats.getLong("MaxDamage") - toolStats.getLong("Damage");
}
return stack.isItemStackDamageable() ? stack.getMaxDamage() - stack.getItemDamage() : Long.MAX_VALUE;
}
private int getGTToolDamagePerContainerCraft(ItemStack aStack) {
try {
@@ -1,9 +1,9 @@
package codechicken.nei.recipe;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
@@ -12,14 +12,9 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.item.ItemStack;
import codechicken.lib.gui.GuiDraw;
import codechicken.nei.FavoriteRecipes;
import codechicken.nei.ItemPanels;
import codechicken.nei.ItemsGrid.ItemsGridSlot;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.NEIClientUtils;
import codechicken.nei.bookmark.BookmarkItem.BookmarkItemType;
import codechicken.nei.bookmark.BookmarksGridSlot;
import codechicken.nei.recipe.Recipe.RecipeId;
public class GuiCraftingRecipe extends GuiRecipe<ICraftingHandler> {
@@ -35,6 +30,7 @@ public class GuiCraftingRecipe extends GuiRecipe<ICraftingHandler> {
public static GuiRecipe<?> createRecipeGui(String outputId, boolean open, Object... results) {
final Minecraft mc = NEIClientUtils.mc();
final RecipeId recipeId;
List<ItemStack> pinnedIngredients = null;
for (int i = 0; i < results.length; i++) {
if (results[i] instanceof ItemStack stack) {
@@ -46,6 +42,12 @@ public class GuiCraftingRecipe extends GuiRecipe<ICraftingHandler> {
recipeId = getRecipeId(mc.currentScreen, (ItemStack) results[0]);
} else if ("recipeId".equals(outputId)) {
recipeId = (RecipeId) results[1];
// optional third argument: ingredients to pin the found recipe with
if (results.length > 2 && results[2] instanceof List<?>list) {
pinnedIngredients = extractIngredients(list);
}
} else {
recipeId = getCurrentRecipeId(mc.currentScreen);
}
@@ -65,13 +67,25 @@ public class GuiCraftingRecipe extends GuiRecipe<ICraftingHandler> {
mc.displayGuiScreen(gui);
}
gui.openTargetRecipe(recipeId);
gui.openTargetRecipe(recipeId, pinnedIngredients);
return gui;
}
return null;
}
private static List<ItemStack> extractIngredients(List<?> list) {
final List<ItemStack> ingredients = new ArrayList<>(list.size());
for (Object item : list) {
if (item instanceof ItemStack stack) {
ingredients.add(stack);
}
}
return ingredients;
}
public static ArrayList<ICraftingHandler> getCraftingHandlers(String outputId, Object... results) {
ArrayList<ICraftingHandler> craftinghandlers = GuiCraftingRecipe.craftinghandlers;
ArrayList<ICraftingHandler> serialCraftingHandlers = GuiCraftingRecipe.serialCraftingHandlers;
@@ -118,39 +132,8 @@ public class GuiCraftingRecipe extends GuiRecipe<ICraftingHandler> {
}
public static RecipeId getRecipeId(GuiScreen gui, ItemStack stackover) {
if (gui instanceof GuiRecipe<?>guiRecipe) {
final Recipe focusedRecipe = guiRecipe.getFocusedRecipe();
if (focusedRecipe != null) {
return focusedRecipe.getRecipeId();
}
}
final Point mouseover = GuiDraw.getMousePosition();
final BookmarksGridSlot panelSlot = ItemPanels.bookmarkPanel.getSlotMouseOver(mouseover.x, mouseover.y);
if (panelSlot != null) {
return panelSlot.getType() == BookmarkItemType.INGREDIENT || panelSlot.getRecipeId() == null
? FavoriteRecipes.getFavorite(stackover)
: panelSlot.getRecipeId();
}
ItemsGridSlot itemSlot = ItemPanels.itemPanel.getSlotMouseOver(mouseover.x, mouseover.y);
if (itemSlot == null) {
itemSlot = ItemPanels.itemPanel.historyPanel.getSlotMouseOver(mouseover.x, mouseover.y);
}
if (itemSlot == null) {
itemSlot = ItemPanels.itemPanel.craftablesPanel.getSlotMouseOver(mouseover.x, mouseover.y);
}
if (itemSlot != null) {
return itemSlot.getRecipeId();
}
return null;
final HoveredRecipe hovered = HoveredRecipe.of(gui, stackover);
return hovered != null ? hovered.recipeId : null;
}
private GuiCraftingRecipe(ArrayList<ICraftingHandler> handlers) {
@@ -122,18 +122,25 @@ public class GuiFavoriteButton extends GuiRecipeButton {
return Recipe.of(this.handlerRef);
}
public void lastKeyTyped(char keyChar, int keyID) {
@Override
public void lastKeyTyped(char keyChar, int keyID) {}
@Override
public boolean onHotkeyPressed() {
if (KeyManager.isKeyDown("bookmark.add") && NEIClientUtils.shiftKey()) {
saveRecipeInBookmark();
return true;
}
if (KeyManager.isKeyDown("bookmark.open_crafting_tree") && NEIClientUtils.shiftKey()
&& saveRecipeInBookmark()) {
final int groupId = ItemPanels.bookmarkPanel.getGrid().getNextGroupId() - 1;
GuiCraftingTree.openCraftingTreeGui(ItemPanels.bookmarkPanel.getGrid(), groupId);
return true;
}
return false;
}
@Override
@@ -177,12 +177,20 @@ public class GuiOverlayButton extends GuiRecipeButton {
hotkeys.put(
NEIClientUtils.getKeyName(NEIClientUtils.SHIFT_HASH, NEIMouseUtils.MOUSE_BTN_LMB),
NEIClientUtils.translate("recipe.overlay.fillCraftingGrid"));
hotkeys.put(
NEIClientUtils.getKeyName(
NEIClientUtils.SHIFT_HASH + NEIClientUtils.CTRL_HASH,
NEIMouseUtils.MOUSE_BTN_LMB),
NEIClientUtils.translate("recipe.overlay.fillCraftingGridOnce"));
}
} else if (canFillCraftingGrid()) {
hotkeys.put(
NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_LMB),
NEIClientUtils.translate("recipe.overlay.fillCraftingGrid"));
hotkeys.put(
NEIClientUtils.getKeyName(NEIClientUtils.CTRL_HASH, NEIMouseUtils.MOUSE_BTN_LMB),
NEIClientUtils.translate("recipe.overlay.fillCraftingGridOnce"));
}
hotkeys.put(
@@ -263,7 +271,7 @@ public class GuiOverlayButton extends GuiRecipeButton {
if (!items.isEmpty()) {
this.missedMaterialsTooltipLineHandler = ItemsTooltipLineHandler
.grid(NEIClientUtils.translate("recipe.overlay.missing"), items, Integer.MAX_VALUE);
.of(NEIClientUtils.translate("recipe.overlay.missing"), items, Integer.MAX_VALUE, 0);
} else {
this.missedMaterialsTooltipLineHandler = null;
}
@@ -329,7 +337,7 @@ public class GuiOverlayButton extends GuiRecipeButton {
if (!requireShiftForOverlayRecipe() || shift) {
if (this.canFillCraftingGrid) {
this.handlerRef.fillCraftingGrid(this.firstGui, 0);
this.handlerRef.fillCraftingGrid(this.firstGui, NEIClientUtils.controlKey() ? 1 : 0);
}
} else if (hasOverlay() && useOverlayRenderer()) {
this.handlerRef.useOverlayRenderer(this.firstGui);
@@ -338,14 +346,21 @@ public class GuiOverlayButton extends GuiRecipeButton {
}
@Override
public void lastKeyTyped(char keyChar, int keyID) {
public void lastKeyTyped(char keyChar, int keyID) {}
@Override
public boolean onHotkeyPressed() {
if (KeyManager.isKeyDown("bookmark.add") && NEIClientUtils.shiftKey()) {
saveRecipeInBookmark();
return true;
}
if (KeyManager.isKeyDown("bookmark.chat_link") && NEIClientUtils.controlKey() && NEIClientUtils.shiftKey()) {
sendRecipeChatLink();
return true;
}
return false;
}
public void saveRecipeInBookmark() {
@@ -8,7 +8,7 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.WeakHashMap;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiScreen;
@@ -51,6 +51,7 @@ import codechicken.nei.guihook.GuiContainerManager;
import codechicken.nei.guihook.IContainerTooltipHandler;
import codechicken.nei.guihook.IGuiClientSide;
import codechicken.nei.guihook.IGuiHandleMouseWheel;
import codechicken.nei.guihook.IGuiHotkeyListener;
import codechicken.nei.recipe.Recipe.RecipeId;
import codechicken.nei.recipe.widget.RecipeWidget;
import codechicken.nei.scroll.ScrollBar;
@@ -62,7 +63,7 @@ import codechicken.nei.util.EmptyContainer;
import codechicken.nei.util.SlotInaccessible;
public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer implements IGuiContainerOverlay,
IGuiClientSide, IGuiHandleMouseWheel, IContainerTooltipHandler, INEIGuiHandler {
IGuiClientSide, IGuiHandleMouseWheel, IGuiHotkeyListener, IContainerTooltipHandler, INEIGuiHandler {
private static final int BORDER_PADDING = 5;
private static final int TRANSPARENCY_BORDER = 4;
@@ -71,15 +72,23 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
0,
0,
176 + TRANSPARENCY_BORDER * 2,
166 + TRANSPARENCY_BORDER * 2).build();
166 + TRANSPARENCY_BORDER * 2)
.setSlices(
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER)
.build();
private static final int BUTTON_WIDTH = 13;
private static final int BUTTON_HEIGHT = 12;
protected static final ScrollBar VERTICAL_SCROLLBAR = new ScrollBar().setTrackWidth(14)
.setOverflowType(OverflowType.AUTO).setScrollPlace(ScrollPlace.END)
.setTrackTexture(new DrawableBuilder("nei:textures/nei_sprites.png", 42, 90, 13, 21).build(), 9, 9)
.setThumbTexture(new DrawableBuilder("nei:textures/nei_sprites.png", 22, 96, 6, 9).build(), 3, 2)
.setTrackTexture(
new DrawableBuilder("nei:textures/nei_sprites.png", 42, 90, 13, 21).setSlices(0, 0, 9, 9).build())
.setThumbTexture(
new DrawableBuilder("nei:textures/nei_sprites.png", 22, 96, 6, 9).setSlices(0, 0, 3, 2).build())
.setThumbPadding(1, 6, 0, 6);
public static final List<IRecipeFilterProvider> recipeFilterers = new LinkedList<>();
@@ -295,73 +304,118 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
}
/**
* Many old mods assumed a fixed NEI window height of {@code 166} pixels. Now that this is no longer the case, their
* tooltip and click zone handling is broken. This helper class fixes these old mods by hacking the {@link #height}
* property's value so that these old mods' calculations return the correct value for the new height.
* Many old mods assumed a fixed NEI window size of {@code 176x166} pixels, with exactly one recipe shown at a time,
* flush against the top-left corner of the window. Now that none of these assumptions hold (the window has a
* flexible size, several recipes can be stacked in the same scrollable list, and the recipe area is inset from the
* window's edges), these old mods' tooltip and click zone handling is broken. This helper class fixes these old
* mods by hacking the {@link #width}, {@link #height}, {@link #guiLeft} and {@link #guiTop} properties' values so
* that these old mods' calculations return the correct value for whichever recipe they are currently being asked
* about.
*
* <p>
* New and old mods often assume that the handler is only drawn when {@link GuiRecipe} is the currently active
* screen in {@link net.minecraft.client.Minecraft}. This is no longer true since the addition of recipe tooltips
* which need to render the recipe in other containers on mouse hover, this class temporarily switches the active
* screen to {@code this} while rendering them to avoid ClassCastExceptions and other similar crashes.
* Since each recipe handled by a {@link RecipeWidget} can sit at a different real position, this must be
* constructed with that widget's top-left edge every time a legacy entry point ({@code mouseClicked},
* {@code handleTooltip}, ...) is invoked for it, rather than once per {@link GuiRecipe}.
*
* <p>
* This class is an {@link AutoCloseable} so that it can be used with try-with-resources, which will ensure that
* {@link #height} and {@link net.minecraft.client.Minecraft#currentScreen} is returned to the correct value
* {@link #width}, {@link #height}, {@link #guiLeft} and {@link #guiTop} are returned to the correct value
* afterwards.
*/
private class CompatibilityHacks implements AutoCloseable {
public static final class CompatibilityHacks implements AutoCloseable {
// The historical NEI recipe screen's fixed size: a single top paging widget 16px tall, no horizontal inset,
// and a 176x166 box. Legacy handlers either read guiLeft/guiTop directly (expecting them to point 16px above
// their recipe, flush on the left) or recompute them from width/height with these same two constants.
private static final int LEGACY_TOP_GAP = 16;
private static final int LEGACY_BOX_WIDTH = 176;
private static final int LEGACY_BOX_HEIGHT = 166;
// layoutWindow() places the recipe container at (guiLeft + 3, guiTop + 32); RecipePageManager then places
// each left-aligned recipe widget another 2px to the right of that. This is the inverse of both, undoing the
// container's own inset so a legacy handler sees its recipe flush with guiLeft as it expects.
private static final int CONTAINER_LEFT_INSET = 3 + 2;
private final GuiRecipe<?> gui;
private final int trueWidth;
private final int trueHeight;
private final int trueGuiLeft;
private final int trueGuiTop;
private CompatibilityHacks() {
trueHeight = height;
trueGuiTop = guiTop;
/**
* @param widget The recipe widget a legacy entry point ({@code mouseClicked}, {@code handleTooltip}, ...) is
* about to be invoked for.
*/
public CompatibilityHacks(GuiRecipe<?> gui, RecipeWidget widget) {
this.gui = gui;
this.trueWidth = gui.width;
this.trueHeight = gui.height;
this.trueGuiLeft = gui.guiLeft;
this.trueGuiTop = gui.guiTop;
isHeightHackApplied = NEIClientConfig.heightHackHandlerRegex.stream()
.map(pattern -> pattern.matcher(handler.original.getHandlerId())).anyMatch(Matcher::matches);
if (isHeightHackApplied) {
// guiTop is the top edge of the recipe screen on the y-axis. Old recipe
// handlers expect a single paging
// widget at the top and at the bottom of the recipe screen with a height of
// 16px each, but GTNH NEI
// moved the bottom paging widget to the top following JEI's design, increasing
// the "forbidden" zone at
// the top to 32px. To fix these old recipe handlers we move guiTop down 16px so
// that they can keep
// working with the old 16px gap.
guiTop += 16;
final String handlerId = gui.handler.original.getHandlerId();
gui.isHeightHackApplied = NEIClientConfig.heightHackHandlerRegex.stream()
.anyMatch(pattern -> pattern.matcher(handlerId).matches());
// The old NEI recipe screen had a fixed width and height and was always
// centered on the screen. Legacy
// recipe handlers use the calculation ((height - 166) / 2) to compute the
// y-value of the top edge of
// the NEI window. GTNH NEI changes the layout so that the recipe screen length
// now has a flexible
// length and is no longer centered vertically. In order for the top-of-screen
// calculation to return the
// correct result, we have to hack the height field with an inverse of the
// calculation using the actual
// top of the recipe screen stored in guiTop.
height = (2 * guiTop) + 166;
// For reference, in case we ever need to modify width as well:
// the legacy calculation used for width is ((width - 176) / 2), which should
// evaluate to be equal to
// guiWidth (the x-value of the left edge of the NEI recipe screen). So if we
// wanted to override width
// as well, we'd do this:
// width = (2 * guiWidth) + 176;
if (gui.isHeightHackApplied) {
gui.guiTop = recipeTop(widget) - LEGACY_TOP_GAP;
gui.guiLeft = widget.x - CONTAINER_LEFT_INSET;
gui.height = (2 * gui.guiTop) + LEGACY_BOX_HEIGHT;
gui.width = (2 * gui.guiLeft) + LEGACY_BOX_WIDTH;
}
}
// recipiesPerPage() == 2 is the IRecipeHandler default: a handler only ends up with this value if it never
// overrode the method, or if it overrode it and chose 2 on purpose. We only care about the latter case, so
// we require the override to actually come from the handler's own class rather than being inherited as-is.
private static final Map<Class<?>, Boolean> overridesRecipiesPerPage = new WeakHashMap<>();
/**
* The real top edge of this widget's recipe, as a legacy handler following the {@link #LEGACY_TOP_GAP}
* convention would expect to find it at guiTop. A few handlers (e.g. ImmersiveEngineering) are left over from
* the old {@code recipiesPerPage() == 2} convention, where two recipes shared one physical page: their own
* {@code mouseClicked}/{@code handleTooltip} still add a hardcoded "recipe height * (recipe % 2)" on top of
* guiTop to find the odd one of the pair. Since every recipe is its own independent widget now, we pre-subtract
* that same offset here so their own addition cancels out and lands back on this widget's real top.
*
* <p>
* This only applies when the handler itself overrides {@link IRecipeHandler#recipiesPerPage()} to return
* {@code 2}: that method defaults to {@code 2} on the interface, so most height-hacked handlers return it
* without ever having implemented this pairing (e.g. BuildCraftCompat never overrides it at all, and only reads
* it elsewhere to pick a cosmetic sub-name), and applying this compensation to them would break them. A handler
* that went out of its way to declare {@code return 2;} itself is making a deliberate statement about this
* legacy convention that a handler which simply never mentions the method isn't.
*/
private static int recipeTop(RecipeWidget widget) {
final RecipeHandlerRef ref = widget.getRecipeHandlerRef();
final HandlerInfo handlerInfo = widget.getHandlerInfo();
final int top = widget.y + handlerInfo.getYShift();
if (ref.handler.recipiesPerPage() == 2 && declaresRecipiesPerPage(ref.handler)) {
return top - handlerInfo.getHeight() * (ref.recipeIndex % 2);
}
return top;
}
private static boolean declaresRecipiesPerPage(IRecipeHandler handler) {
return overridesRecipiesPerPage.computeIfAbsent(handler.getClass(), clazz -> {
try {
return clazz.getMethod("recipiesPerPage").getDeclaringClass() != IRecipeHandler.class;
} catch (NoSuchMethodException e) {
return false;
}
});
}
@Override
public void close() {
guiTop = trueGuiTop;
height = trueHeight;
this.gui.guiLeft = this.trueGuiLeft;
this.gui.guiTop = this.trueGuiTop;
this.gui.width = this.trueWidth;
this.gui.height = this.trueHeight;
isHeightHackApplied = false;
this.gui.isHeightHackApplied = false;
}
}
@@ -524,6 +578,13 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
}
public int openTargetRecipe(RecipeId recipeId) {
return openTargetRecipe(recipeId, null);
}
/**
* @param pinnedIngredients when not null, the found recipe is paused with these ingredients shown
*/
public int openTargetRecipe(RecipeId recipeId, List<ItemStack> pinnedIngredients) {
int recipeIndex = -1;
int recipetype = 0;
@@ -551,6 +612,10 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
setRecipePage(recipetype);
this.handlerPages.gotoRecipeIndex(Math.max(0, recipeIndex));
if (pinnedIngredients != null && recipeIndex >= 0) {
this.handlerPages.pinRecipe(recipeIndex, pinnedIngredients);
}
return recipeIndex;
}
@@ -633,6 +698,7 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
if (GuiRecipe.searchField.isVisible() && GuiRecipe.searchField.focused()) {
GuiRecipe.searchField.lastKeyTyped(i, c);
GuiRecipe.searchField.onHotkeyPressed();
return;
}
@@ -657,7 +723,24 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
if (i == mc.gameSettings.keyBindInventory.getKeyCode()) {
this.mc.displayGuiScreen(getFirstScreenGeneral());
NEICPH.sendRequestContainer();
} else if (KeyManager.isKeyDown("recipe.back")) {
}
}
@Override
public boolean onHotkeyPressed() {
if (GuiRecipe.searchField.isVisible() && GuiRecipe.searchField.onHotkeyPressed()
|| this.recipeCatalyst.isShowWidget() && this.recipeCatalyst.onHotkeyPressed()
|| this.container.onHotkeyPressed()) {
return true;
}
return false;
}
@Override
public boolean onScreenHotkeyPressed() {
if (KeyManager.isKeyDown("recipe.back")) {
this.mc.displayGuiScreen(this.prevGui);
} else if (KeyManager.isKeyDown("recipe.prev_machine")) {
prevType();
@@ -667,7 +750,11 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
prevPage();
} else if (KeyManager.isKeyDown("recipe.next_recipe")) {
nextPage();
} else {
return false;
}
return true;
}
@Override
@@ -687,10 +774,8 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
return;
}
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
if (this.container.handleClick(mousex, mousey, button)) {
return;
}
if (this.container.handleClick(mousex, mousey, button)) {
return;
}
if (this.recipeTabs.mouseClicked(mousex, mousey, button)) {
@@ -738,11 +823,7 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer 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
// could be confusing...
// GTNH NEI. So no old mods will use this.
// First, invoke scroll handling over recipe handler tabbar. Makes sure it is
// not overwritten by recipe
@@ -755,10 +836,8 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
return;
}
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
if (this.container.onMouseWheel(scroll, mouse.x, mouse.y)) {
return;
}
if (this.container.onMouseWheel(scroll, mouse.x, mouse.y)) {
return;
}
// If shift is held, try switching to the next recipe handler. Replicates the
@@ -810,9 +889,7 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
currenttip = this.recipeCatalyst.handleTooltip(mousex, mousey, currenttip);
}
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
currenttip = this.container.handleTooltip(mousex, mousey, currenttip);
}
currenttip = this.container.handleTooltip(mousex, mousey, currenttip);
this.recipeTabs.handleTooltip(mousex, mousey, currenttip);
@@ -858,9 +935,7 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
hotkeys = this.recipeCatalyst.handleHotkeys(mousex, mousey, hotkeys);
}
try (CompatibilityHacks compatibilityHacks = new CompatibilityHacks()) {
hotkeys = this.container.handleHotkeys(mousex, mousey, hotkeys);
}
hotkeys = this.container.handleHotkeys(mousex, mousey, hotkeys);
return hotkeys;
}
@@ -914,11 +989,7 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
this.guiLeft - TRANSPARENCY_BORDER,
this.guiTop - TRANSPARENCY_BORDER,
this.xSize + TRANSPARENCY_BORDER * 2,
this.ySize + TRANSPARENCY_BORDER * 2,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER);
this.ySize + TRANSPARENCY_BORDER * 2);
drawJEITabs(mouseX, mouseY);
}
@@ -16,11 +16,15 @@ public abstract class GuiRecipeButton extends GuiNEIButton {
public List<GuiRecipeButton> buttonList;
public RecipeHandlerRef handlerRef;
public int width;
public int height;
public UpdateRecipeButtonsEvent(GuiRecipe<?> gui, RecipeHandlerRef handlerRef,
public UpdateRecipeButtonsEvent(GuiRecipe<?> gui, RecipeHandlerRef handlerRef, int width, int height,
List<GuiRecipeButton> buttonList) {
super(gui);
this.handlerRef = handlerRef;
this.width = width;
this.height = height;
this.buttonList = new ArrayList<>(buttonList);
}
@@ -29,16 +33,17 @@ public abstract class GuiRecipeButton extends GuiNEIButton {
public HandlerInfo handlerInfo;
public Pre(GuiRecipe<?> gui, RecipeHandlerRef handlerRef, HandlerInfo handlerInfo) {
super(gui, handlerRef, new ArrayList<>());
public Pre(GuiRecipe<?> gui, RecipeHandlerRef handlerRef, int width, int height, HandlerInfo handlerInfo) {
super(gui, handlerRef, width, height, new ArrayList<>());
this.handlerInfo = handlerInfo;
}
}
public static class Post extends UpdateRecipeButtonsEvent {
public Post(GuiRecipe<?> gui, RecipeHandlerRef handlerRef, List<GuiRecipeButton> buttonList) {
super(gui, handlerRef, buttonList);
public Post(GuiRecipe<?> gui, RecipeHandlerRef handlerRef, int width, int height,
List<GuiRecipeButton> buttonList) {
super(gui, handlerRef, width, height, buttonList);
}
}
}
@@ -59,6 +64,10 @@ public abstract class GuiRecipeButton extends GuiNEIButton {
public abstract void lastKeyTyped(char keyChar, int keyID);
public boolean onHotkeyPressed() {
return false;
}
public abstract void drawItemOverlay();
public void update() {
@@ -55,11 +55,17 @@ public class GuiRecipeCatalyst extends ScrollContainer {
0,
0,
28 + TRANSPARENCY_BORDER * 2,
28 + TRANSPARENCY_BORDER * 2).setTextureSize(28 + TRANSPARENCY_BORDER * 2, 28 + TRANSPARENCY_BORDER * 2)
28 + TRANSPARENCY_BORDER * 2)
.setTextureSize(28 + TRANSPARENCY_BORDER * 2, 28 + TRANSPARENCY_BORDER * 2)
.setSlices(
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER)
.build();
private static final DrawableResource FG_TEXTURE = new DrawableBuilder("nei:textures/slot.png", 0, 0, 18, 18)
.setTextureSize(18, 18).build();
.setTextureSize(18, 18).setSlices(1, 1, 1, 1).build();
private static final ScrollBar VERTICAL_SCROLLBAR = ScrollBar.defaultVerticalBar()
.setOverflowType(ScrollBar.OverflowType.AUTO).setTrackPadding(0, BORDER_PADDING, -13, BORDER_PADDING)
@@ -155,20 +161,12 @@ public class GuiRecipeCatalyst extends ScrollContainer {
this.x - TRANSPARENCY_BORDER,
this.y - TRANSPARENCY_BORDER,
this.w + TRANSPARENCY_BORDER * 2,
this.h + TRANSPARENCY_BORDER * 2,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER);
this.h + TRANSPARENCY_BORDER * 2);
FG_TEXTURE.draw(
this.x + this.paddingInlineStart - 1,
this.y + this.paddingBlockStart - 1,
this.w - this.paddingInlineStart - this.paddingInlineEnd + 2,
this.h - this.paddingBlockStart - this.paddingBlockEnd + 2,
1,
1,
1,
1);
this.h - this.paddingBlockStart - this.paddingBlockEnd + 2);
super.draw(mx, my);
}
@@ -12,6 +12,7 @@ public class HandlerInfo {
public static int DEFAULT_HEIGHT = 65;
public static int DEFAULT_WIDTH = 166;
public static final int DEFAULT_IMAGE_TEXTURE_SIZE = 256;
@Deprecated
public static int DEFAULT_MAX_PER_PAGE = 1;
@@ -70,9 +71,22 @@ public class HandlerInfo {
}
public boolean setImage(String resourceLocation, int imageX, int imageY, int imageWidth, int imageHeight) {
return setImage(
resourceLocation,
imageX,
imageY,
imageWidth,
imageHeight,
DEFAULT_IMAGE_TEXTURE_SIZE,
DEFAULT_IMAGE_TEXTURE_SIZE);
}
public boolean setImage(String resourceLocation, int imageX, int imageY, int imageWidth, int imageHeight,
int textureWidth, int textureHeight) {
if (hasImageOrItem()) return false;
this.image = new DrawableBuilder(resourceLocation, imageX, imageY, imageWidth, imageHeight).build();
this.image = new DrawableBuilder(resourceLocation, imageX, imageY, imageWidth, imageHeight)
.setTextureSize(textureWidth, textureHeight).build();
return true;
}
@@ -211,6 +225,20 @@ public class HandlerInfo {
return this;
}
public Builder setDisplayImage(ResourceLocation location, int imageX, int imageY, int imageWidth,
int imageHeight, int textureWidth, int textureHeight) {
this.info.itemStack = null;
this.info.setImage(
location.toString(),
imageX,
imageY,
imageWidth,
imageHeight,
textureWidth,
textureHeight);
return this;
}
@Deprecated
public Builder setUseCustomScroll(boolean useCustomScroll) {
this.info.setUseCustomScroll(useCustomScroll);
@@ -0,0 +1,87 @@
package codechicken.nei.recipe;
import java.awt.Point;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.item.ItemStack;
import codechicken.lib.gui.GuiDraw;
import codechicken.nei.FavoriteRecipes;
import codechicken.nei.ItemPanels;
import codechicken.nei.ItemsGrid.ItemsGridSlot;
import codechicken.nei.bookmark.BookmarkItem.BookmarkItemType;
import codechicken.nei.bookmark.BookmarksGridSlot;
import codechicken.nei.recipe.Recipe.RecipeId;
public final class HoveredRecipe {
public enum Source {
RECIPE_GUI,
BOOKMARK,
ITEM_PANEL,
OTHER
}
public final RecipeId recipeId;
public final Source source;
public final boolean favorite;
private HoveredRecipe(RecipeId recipeId, Source source, boolean favorite) {
this.recipeId = recipeId;
this.source = source;
this.favorite = favorite;
}
public static HoveredRecipe of(GuiScreen gui, ItemStack stackover) {
if (gui instanceof GuiRecipe<?>guiRecipe) {
final Recipe focusedRecipe = guiRecipe.getFocusedRecipe();
if (focusedRecipe != null) {
return new HoveredRecipe(focusedRecipe.getRecipeId(), Source.RECIPE_GUI, false);
}
}
final Point mouseover = GuiDraw.getMousePosition();
final BookmarksGridSlot bookmarkSlot = ItemPanels.bookmarkPanel.getSlotMouseOver(mouseover.x, mouseover.y);
if (bookmarkSlot != null) {
if (bookmarkSlot.getType() != BookmarkItemType.INGREDIENT && bookmarkSlot.getRecipeId() != null) {
return new HoveredRecipe(bookmarkSlot.getRecipeId(), Source.BOOKMARK, false);
}
return favorite(stackover, Source.BOOKMARK);
}
ItemsGridSlot itemSlot = ItemPanels.itemPanel.getSlotMouseOver(mouseover.x, mouseover.y);
if (itemSlot == null) {
itemSlot = ItemPanels.itemPanel.historyPanel.getSlotMouseOver(mouseover.x, mouseover.y);
}
if (itemSlot == null) {
itemSlot = ItemPanels.itemPanel.craftablesPanel.getSlotMouseOver(mouseover.x, mouseover.y);
}
if (itemSlot != null) {
if (itemSlot.getRecipeId() != null) {
return new HoveredRecipe(itemSlot.getRecipeId(), Source.ITEM_PANEL, false);
}
return favorite(stackover, Source.ITEM_PANEL);
}
if (gui instanceof GuiRecipe<?>guiRecipe && guiRecipe.getSlotAtPosition(mouseover.x, mouseover.y) != null) {
return favorite(stackover, Source.RECIPE_GUI);
}
return favorite(stackover, Source.OTHER);
}
private static HoveredRecipe favorite(ItemStack stackover, Source source) {
final RecipeId recipeId = FavoriteRecipes.getFavorite(stackover);
return recipeId != null ? new HoveredRecipe(recipeId, source, true) : null;
}
}
@@ -1,5 +1,6 @@
package codechicken.nei.recipe;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
@@ -167,6 +168,8 @@ public class RecipeHandlerRef {
return transferAlghoritmCache.computeIfAbsent(overlayHandler.getClass(), clazz -> {
try {
final Method transferRecipe = overlayHandler.getClass()
.getMethod("transferRecipe", GuiContainer.class, IRecipeHandler.class, int.class, int.class);
if (overlayHandler instanceof DefaultOverlayHandler) {
final Class<?> overlayRecipeClazz = overlayHandler.getClass()
@@ -178,14 +181,11 @@ public class RecipeHandlerRef {
boolean.class)
.getDeclaringClass();
if (overlayRecipeClazz.equals(DefaultOverlayHandler.class)) {
return true;
}
return overlayRecipeClazz.equals(DefaultOverlayHandler.class)
|| !transferRecipe.getDeclaringClass().equals(DefaultOverlayHandler.class);
}
return !overlayHandler.getClass()
.getMethod("transferRecipe", GuiContainer.class, IRecipeHandler.class, int.class, int.class)
.isDefault();
return !transferRecipe.isDefault();
} catch (NoSuchMethodException e) {}
return false;
@@ -7,14 +7,10 @@ import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.item.ItemStack;
import codechicken.lib.gui.GuiDraw;
import codechicken.lib.vec.Rectangle4i;
import codechicken.nei.BookmarkPanel.BookmarkViewMode;
import codechicken.nei.FavoriteRecipes;
import codechicken.nei.ItemPanels;
import codechicken.nei.ItemsGrid.ItemsGridSlot;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.api.ShortcutInputHandler;
import codechicken.nei.bookmark.BookmarkItem.BookmarkItemType;
import codechicken.nei.bookmark.BookmarksGridSlot;
import codechicken.nei.guihook.GuiContainerManager;
import codechicken.nei.guihook.IContainerInputHandler;
@@ -33,6 +29,11 @@ public class RecipeItemInputHandler implements IContainerInputHandler, IContaine
@Override
public boolean lastKeyTyped(GuiContainer gui, char keyChar, int keyCode) {
return false;
}
@Override
public boolean onHotkeyPressed(GuiContainer gui) {
return ShortcutInputHandler.handleKeyEvent(GuiContainerManager.getStackMouseOver(gui));
}
@@ -58,7 +59,9 @@ public class RecipeItemInputHandler implements IContainerInputHandler, IContaine
List<String> currenttip) {
if (itemstack != null) {
final RecipeId recipeId = getHoveredRecipeId(gui, itemstack, mousex, mousey);
final HoveredRecipe hovered = HoveredRecipe.of(gui, itemstack);
final RecipeId recipeId = hovered != null && showRecipeTooltip(hovered, mousex, mousey) ? hovered.recipeId
: null;
if (recipeId == null) {
recipeTooltipLineHandler = null;
@@ -76,49 +79,21 @@ public class RecipeItemInputHandler implements IContainerInputHandler, IContaine
return currenttip;
}
private RecipeId getHoveredRecipeId(GuiContainer gui, ItemStack itemstack, int mousex, int mousey) {
private boolean showRecipeTooltip(HoveredRecipe hovered, int mousex, int mousey) {
return switch (hovered.source) {
case BOOKMARK -> !hovered.favorite && showRecipeTooltipInBookmarks(mousex, mousey);
case ITEM_PANEL -> NEIClientConfig.showRecipeTooltipInPanel();
case RECIPE_GUI -> hovered.favorite && NEIClientConfig.showRecipeTooltipInGui();
default -> false;
};
}
if (ItemPanels.bookmarkPanel.contains(mousex, mousey)) {
final BookmarksGridSlot slot = ItemPanels.bookmarkPanel.getSlotMouseOver(mousex, mousey);
if (slot != null && slot.getRecipeId() != null
&& slot.getType() != BookmarkItemType.INGREDIENT
&& NEIClientConfig.getRecipeTooltipsMode() != 0) {
final int tooltipMode = NEIClientConfig.getRecipeTooltipsMode();
final BookmarkViewMode viewMode = slot.getGroup().viewMode;
if (tooltipMode == 3 || viewMode == BookmarkViewMode.DEFAULT && tooltipMode == 1
|| viewMode == BookmarkViewMode.TODO_LIST && tooltipMode == 2) {
return slot.getRecipeId();
}
}
return null;
}
if (NEIClientConfig.showRecipeTooltipInPanel()) {
ItemsGridSlot panelSlot = ItemPanels.itemPanel.getSlotMouseOver(mousex, mousey);
if (panelSlot == null) {
panelSlot = ItemPanels.itemPanel.historyPanel.getSlotMouseOver(mousex, mousey);
}
if (panelSlot == null) {
panelSlot = ItemPanels.itemPanel.craftablesPanel.getSlotMouseOver(mousex, mousey);
}
if (panelSlot != null) {
return panelSlot.getRecipeId();
}
}
if (gui instanceof GuiRecipe<?>guiRecipe
&& new Rectangle4i(gui.guiLeft, gui.guiTop, gui.xSize, gui.ySize).contains(mousex, mousey)
&& NEIClientConfig.showRecipeTooltipInGui()
&& guiRecipe.getFocusedRecipe() == null) {
return FavoriteRecipes.getFavorite(itemstack);
}
return null;
private boolean showRecipeTooltipInBookmarks(int mousex, int mousey) {
final BookmarksGridSlot slot = ItemPanels.bookmarkPanel.getSlotMouseOver(mousex, mousey);
final int tooltipMode = NEIClientConfig.getRecipeTooltipsMode();
final BookmarkViewMode viewMode = slot.getGroup().viewMode;
return tooltipMode == 3 || viewMode == BookmarkViewMode.DEFAULT && tooltipMode == 1
|| viewMode == BookmarkViewMode.TODO_LIST && tooltipMode == 2;
}
@Override
@@ -21,6 +21,7 @@ import codechicken.nei.ItemStackAmount;
import codechicken.nei.ItemsTooltipLineHandler;
import codechicken.nei.ItemsTooltipLineHandler.StacksAmountRenderer;
import codechicken.nei.ItemsTooltipLineHandler.TotalAmountRenderer;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.NEIClientUtils;
import codechicken.nei.PositionedStack;
import codechicken.nei.api.IGuiContainerOverlay;
@@ -163,9 +164,9 @@ public class RecipeItemsTooltipLineHandler extends CompositeTooltipLineHandler {
protected void addResultLine(List<ItemStack> items, Set<NBTTagCompound> approximate) {
final String label = NEIClientUtils.translate("recipe.items.results");
final ItemsTooltipLineHandler line = items.size() == 1
? ItemsTooltipLineHandler.list(label, items, maxLineRows())
: ItemsTooltipLineHandler.grid(label, items, maxLineRows());
final int maxRows = maxLineRows();
final int maxNamedRows = items.size() == 1 ? NEIClientConfig.getMaxVerticalTooltipRows() : 0;
final ItemsTooltipLineHandler line = ItemsTooltipLineHandler.of(label, items, maxRows, maxNamedRows);
if (line.isEmpty()) {
return;
@@ -188,7 +189,9 @@ public class RecipeItemsTooltipLineHandler extends CompositeTooltipLineHandler {
protected void addRequiredLine(List<ItemStack> items, Set<NBTTagCompound> approximate) {
final String label = NEIClientUtils.translate("recipe.items.ingredients");
final ItemsTooltipLineHandler line = ItemsTooltipLineHandler.list(label, items, maxLineRows());
final int maxRows = maxLineRows();
final int maxNamedRows = NEIClientConfig.getMaxVerticalTooltipRows();
final ItemsTooltipLineHandler line = ItemsTooltipLineHandler.of(label, items, maxRows, maxNamedRows);
if (line.isEmpty()) {
return;
@@ -211,8 +214,9 @@ public class RecipeItemsTooltipLineHandler extends CompositeTooltipLineHandler {
protected void addNeededLine(List<ItemStack> items, Set<NBTTagCompound> approximate, boolean vertical) {
final String label = NEIClientUtils.translate("recipe.items.ingredients_needed");
final ItemsTooltipLineHandler line = vertical ? ItemsTooltipLineHandler.list(label, items, maxLineRows())
: ItemsTooltipLineHandler.grid(label, items, maxLineRows());
final int maxRows = maxLineRows();
final int maxNamedRows = vertical ? NEIClientConfig.getMaxVerticalTooltipRows() : 0;
final ItemsTooltipLineHandler line = ItemsTooltipLineHandler.of(label, items, maxRows, maxNamedRows);
if (line.isEmpty()) {
return;
@@ -6,6 +6,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.item.ItemStack;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.Widget;
import codechicken.nei.recipe.widget.RecipeWidget;
@@ -42,6 +44,8 @@ public class RecipePageManager {
}
this.cacheKey = cacheKey;
final List<RecipeWidget> pausedWidgets = getPausedWidgets();
this.pageWidgets.clear();
final boolean allowOverflowY = this.handlerInfo.isAllowOverflowY();
@@ -76,6 +80,15 @@ public class RecipePageManager {
for (int groupIndex = 0; groupIndex < groupedRecipes.size(); groupIndex++) {
final RecipeWidget widget = new RecipeWidget(this.handler.original, groupedRecipes.get(groupIndex));
// keep the pause of the rebuilt widgets
for (RecipeWidget paused : pausedWidgets) {
final int pausedIndex = paused.getRecipeHandlerRef().recipeIndex;
if (widget.containsRecipeIndex(pausedIndex)) {
widget.pinRecipe(pausedIndex, paused.getPinnedIngredients());
}
}
widget.w = width;
if (allowOverflowY) {
@@ -144,6 +157,33 @@ public class RecipePageManager {
return false;
}
private List<RecipeWidget> getPausedWidgets() {
final List<RecipeWidget> paused = new ArrayList<>();
for (List<Widget> widgets : this.pageWidgets) {
for (Widget widget : widgets) {
if (widget instanceof RecipeWidget recipeWidget && recipeWidget.isPaused()) {
paused.add(recipeWidget);
}
}
}
return paused;
}
public boolean pinRecipe(int recipeIndex, List<ItemStack> ingredients) {
for (List<Widget> widgets : this.pageWidgets) {
for (Widget widget : widgets) {
if (widget instanceof RecipeWidget recipeWidget && recipeWidget.containsRecipeIndex(recipeIndex)) {
return recipeWidget.pinRecipe(recipeIndex, ingredients);
}
}
}
return false;
}
public List<Widget> getCurrentPageWidgets() {
if (this.pageWidgets.isEmpty()) {
return Collections.emptyList();
@@ -25,7 +25,13 @@ public class RecipeTooltipLineHandler implements ITooltipLineHandler {
0,
0,
176 + TRANSPARENCY_BORDER * 2,
166 + TRANSPARENCY_BORDER * 2).build();
166 + TRANSPARENCY_BORDER * 2)
.setSlices(
BG_PADDING + TRANSPARENCY_BORDER,
BG_PADDING + TRANSPARENCY_BORDER,
BG_PADDING + TRANSPARENCY_BORDER,
BG_PADDING + TRANSPARENCY_BORDER)
.build();
protected long lastUpdate = System.currentTimeMillis();
protected String recipeName = "";
@@ -105,11 +111,7 @@ public class RecipeTooltipLineHandler implements ITooltipLineHandler {
-TRANSPARENCY_BORDER,
-TRANSPARENCY_BORDER,
size.width + TRANSPARENCY_BORDER * 2,
size.height + TRANSPARENCY_BORDER * 2,
BG_PADDING + TRANSPARENCY_BORDER,
BG_PADDING + TRANSPARENCY_BORDER,
BG_PADDING + TRANSPARENCY_BORDER,
BG_PADDING + TRANSPARENCY_BORDER);
size.height + TRANSPARENCY_BORDER * 2);
GL11.glEnable(GL11.GL_ALPHA_TEST);
@@ -131,7 +131,14 @@ public class StackInfo {
}
if (isFluidDisplayItem(stackA)) {
return isFluidDisplayItem(stackB) && getFluid(stackA).isFluidEqual(getFluid(stackB));
if (isFluidDisplayItem(stackB)) {
final FluidStack fluidA = getFluid(stackA);
final FluidStack fluidB = getFluid(stackB);
return fluidA != null && fluidA.isFluidEqual(fluidB);
}
return false;
}
if (stackA.getItem() != stackB.getItem()) {
@@ -287,6 +287,11 @@ public abstract class TemplateRecipeHandler implements ICraftingHandler, IUsageH
@Override
public boolean lastKeyTyped(GuiContainer gui, char keyChar, int keyCode) {
return false;
}
@Override
public boolean onHotkeyPressed(GuiContainer gui) {
if (!canHandle(gui)) return false;
if (KeyManager.isKeyDown("recipe.recipe")) {
@@ -3,11 +3,9 @@ package codechicken.nei.recipe.chain;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.item.ItemStack;
@@ -23,8 +21,8 @@ import codechicken.nei.ItemSorter;
import codechicken.nei.ItemStackAmount;
import codechicken.nei.ItemsTooltipLineHandler;
import codechicken.nei.ItemsTooltipLineHandler.AmountRenderer;
import codechicken.nei.ItemsTooltipLineHandler.StacksAmountRenderer;
import codechicken.nei.ItemsTooltipLineHandler.TotalAmountRenderer;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.NEIClientUtils;
import codechicken.nei.bookmark.BookmarkItem;
import codechicken.nei.drawable.DrawableResource;
@@ -38,25 +36,6 @@ import codechicken.nei.recipe.StackInfo;
public class RecipeChainTooltipLineHandler extends CompositeTooltipLineHandler {
protected static class MissingStacksAmountRenderer extends StacksAmountRenderer {
protected static final int MISSING_COLOR = 0xFF5555;
protected final Set<NBTTagCompound> missing;
private final Map<ItemStack, Boolean> cache = new IdentityHashMap<>();
public MissingStacksAmountRenderer(Set<NBTTagCompound> missing) {
this.missing = missing;
}
@Override
protected int getColor(ItemStack stack) {
return this.cache.computeIfAbsent(stack, key -> this.missing.contains(StackInfo.itemStackToNBT(key, false)))
? MISSING_COLOR
: super.getColor(stack);
}
}
protected static class HandlerIconRenderer extends TotalAmountRenderer {
protected static final int ICON_SIZE = 16;
@@ -284,35 +263,15 @@ public class RecipeChainTooltipLineHandler extends CompositeTooltipLineHandler {
}
}
final Set<NBTTagCompound> missing = new HashSet<>();
if (this.lastShiftKey) {
for (ItemStack stack : inputs) {
missing.add(StackInfo.itemStackToNBT(stack, false));
}
}
// items with a shortage are already in the inputs list, holding the missing amount instead of the required one
final List<ItemStack> covered = new ArrayList<>();
for (ItemStack stack : available) {
if (!missing.contains(StackInfo.itemStackToNBT(stack, false))) {
covered.add(stack);
}
}
final Comparator<ItemStack> comparator = Comparator
.comparing((ItemStack stack) -> StackInfo.getFluid(stack) != null)
.thenComparingInt(stack -> -1 * stack.stackSize).thenComparing(ItemSorter.instance);
inputs.sort(comparator);
covered.sort(comparator);
available.sort(comparator);
outputs.sort(comparator);
remainder.sort(comparator);
// missing items stay on top of the list
inputs.addAll(covered);
if (this.lastShiftKey) {
outputs.addAll(remainder);
}
@@ -331,12 +290,21 @@ public class RecipeChainTooltipLineHandler extends CompositeTooltipLineHandler {
EnumChatFormatting.BLUE,
new HandlerIconRenderer(craftingHandlers));
if (this.lastShiftKey) {
addLine(
NEIClientUtils.translate("bookmark.crafting_chain.missing"),
inputs,
true,
EnumChatFormatting.RED,
new TotalAmountRenderer());
}
addLine(
NEIClientUtils.translate("bookmark.crafting_chain.input"),
inputs,
this.lastShiftKey ? available : inputs,
this.lastShiftKey,
EnumChatFormatting.YELLOW,
this.lastShiftKey ? new MissingStacksAmountRenderer(missing) : new TotalAmountRenderer());
new TotalAmountRenderer());
}
protected static void putHandlerInfo(Map<NBTTagCompound, HandlerInfo> handlers, ItemStack stack,
@@ -354,8 +322,8 @@ public class RecipeChainTooltipLineHandler extends CompositeTooltipLineHandler {
protected void addLine(String label, List<ItemStack> items, boolean vertical, EnumChatFormatting labelColor,
AmountRenderer amountRenderer) {
final ItemsTooltipLineHandler line = vertical ? ItemsTooltipLineHandler.list(label, items, maxLineRows())
: ItemsTooltipLineHandler.grid(label, items, maxLineRows());
final int maxNamedRows = vertical ? NEIClientConfig.getMaxVerticalTooltipRows() : 0;
final ItemsTooltipLineHandler line = ItemsTooltipLineHandler.of(label, items, maxLineRows(), maxNamedRows);
if (line.isEmpty()) {
return;
@@ -25,6 +25,7 @@ public class CheckboxWidget extends Button implements IUpdatableWidget {
@Override
public boolean onButtonPress(boolean rightclick) {
if (this.state == 2) return false;
this.value = !this.value;
onChange.accept(this);
return true;
@@ -30,19 +30,24 @@ import codechicken.nei.ClientHandler;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.NEIClientUtils;
import codechicken.nei.Widget;
import codechicken.nei.api.API;
import codechicken.nei.drawable.DrawableBuilder;
import codechicken.nei.drawable.DrawableResource;
import codechicken.nei.guihook.GuiContainerManager;
import codechicken.nei.guihook.IContainerTooltipHandler;
import codechicken.nei.recipe.GuiRecipe;
import codechicken.nei.recipe.GuiRecipeButton;
import codechicken.nei.recipe.GuiRecipeTab;
import codechicken.nei.recipe.HandlerInfo;
import codechicken.nei.recipe.IRecipeHandler;
import codechicken.nei.recipe.RecipeHandlerRef;
import codechicken.nei.recipe.ShapedRecipeHandler;
import codechicken.nei.recipe.TemplateRecipeHandler;
import codechicken.nei.recipe.TemplateRecipeHandler.RecipeTransferRect;
import codechicken.nei.recipe.widget.RecipeWidget;
import codechicken.nei.scroll.ScrollContainer;
public class DebugHandlerWidget extends Widget {
public class DebugHandlerWidget extends Widget implements IContainerTooltipHandler {
private static class HandlerInfoRecord {
@@ -152,6 +157,9 @@ public class DebugHandlerWidget extends Widget {
private Map<String, HandlerInfoRecord> patches = new HashMap<>();
private HandlerInfoRecord record;
private RecipeWidget hoveredRecipe;
private boolean duplicatesOnly = false;
private String duplicatesHandlerId;
private Point dragPoint = null;
private boolean showWidget = false;
@@ -162,11 +170,19 @@ public class DebugHandlerWidget extends Widget {
0,
0,
176 + TRANSPARENCY_BORDER * 2,
166 + TRANSPARENCY_BORDER * 2).build();
166 + TRANSPARENCY_BORDER * 2)
.setSlices(
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER)
.build();
private static final int BORDER_PADDING = 6;
private static final int TRANSPARENCY_BORDER = 4;
public DebugHandlerWidget() {
protected DebugHandlerWidget() {
GuiContainerManager.addTooltipHandler(this);
this.w = WINDOW_WIDTH;
this.x = 10;
this.y = 50;
@@ -242,6 +258,20 @@ public class DebugHandlerWidget extends Widget {
saveHandlerInfoPatch();
}, y);
if (Boolean.getBoolean("nei.recipeOrigins")) {
y = addCheckboxWidget("DuplicatesOnly", button -> {
this.duplicatesOnly = button.value;
if (NEIClientUtils.getGuiContainer() instanceof GuiRecipe<?>recipe) {
this.duplicatesHandlerId = getHandlerID(recipe.getHandler());
recipe.forceRefreshPage();
}
}, y + LINE_HEIGHT / 2);
// only the handler the checkbox was toggled on is filtered, so switching handlers does not pay for a rescan
API.addRecipeFilter(() -> this.duplicatesOnly ? this::isDuplicate : null);
}
this.h = 10 + LINE_HEIGHT + y;
this.container.h = this.h - INLINE_PADDING * 2 - LINE_HEIGHT;
}
@@ -355,11 +385,7 @@ public class DebugHandlerWidget extends Widget {
this.x - TRANSPARENCY_BORDER,
this.y - TRANSPARENCY_BORDER,
this.w + TRANSPARENCY_BORDER * 2,
this.h + TRANSPARENCY_BORDER * 2,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER,
BORDER_PADDING + TRANSPARENCY_BORDER);
this.h + TRANSPARENCY_BORDER * 2);
GuiDraw.drawRect(
this.x + INLINE_PADDING,
this.y + BORDER_PADDING,
@@ -417,6 +443,11 @@ public class DebugHandlerWidget extends Widget {
updateOverride();
if (this.values.get("duplicatesonly") instanceof CheckboxWidget duplicates) {
duplicates.state = handler instanceof ShapedRecipeHandler ? 0 : 2;
resetDuplicatesOnly();
}
this.container.removeIf(
widget -> this.values.get("height") == widget || this.values.get("height_readonly") == widget);
if (handler.getRecipeHeight(0) > 0) {
@@ -430,11 +461,26 @@ public class DebugHandlerWidget extends Widget {
} else {
this.record = null;
this.showWidget = false;
resetDuplicatesOnly();
}
this.container.update();
}
private void resetDuplicatesOnly() {
if (!this.duplicatesOnly) return;
this.duplicatesOnly = false;
if (this.values.get("duplicatesonly") instanceof CheckboxWidget duplicates) {
duplicates.updateValue("false");
}
if (NEIClientUtils.getGuiContainer() instanceof GuiRecipe<?>recipe) {
recipe.forceRefreshPage();
}
}
@Override
public List<String> handleTooltip(int mx, int my, List<String> tooltip) {
return this.record != null ? this.container.handleTooltip(mx, my, tooltip) : tooltip;
@@ -466,6 +512,11 @@ public class DebugHandlerWidget extends Widget {
return false;
}
@Override
public boolean onHotkeyPressed() {
return this.record != null && this.container.onHotkeyPressed();
}
@Override
public void onGuiClick(int mx, int my) {
if (this.record != null) {
@@ -532,9 +583,51 @@ public class DebugHandlerWidget extends Widget {
}
}
@Override
public List<String> handleTooltip(GuiContainer gui, int mousex, int mousey, List<String> tooltip) {
final RecipeWidget widget = this.hoveredRecipe;
this.hoveredRecipe = null;
if (widget == null || !this.showWidget || !tooltip.isEmpty()) {
return tooltip;
}
final RecipeHandlerRef ref = widget.getRecipeHandlerRef();
for (List<String> origin : RecipeOrigins.findOrigins(ref.handler, ref.recipeIndex)) {
addOriginLines(tooltip, origin);
}
return tooltip;
}
private static void addOriginLines(List<String> tooltip, List<String> lines) {
tooltip.add(
EnumChatFormatting.WHITE + NEIClientUtils.translate("debug.RecipeHandler.RegisteredBy", lines.get(0))
+ GuiDraw.TOOLTIP_LINESPACE);
for (int i = 1; i < lines.size(); i++) {
tooltip.add(EnumChatFormatting.GRAY + lines.get(i));
}
tooltip.add("");
}
// only the handler the checkbox was toggled on is filtered
private boolean isDuplicate(IRecipeHandler handler, int recipeIndex) {
return !getHandlerID(handler).equals(this.duplicatesHandlerId)
|| RecipeOrigins.hasDuplicates(handler, recipeIndex);
}
public void drawGuiPlaceholder(RecipeWidget widget) {
if (!this.showWidget) return;
final Point mouse = GuiDraw.getMousePosition();
if (widget.getRecipeHandlerRef().handler instanceof ShapedRecipeHandler && widget.contains(mouse.x, mouse.y)
&& widget.getStackMouseOver(mouse.x, mouse.y) == null) {
this.hoveredRecipe = widget;
}
NEIClientUtils.gl2DRenderContext(() -> {
// background
@@ -0,0 +1,317 @@
package codechicken.nei.recipe.debug;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.CraftingManager;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.ShapedRecipes;
import net.minecraft.item.crafting.ShapelessRecipes;
import net.minecraftforge.oredict.ShapedOreRecipe;
import net.minecraftforge.oredict.ShapelessOreRecipe;
import codechicken.nei.NEIServerUtils;
import codechicken.nei.PositionedStack;
import codechicken.nei.recipe.IRecipeHandler;
import codechicken.nei.recipe.ShapedRecipeHandler;
import codechicken.nei.recipe.ShapedRecipeHandler.CachedShapedRecipe;
import codechicken.nei.recipe.TemplateRecipeHandler.CachedRecipe;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.ModContainer;
/**
* Tracks which mod registered each crafting recipe, so the recipe handler debug widget can show it in the recipe
* tooltip. {@link TrackingList} is spliced in as the type of {@link CraftingManager}'s internal recipe list by
* {@link codechicken.nei.asm.NEITransformer} (see the {@code <init>} transform there) in place of the plain
* {@code ArrayList} vanilla constructs it with. Patching the constructor itself, rather than swapping the field later
* on, means every single recipe is observed, including vanilla's own, which are added by that same constructor before
* any mod code ever runs. Opt-in via {@code -Dnei.recipeOrigins=true}, since capturing a stack trace per recipe slows
* down loading.
*/
public class RecipeOrigins {
private static final int MAX_FRAMES = 10;
private static final Map<IRecipe, List<String>> origins = Collections.synchronizedMap(new IdentityHashMap<>());
private static class OutputIndex {
final int size;
final Map<Item, List<IRecipe>> byItem;
OutputIndex(int size, Map<Item, List<IRecipe>> byItem) {
this.size = size;
this.byItem = byItem;
}
}
private static final Map<CachedRecipe, List<List<String>>> cache = new WeakHashMap<>();
private static volatile OutputIndex outputIndex = new OutputIndex(-1, Collections.emptyMap());
private RecipeOrigins() {}
public static List<List<String>> findOrigins(IRecipeHandler handler, int recipeIndex) {
if (!(handler instanceof ShapedRecipeHandler shaped)) return Collections.emptyList();
final CachedRecipe recipe = shaped.arecipes.get(recipeIndex);
List<List<String>> found;
synchronized (cache) {
found = cache.get(recipe);
}
if (found == null) {
found = findOrigins(shaped, recipe.getResult(), recipe.getIngredients());
synchronized (cache) {
cache.put(recipe, found);
}
}
return found;
}
public static boolean hasDuplicates(IRecipeHandler handler, int recipeIndex) {
return handler instanceof ShapedRecipeHandler shaped
&& hasDuplicateCandidates(shaped.arecipes.get(recipeIndex).getResult())
&& findOrigins(handler, recipeIndex).size() > 1;
}
private static boolean hasDuplicateCandidates(PositionedStack result) {
if (origins.isEmpty() || result == null || result.item == null) return false;
int count = 0;
for (IRecipe recipe : getRecipesByOutput(result.item.getItem())) {
if (origins.containsKey(recipe)
&& NEIServerUtils.areStacksSameTypeCrafting(recipe.getRecipeOutput(), result.item)
&& ++count > 1) {
return true;
}
}
return false;
}
private static List<List<String>> findOrigins(ShapedRecipeHandler handler, PositionedStack result,
List<PositionedStack> ingredients) {
if (origins.isEmpty() || result == null || result.item == null) return Collections.emptyList();
final List<List<String>> found = new ArrayList<>();
for (IRecipe recipe : getRecipesByOutput(result.item.getItem())) {
final List<String> origin = origins.get(recipe);
if (origin != null && NEIServerUtils.areStacksSameTypeCrafting(recipe.getRecipeOutput(), result.item)
&& matchesIngredients(handler, recipe, ingredients)) {
final List<String> entry = new ArrayList<>(origin);
entry.add(recipe.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(recipe)));
found.add(entry);
}
}
return found;
}
private static List<IRecipe> getRecipesByOutput(Item item) {
final List<IRecipe> recipes = CraftingManager.getInstance().getRecipeList();
OutputIndex index = outputIndex;
if (recipes.size() != index.size) {
synchronized (RecipeOrigins.class) {
index = outputIndex;
if (recipes.size() != index.size) {
final Map<Item, List<IRecipe>> byItem = new HashMap<>();
for (IRecipe recipe : recipes) {
final ItemStack output = recipe.getRecipeOutput();
if (output != null)
byItem.computeIfAbsent(output.getItem(), k -> new ArrayList<>()).add(recipe);
}
outputIndex = index = new OutputIndex(recipes.size(), byItem);
}
}
}
return index.byItem.getOrDefault(item, Collections.emptyList());
}
private static boolean matchesIngredients(ShapedRecipeHandler handler, IRecipe recipe,
List<PositionedStack> ingredients) {
if (recipe instanceof ShapedRecipes shaped) {
return matchesGrid(handler.new CachedShapedRecipe(shaped), ingredients);
}
if (recipe instanceof ShapedOreRecipe shaped)
return matchesGrid(handler.forgeShapedRecipe(shaped), ingredients);
if (recipe instanceof ShapelessRecipes shapeless) return matchesUnordered(shapeless.recipeItems, ingredients);
if (recipe instanceof ShapelessOreRecipe shapeless) return matchesUnordered(shapeless.getInput(), ingredients);
return false;
}
private static boolean matchesGrid(CachedShapedRecipe reference, List<PositionedStack> ingredients) {
if (reference == null || reference.ingredients.size() != ingredients.size()) return false;
for (PositionedStack expected : reference.ingredients) {
final PositionedStack actual = ingredients.stream()
.filter(stack -> stack.relx == expected.relx && stack.rely == expected.rely).findFirst()
.orElse(null);
if (actual == null || !matchesItems(expected.items, actual.items)) return false;
}
return true;
}
private static boolean matchesItems(ItemStack[] expected, ItemStack[] actual) {
for (ItemStack candidate : expected) {
for (ItemStack item : actual) {
if (NEIServerUtils.areStacksSameTypeCrafting(candidate, item)) return true;
}
}
return false;
}
private static boolean matchesUnordered(List<?> inputs, List<PositionedStack> ingredients) {
final List<PositionedStack> remaining = new ArrayList<>(ingredients);
for (Object input : inputs) {
if (input == null) continue;
final PositionedStack match = remaining.stream().filter(stack -> matchesInput(input, stack)).findFirst()
.orElse(null);
if (match == null) return false;
remaining.remove(match);
}
return remaining.isEmpty();
}
private static boolean matchesInput(Object input, PositionedStack stack) {
final List<?> candidates = input instanceof List<?>list ? list : Collections.singletonList(input);
for (Object candidate : candidates) {
if (!(candidate instanceof ItemStack candidateStack)) continue;
for (ItemStack item : stack.items) {
if (NEIServerUtils.areStacksSameTypeCrafting(candidateStack, item)) return true;
}
}
return false;
}
public static class TrackingList extends ArrayList<IRecipe> {
@Override
public boolean add(IRecipe recipe) {
origins.put(recipe, captureOrigin());
return super.add(recipe);
}
@Override
public void add(int index, IRecipe recipe) {
origins.put(recipe, captureOrigin());
super.add(index, recipe);
}
@Override
public IRecipe set(int index, IRecipe recipe) {
origins.put(recipe, captureOrigin());
return super.set(index, recipe);
}
@Override
public boolean addAll(Collection<? extends IRecipe> recipes) {
final List<String> origin = captureOrigin();
for (IRecipe recipe : recipes) origins.put(recipe, origin);
return super.addAll(recipes);
}
@Override
public boolean addAll(int index, Collection<? extends IRecipe> recipes) {
final List<String> origin = captureOrigin();
for (IRecipe recipe : recipes) origins.put(recipe, origin);
return super.addAll(index, recipes);
}
@Override
public IRecipe remove(int index) {
final IRecipe removed = super.remove(index);
origins.remove(removed);
return removed;
}
@Override
public boolean remove(Object recipe) {
final boolean removed = super.remove(recipe);
if (removed) origins.remove(recipe);
return removed;
}
@Override
public void clear() {
origins.clear();
super.clear();
}
}
private static List<String> captureOrigin() {
final List<StackTraceElement> frames = findCallerFrames();
if (frames.isEmpty()) return Collections.singletonList("Unknown");
final ModContainer container = Loader.instance().activeModContainer();
final List<String> lines = new ArrayList<>(frames.size() + 1);
lines.add(container != null ? container.getName() : "Unknown");
for (int i = 0; i < frames.size(); i++) {
lines.add(formatFrame(frames.get(i)));
}
return lines;
}
private static String formatFrame(StackTraceElement frame) {
return frame.getFileName() != null ? frame.getClassName() + "."
+ frame.getMethodName()
+ "("
+ frame.getFileName()
+ ":"
+ frame.getLineNumber()
+ ")" : frame.getClassName() + "." + frame.getMethodName();
}
private static List<StackTraceElement> findCallerFrames() {
final List<StackTraceElement> frames = new ArrayList<>(MAX_FRAMES);
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
final String className = element.getClassName();
if (className.startsWith("java.") || className.startsWith("net.minecraft.")
|| className.startsWith("net.minecraftforge.")
|| className.startsWith("cpw.mods.fml.")
|| className.startsWith("sun.")
|| className.startsWith("jdk.")
|| className.startsWith("org.spongepowered.")
|| className.startsWith("codechicken.nei.recipe.debug.RecipeOrigins")) {
continue;
}
frames.add(element);
if (frames.size() >= MAX_FRAMES) break;
}
return frames;
}
}
@@ -88,6 +88,12 @@ public class GTFluidStackStringifyHandler implements IStackStringifyHandler {
return (FluidStack) obj;
}
final Fluid fluid = FluidRegistry.getFluid(stack.getItemDamage());
if (fluid != null) {
return new FluidStack(fluid, 0);
}
} else if (item == GameRegistry.findItem("ae2fc", "fluid_packet")) {
NBTTagCompound nbtTag = stack.getTagCompound();
@@ -1,6 +1,7 @@
package codechicken.nei.recipe.widget;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -50,6 +51,11 @@ public class PermutationsCycler<K> {
return false;
}
public void invalidate() {
this.favoriteRevision = -1;
validate();
}
public boolean contains(K key) {
validate();
return this.permutations.containsKey(key);
@@ -79,6 +85,11 @@ public class PermutationsCycler<K> {
return items;
}
public int getFavoriteCount(K key) {
get(key);
return this.favoriteCounts.getOrDefault(key, 0);
}
public ItemStack getCycled(K key, int cycle) {
final List<ItemStack> items = get(key);
final int favoriteCount = this.favoriteCounts.getOrDefault(key, 0);
@@ -96,4 +107,10 @@ public class PermutationsCycler<K> {
return -1;
}
// Hash key with the same equality as NEIServerUtils.areStacksSameType
public static Object getStackKey(ItemStack stack) {
return Arrays
.asList(stack.getItem(), stack.getHasSubtypes() ? stack.getItemDamage() : 0, stack.stackTagCompound);
}
}
@@ -2,23 +2,109 @@ package codechicken.nei.recipe.widget;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.item.ItemStack;
import codechicken.nei.FavoriteRecipes;
import codechicken.nei.NEIServerUtils;
import codechicken.nei.PositionedStack;
import codechicken.nei.recipe.GuiRecipeTab;
import codechicken.nei.recipe.HandlerInfo;
import codechicken.nei.recipe.IRecipeHandler;
import codechicken.nei.recipe.StackInfo;
public class RecipeGroup {
// position and items of a stack at load time, the handler may change the stack later
protected static class StackState {
public final int relx;
public final int rely;
public final ItemStack[] items;
public StackState(PositionedStack pStack) {
this.relx = pStack.relx;
this.rely = pStack.rely;
this.items = pStack.items;
}
}
// filtered permutations of a slot, favorites first, with their indices in PositionedStack.items
protected static class FilteredSlot {
public final List<ItemStack> items;
public final int[] indices;
public final int favoriteCount;
public FilteredSlot(PositionedStack pStack) {
this.items = pStack.getFilteredPermutations();
this.indices = new int[this.items.size()];
// filtering keeps the original instances, so an identity lookup finds the source index
for (int i = 0; i < this.indices.length; i++) {
this.indices[i] = indexOfInstance(pStack.items, this.items.get(i));
}
int favoriteCount = 0;
while (this.items.size() > 1 && favoriteCount < this.items.size()
&& FavoriteRecipes.containsManual(this.items.get(favoriteCount))) {
favoriteCount++;
}
this.favoriteCount = favoriteCount;
}
private static int indexOfInstance(ItemStack[] items, ItemStack stack) {
for (int i = 0; i < items.length; i++) {
if (items[i] == stack) {
return i;
}
}
return 0;
}
}
protected static class MemberStacks {
public final List<PositionedStack> inputs;
public final List<PositionedStack> outputs;
public final List<PositionedStack> auxiliaries;
public final List<PositionedStack> cycling;
public final List<StackState> states = new ArrayList<>();
public final Map<Point, PositionedStack> slots = new HashMap<>();
public final Map<Point, FilteredSlot> filtered = new HashMap<>();
public MemberStacks(List<PositionedStack> inputs, List<PositionedStack> outputs,
List<PositionedStack> auxiliaries) {
this.inputs = inputs;
this.outputs = outputs;
this.auxiliaries = auxiliaries;
if (auxiliaries.isEmpty()) {
this.cycling = inputs;
} else {
this.cycling = new ArrayList<>(inputs);
this.cycling.addAll(auxiliaries);
}
for (PositionedStack pStack : this.cycling) {
this.slots.putIfAbsent(new Point(pStack.relx, pStack.rely), pStack);
}
for (PositionedStack pStack : inputs) this.states.add(new StackState(pStack));
for (PositionedStack pStack : outputs) this.states.add(new StackState(pStack));
for (PositionedStack pStack : auxiliaries) this.states.add(new StackState(pStack));
}
}
private static final List<Integer> SINGLE_MEMBER = Collections.singletonList(0);
private final IRecipeHandler handler;
@@ -26,16 +112,33 @@ public class RecipeGroup {
private final HandlerInfo handlerInfo;
private final PermutationsCycler<Point> slotPermutations;
private final MemberStacks[] memberStacks;
private Map<Point, PositionedStack[]> slotStacks = null;
private final Set<Point> sharedSlots = new HashSet<>();
private List<Integer> allowedMembers = null;
private Boolean cycling = null;
public RecipeGroup(IRecipeHandler handler, List<Integer> recipeIndices) {
this.handler = handler;
this.recipeIndices = new ArrayList<>(recipeIndices);
this.handlerInfo = GuiRecipeTab.getHandlerInfo(handler);
this.memberStacks = new MemberStacks[this.recipeIndices.size()];
this.slotPermutations = new PermutationsCycler<Point>(
new HashMap<>(),
new HashMap<>(),
this::collectSlotPermutations).onInvalidate(() -> this.allowedMembers = null);
this::collectSlotPermutations).onInvalidate(() -> {
this.allowedMembers = null;
this.cycling = null;
this.slotStacks = null;
for (MemberStacks stacks : this.memberStacks) {
if (stacks != null) {
stacks.filtered.clear();
}
}
this.sharedSlots.clear();
});
}
public int size() {
@@ -58,34 +161,49 @@ public class RecipeGroup {
return this.recipeIndices.contains(recipeIndex);
}
public int indexOf(int recipeIndex) {
return this.recipeIndices.indexOf(recipeIndex);
}
public List<ItemStack> getPermutations(Point slotKey) {
return this.slotPermutations.get(slotKey);
}
public boolean hasPermutation(int member, Point slotKey, ItemStack stack) {
this.slotPermutations.validate();
if (stack == null) {
return false;
}
final List<ItemStack> permutations = getFilteredPermutations(member, slotKey);
return permutations != null && PermutationsCycler.indexOf(permutations, stack) != -1;
}
// the given member if it has the item in the slot, otherwise the next member that has it
public int findMember(Point slotKey, ItemStack stack, int from) {
for (int step = 0; step < size(); step++) {
final int member = (from + step) % size();
if (hasPermutation(member, slotKey, stack)) {
return member;
}
}
return -1;
}
public List<PositionedStack> getInputs(int member) {
return this.handler.getIngredientStacks(getRecipeIndex(member));
return getStacks(member).inputs;
}
public List<PositionedStack> getOutputs(int member) {
final int recipeIndex = getRecipeIndex(member);
final PositionedStack pStackResult = this.handler.getResultStack(recipeIndex);
return pStackResult != null ? Arrays.asList(pStackResult) : this.handler.getOtherStacks(recipeIndex);
}
public List<PositionedStack> getCatalysts(int member) {
final int recipeIndex = getRecipeIndex(member);
if (this.handler.getResultStack(recipeIndex) == null) {
return Collections.emptyList();
}
return this.handler.getOtherStacks(recipeIndex);
return getStacks(member).outputs;
}
public List<PositionedStack> getCyclingStacks(int member) {
final List<PositionedStack> stacks = new ArrayList<>(getInputs(member));
stacks.addAll(getCatalysts(member));
return stacks;
return getStacks(member).cycling;
}
public Point getSlotKey(PositionedStack pStack) {
@@ -93,39 +211,108 @@ public class RecipeGroup {
}
public PositionedStack getSlot(int member, Point slotKey) {
for (PositionedStack pStack : getCyclingStacks(member)) {
if (pStack.relx == slotKey.x && pStack.rely == slotKey.y) {
return pStack;
}
}
return null;
return getStacks(member).slots.get(slotKey);
}
public PositionedStack getOutputSlot(int member, Point slotKey) {
protected MemberStacks getStacks(int member) {
for (PositionedStack pStack : getOutputs(member)) {
if (pStack.relx == slotKey.x && pStack.rely == slotKey.y) {
return pStack;
}
if (this.memberStacks[member] == null) {
this.memberStacks[member] = loadStacks(member);
}
return null;
return this.memberStacks[member];
}
public List<ItemStack> getOutputVariants(Point slotKey) {
final List<ItemStack> items = new ArrayList<>();
protected MemberStacks loadStacks(int member) {
final int recipeIndex = getRecipeIndex(member);
final List<PositionedStack> inputs = new ArrayList<>(this.handler.getIngredientStacks(recipeIndex));
final PositionedStack pStackResult = this.handler.getResultStack(recipeIndex);
final List<PositionedStack> others = new ArrayList<>(this.handler.getOtherStacks(recipeIndex));
for (int member = 0; member < size(); member++) {
final PositionedStack slot = getOutputSlot(member, slotKey);
if (pStackResult != null) {
return new MemberStacks(inputs, Collections.singletonList(pStackResult), others);
}
if (slot != null && PermutationsCycler.indexOf(items, slot.item) == -1) {
items.add(slot.item);
return new MemberStacks(inputs, others, Collections.emptyList());
}
// re-reads the member stacks from the handler, keeps the cached instances when nothing changed
public void refresh(int member) {
if (this.memberStacks[member] == null) {
return;
}
final MemberStacks stacks = loadStacks(member);
if (!isSameStacks(this.memberStacks[member], stacks)) {
this.memberStacks[member] = stacks;
this.slotPermutations.invalidate();
}
}
// index into PositionedStack.items of the permutation to render, -1 when the slot is unknown
public int getCycledPermutationIndex(int member, Point slotKey, int index) {
this.slotPermutations.validate();
final FilteredSlot filtered = getFilteredSlot(member, slotKey);
if (filtered == null || filtered.indices.length == 0) {
return -1;
}
final int size = filtered.favoriteCount > 0 ? filtered.favoriteCount : filtered.indices.length;
return filtered.indices[index % size];
}
protected boolean isSameStacks(MemberStacks stacksA, MemberStacks stacksB) {
if (stacksA.inputs.size() != stacksB.inputs.size() || stacksA.outputs.size() != stacksB.outputs.size()
|| stacksA.auxiliaries.size() != stacksB.auxiliaries.size()) {
return false;
}
for (int i = 0; i < stacksA.states.size(); i++) {
final StackState stateA = stacksA.states.get(i);
final StackState stateB = stacksB.states.get(i);
if (stateA.relx != stateB.relx || stateA.rely != stateB.rely || !isSameItems(stateA.items, stateB.items)) {
return false;
}
}
return items;
return true;
}
protected boolean isSameItems(ItemStack[] itemsA, ItemStack[] itemsB) {
if (itemsA == itemsB) {
return true;
}
if (itemsA.length != itemsB.length) {
return false;
}
for (int i = 0; i < itemsA.length; i++) {
if (!isSameStack(itemsA[i], itemsB[i])) {
return false;
}
}
return true;
}
// true when the group has something to cycle: several members or a slot with several permutations
public boolean canCycle() {
this.slotPermutations.validate();
if (this.cycling == null) {
this.cycling = size() > 1 || getStacks(0).slots.keySet().stream()
.anyMatch(slotKey -> getFilteredPermutations(0, slotKey).size() > 1);
}
return this.cycling;
}
public List<Integer> getAllowedMembers() {
@@ -135,17 +322,20 @@ public class RecipeGroup {
return SINGLE_MEMBER;
}
if (this.allowedMembers == null) {
final Set<Point> favoriteSlots = new HashSet<>();
if (this.allowedMembers == null && !FavoriteRecipes.hasManual()) {
this.allowedMembers = new ArrayList<>(size());
for (int member = 0; member < size(); member++) {
for (PositionedStack pStack : getCyclingStacks(member)) {
final Point slotKey = getSlotKey(pStack);
final List<ItemStack> items = getPermutations(slotKey);
this.allowedMembers.add(member);
}
if (items.size() > 1 && items.stream().anyMatch(FavoriteRecipes::containsManual)) {
favoriteSlots.add(slotKey);
}
} else if (this.allowedMembers == null) {
final Set<Point> favoriteSlots = new HashSet<>();
for (Point slotKey : getSlotStacks().keySet()) {
// a shared slot gives every member the same misses, so it can't affect the filter
if (!this.sharedSlots.contains(slotKey) && this.slotPermutations.getFavoriteCount(slotKey) > 0) {
favoriteSlots.add(slotKey);
}
}
@@ -155,6 +345,72 @@ public class RecipeGroup {
return this.allowedMembers;
}
protected Map<Point, PositionedStack[]> getSlotStacks() {
if (this.slotStacks == null) {
this.slotStacks = new LinkedHashMap<>();
for (int member = 0; member < size(); member++) {
for (PositionedStack pStack : getCyclingStacks(member)) {
final PositionedStack[] stacks = this.slotStacks
.computeIfAbsent(getSlotKey(pStack), key -> new PositionedStack[size()]);
if (stacks[member] == null) {
stacks[member] = pStack;
}
}
}
for (Map.Entry<Point, PositionedStack[]> entry : this.slotStacks.entrySet()) {
if (isSharedSlot(entry.getValue())) {
this.sharedSlots.add(entry.getKey());
}
}
}
return this.slotStacks;
}
// all members hold the same permutations in this slot
protected boolean isSharedSlot(PositionedStack[] stacks) {
ItemStack[] first = null;
for (PositionedStack pStack : stacks) {
if (pStack == null) {
continue;
}
if (first == null) {
first = pStack.items;
} else if (!isSameItems(first, pStack.items)) {
return false;
}
}
return true;
}
protected boolean isSameStack(ItemStack stackA, ItemStack stackB) {
return stackA == stackB || NEIServerUtils.areStacksSameType(stackA, stackB)
&& StackInfo.getAmount(stackA) == StackInfo.getAmount(stackB);
}
protected List<ItemStack> getFilteredPermutations(int member, Point slotKey) {
final FilteredSlot filtered = getFilteredSlot(member, slotKey);
return filtered != null ? filtered.items : null;
}
protected FilteredSlot getFilteredSlot(int member, Point slotKey) {
final MemberStacks stacks = getStacks(member);
final PositionedStack slot = stacks.slots.get(slotKey);
if (slot == null) {
return null;
}
return stacks.filtered.computeIfAbsent(slotKey, key -> new FilteredSlot(slot));
}
protected List<Integer> filterMembers(Set<Point> favoriteSlots) {
final List<Integer> indices = new ArrayList<>();
int minMisses = Integer.MAX_VALUE;
@@ -179,9 +435,9 @@ public class RecipeGroup {
int misses = 0;
for (Point slotKey : favoriteSlots) {
final PositionedStack slot = getSlot(member, slotKey);
final List<ItemStack> permutations = getFilteredPermutations(member, slotKey);
if (slot != null && slot.getFilteredPermutations().stream().noneMatch(FavoriteRecipes::containsManual)) {
if (permutations != null && permutations.stream().noneMatch(FavoriteRecipes::containsManual)) {
misses++;
}
}
@@ -190,21 +446,31 @@ public class RecipeGroup {
}
protected List<ItemStack> collectSlotPermutations(Point slotKey) {
final List<ItemStack> items = new ArrayList<>();
if (!getSlotStacks().containsKey(slotKey)) {
return Collections.emptyList();
}
final Map<Object, ItemStack> items = new LinkedHashMap<>();
final boolean shared = this.sharedSlots.contains(slotKey);
for (int member = 0; member < size(); member++) {
final PositionedStack slot = getSlot(member, slotKey);
final List<ItemStack> permutations = getFilteredPermutations(member, slotKey);
if (slot != null) {
for (ItemStack stack : slot.getFilteredPermutations()) {
if (PermutationsCycler.indexOf(items, stack) == -1) {
items.add(stack);
if (permutations != null) {
for (ItemStack stack : permutations) {
if (stack != null) {
items.putIfAbsent(PermutationsCycler.getStackKey(stack), stack);
}
}
if (shared) {
break;
}
}
}
return items;
return new ArrayList<>(items.values());
}
}
@@ -2,6 +2,7 @@ package codechicken.nei.recipe.widget;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -21,7 +22,6 @@ import org.lwjgl.opengl.GL12;
import codechicken.lib.gui.GuiDraw;
import codechicken.lib.vec.Rectangle4i;
import codechicken.nei.FavoriteRecipes;
import codechicken.nei.ItemsTooltipLineHandler;
import codechicken.nei.KeyManager;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.NEIClientUtils;
@@ -29,6 +29,8 @@ import codechicken.nei.PositionedStack;
import codechicken.nei.Widget;
import codechicken.nei.api.IGuiContainerOverlay;
import codechicken.nei.api.ShortcutInputHandler;
import codechicken.nei.drawable.DrawableBuilder;
import codechicken.nei.drawable.DrawableResource;
import codechicken.nei.guihook.GuiContainerManager;
import codechicken.nei.recipe.AcceptsFollowingTooltipLineHandler;
import codechicken.nei.recipe.Badge;
@@ -47,28 +49,35 @@ import codechicken.nei.util.NEIMouseUtils;
public class RecipeWidget extends Widget {
protected static final int TICKS_PER_CYCLE = 20;
protected static final DrawableResource PAUSE_MARKER = new DrawableBuilder(
"nei:textures/pinned_pause.png",
0,
0,
8,
8).setTextureSize(8, 8).build();
protected static final int PAUSE_MARKER_OFFSET = 1;
protected final RecipeGroup group;
protected final IRecipeHandler handler;
protected final HandlerInfo handlerInfo;
protected final PermutationsCycler<PositionedStack> renderPermutations;
protected final Map<PositionedStack, List<Badge>> badgeCache = new WeakHashMap<>();
protected final Map<Integer, List<GuiRecipeButton>> buttonCache = new HashMap<>();
protected AcceptsFollowingTooltipLineHandler acceptsTooltip;
protected ItemsTooltipLineHandler variantsTooltip;
protected Point variantsSlotKey;
protected int activeMember = 0;
protected int memberCursor = -1;
protected int slotCycle = 0;
// single state counter: member = allowed[cycle % size], permutation index = cycle / size
protected int cycle = 0;
protected int cycleticks = 0;
protected int lastcycle = -1;
protected int favoriteRevision = FavoriteRecipes.getRevision();
protected boolean showAsWidget = false;
protected boolean update = true;
protected boolean paused = false;
protected List<ItemStack> pinnedIngredients = null;
protected Map<Point, ItemStack> pinnedSlots = Collections.emptyMap();
public RecipeWidget(RecipeHandlerRef handlerRef) {
this(handlerRef.handler, Collections.singletonList(handlerRef.recipeIndex));
@@ -82,13 +91,6 @@ public class RecipeWidget extends Widget {
this.group = group;
this.handler = group.getHandler();
this.handlerInfo = group.getHandlerInfo();
this.renderPermutations = new PermutationsCycler<PositionedStack>(
new WeakHashMap<>(),
new WeakHashMap<>(),
PositionedStack::getFilteredPermutations).onInvalidate(() -> {
this.badgeCache.clear();
this.acceptsTooltip = null;
});
update();
}
@@ -117,6 +119,104 @@ public class RecipeWidget extends Widget {
this.showAsWidget = show;
}
public boolean isPaused() {
return this.paused;
}
public List<ItemStack> getPinnedIngredients() {
return this.pinnedIngredients;
}
public void setPaused(boolean paused) {
final boolean wasPaused = this.paused;
this.paused = paused && this.group.canCycle();
if (!this.paused) {
this.pinnedIngredients = null;
this.pinnedSlots = Collections.emptyMap();
if (wasPaused) {
syncCycle();
}
}
}
/**
* Pauses the widget on the recipe. Every given ingredient needs an input slot of its own that accepts it, otherwise
* the recipe is not pinned.
*/
public boolean pinRecipe(int recipeIndex, List<ItemStack> ingredients) {
final int member = this.group.indexOf(recipeIndex);
if (member < 0 || !this.group.canCycle()) {
return false;
}
final Map<Point, ItemStack> slots = ingredients != null
? matchIngredients(this.group.getInputs(member), ingredients)
: Collections.emptyMap();
if (slots == null) {
return false;
}
this.pinnedIngredients = ingredients;
this.pinnedSlots = slots;
this.paused = true;
setActiveMember(member);
updatePermutations();
return true;
}
// assigns each ingredient to its own slot (bipartite matching), null when some ingredient has no slot
private Map<Point, ItemStack> matchIngredients(List<PositionedStack> stacks, List<ItemStack> ingredients) {
final int[] slotOwners = new int[stacks.size()];
Arrays.fill(slotOwners, -1);
for (int ingredient = 0; ingredient < ingredients.size(); ingredient++) {
if (!assignIngredient(ingredient, stacks, ingredients, slotOwners, new boolean[stacks.size()])) {
return null;
}
}
final Map<Point, ItemStack> slots = new HashMap<>();
for (int slot = 0; slot < slotOwners.length; slot++) {
if (slotOwners[slot] != -1) {
slots.put(this.group.getSlotKey(stacks.get(slot)), ingredients.get(slotOwners[slot]));
}
}
return slots;
}
private boolean assignIngredient(int ingredient, List<PositionedStack> stacks, List<ItemStack> ingredients,
int[] slotOwners, boolean[] visited) {
for (int slot = 0; slot < stacks.size(); slot++) {
if (!visited[slot] && stacks.get(slot).getPermutationIndex(ingredients.get(ingredient)) != -1) {
visited[slot] = true;
if (slotOwners[slot] == -1
|| assignIngredient(slotOwners[slot], stacks, ingredients, slotOwners, visited)) {
slotOwners[slot] = ingredient;
return true;
}
}
}
return false;
}
private void applyPinnedIngredient(PositionedStack pStack) {
final ItemStack ingredient = this.pinnedSlots.get(this.group.getSlotKey(pStack));
if (ingredient != null) {
pStack.setPermutationToRender(ingredient);
}
}
@Override
public void update() {
@@ -142,11 +242,11 @@ public class RecipeWidget extends Widget {
return this.buttonCache.computeIfAbsent(this.activeMember, this::createButtons);
}
protected List<GuiRecipeButton> getRecipeButtonsIfInit() {
private List<GuiRecipeButton> getRecipeButtonsIfInit() {
return this.buttonCache.getOrDefault(this.activeMember, Collections.emptyList());
}
protected List<GuiRecipeButton> createButtons(int member) {
private List<GuiRecipeButton> createButtons(int member) {
if (this.group.getOutputs(member).isEmpty()) {
return Collections.emptyList();
@@ -156,13 +256,15 @@ public class RecipeWidget extends Widget {
.unmodifiableList(initButtons(RecipeHandlerRef.of(this.handler, this.group.getRecipeIndex(member))));
}
protected List<GuiRecipeButton> initButtons(RecipeHandlerRef ref) {
private List<GuiRecipeButton> initButtons(RecipeHandlerRef ref) {
final GuiRecipe<?> guiRecipe = getGuiRecipe();
if (guiRecipe != null) {
final UpdateRecipeButtonsEvent.Pre preEvent = new UpdateRecipeButtonsEvent.Pre(
guiRecipe,
ref,
this.w,
this.h,
this.handlerInfo);
if (MinecraftForge.EVENT_BUS.post(preEvent)) {
@@ -171,6 +273,8 @@ public class RecipeWidget extends Widget {
final UpdateRecipeButtonsEvent.Post postEvent = new UpdateRecipeButtonsEvent.Post(
guiRecipe,
ref,
this.w,
this.h,
getDefaultButtons(ref));
MinecraftForge.EVENT_BUS.post(postEvent);
return postEvent.buttonList;
@@ -182,7 +286,7 @@ public class RecipeWidget extends Widget {
}
protected List<GuiRecipeButton> getDefaultButtons(RecipeHandlerRef ref) {
private List<GuiRecipeButton> getDefaultButtons(RecipeHandlerRef ref) {
GuiContainer guiContainer = NEIClientUtils.getGuiContainer();
final List<GuiRecipeButton> buttons = new ArrayList<>();
final boolean showFavorites = NEIClientConfig.favoritesEnabled() && handlerInfo.getShowFavoritesButton();
@@ -213,13 +317,17 @@ public class RecipeWidget extends Widget {
if (this.update) {
this.update = false;
if (tickCycle()) {
if (this.showAsWidget || !contains(mouseX, mouseY)) {
nextMember();
}
updatePermutations(this.lastcycle);
if (this.lastcycle == -1) {
this.lastcycle = 0;
applyCycle();
} else if (tickCycle() && !isCycleFrozen(mouseX, mouseY)) {
this.cycle++;
applyCycle();
} else if (this.favoriteRevision != FavoriteRecipes.getRevision()) {
this.favoriteRevision = FavoriteRecipes.getRevision();
this.badgeCache.clear();
this.acceptsTooltip = null;
updatePermutations();
}
for (GuiRecipeButton button : getRecipeButtons()) {
@@ -240,21 +348,7 @@ public class RecipeWidget extends Widget {
GuiContainerManager.enableMatrixStackLogging();
for (PositionedStack pStack : getInputs()) {
if (!this.renderPermutations.contains(pStack)) {
updatePermutationsFor(pStack);
}
drawItem(pStack, mouseX, mouseY, yShift, true);
}
for (PositionedStack pStack : getCatalysts()) {
if (!this.renderPermutations.contains(pStack)) {
updatePermutationsFor(pStack);
}
for (PositionedStack pStack : getCyclingStacks()) {
drawItem(pStack, mouseX, mouseY, yShift, true);
}
@@ -279,6 +373,10 @@ public class RecipeWidget extends Widget {
GL11.glTranslatef(-this.x, -this.y - yShift, 0);
GL11.glPopAttrib();
if (this.paused) {
NEIClientUtils.gl2DRenderContext(this::drawPauseMarker);
}
if (!this.showAsWidget) {
final Minecraft mc = NEIClientUtils.mc();
for (GuiRecipeButton button : getRecipeButtons()) {
@@ -293,7 +391,22 @@ public class RecipeWidget extends Widget {
DebugHandlerWidget.instance.drawGuiPlaceholder(this);
}
protected void drawItem(PositionedStack pStack, int mouseX, int mouseY, int yShift, boolean input) {
private void drawPauseMarker() {
GL11.glColor4f(1, 1, 1, 1);
PAUSE_MARKER.draw(this.x + PAUSE_MARKER_OFFSET, this.y + PAUSE_MARKER_OFFSET);
}
private boolean isPauseMarkerMouseOver(int mouseX, int mouseY) {
final int left = this.x + PAUSE_MARKER_OFFSET;
final int top = this.y + PAUSE_MARKER_OFFSET;
return this.paused && mouseX >= left
&& mouseX < left + PAUSE_MARKER.getWidth()
&& mouseY >= top
&& mouseY < top + PAUSE_MARKER.getHeight();
}
private void drawItem(PositionedStack pStack, int mouseX, int mouseY, int yShift, boolean input) {
pStack.draw(mouseX - this.x, mouseY - this.y - yShift);
if (this.handlerInfo.getShowBadge()) {
@@ -306,7 +419,7 @@ public class RecipeWidget extends Widget {
}
}
protected List<Badge> getBadges(PositionedStack pStack, boolean input) {
private List<Badge> getBadges(PositionedStack pStack, boolean input) {
return this.badgeCache.computeIfAbsent(pStack, k -> {
final List<Badge> badges = k.getBadges();
@@ -335,7 +448,7 @@ public class RecipeWidget extends Widget {
}
protected void drawBadge(PositionedStack pStack, boolean input) {
private void drawBadge(PositionedStack pStack, boolean input) {
final List<Badge> badges = getBadges(pStack, input);
for (Badge badge : badges) {
@@ -343,24 +456,6 @@ public class RecipeWidget extends Widget {
}
}
@Override
public boolean handleKeyPress(int keyID, char keyChar) {
if (NEIClientConfig.favoritesEnabled()
&& KeyManager.isHashDown("bookmark.favorite_item", NEIClientUtils.SHIFT_HASH)) {
final Point mouse = GuiDraw.getMousePosition();
final PositionedStack hovered = getCyclingStackMouseOver(mouse.x, mouse.y);
if (hovered != null && getPermutations(hovered).size() > 1) {
FavoriteRecipes.toggleFavoriteItem(hovered.item);
return true;
}
}
return false;
}
@Override
public void lastKeyTyped(int keyID, char keyChar) {
final Point mouse = GuiDraw.getMousePosition();
@@ -376,9 +471,36 @@ public class RecipeWidget extends Widget {
}
@Override
public boolean onHotkeyPressed() {
final Point mouse = GuiDraw.getMousePosition();
if (NEIClientConfig.favoritesEnabled()
&& KeyManager.isHashDown("bookmark.favorite_item", NEIClientUtils.SHIFT_HASH)) {
final PositionedStack hovered = getCyclingStackMouseOver(mouse.x, mouse.y);
if (hovered != null && getPermutations(hovered).size() > 1) {
FavoriteRecipes.toggleFavoriteItem(hovered.item);
setPaused(false);
return true;
}
}
return forEachButtons(
button -> button.contains(mouse.x, mouse.y) && button.onHotkeyPressed() ? true : null,
false);
}
@Override
public boolean handleClick(int mouseX, int mouseY, int button) {
if (button == 0 && isPauseMarkerMouseOver(mouseX, mouseY)) {
NEIClientUtils.playClickSound();
setPaused(false);
return true;
}
if (ShortcutInputHandler.handleMouseClick(getStackMouseOver(mouseX, mouseY))) {
return true;
}
@@ -406,7 +528,9 @@ public class RecipeWidget extends Widget {
return false;
}
return this.handler.mouseClicked(guiRecipe, button, getRecipeIndex());
try (GuiRecipe.CompatibilityHacks hack = new GuiRecipe.CompatibilityHacks(guiRecipe, this)) {
return this.handler.mouseClicked(guiRecipe, button, getRecipeIndex());
}
}
@Override
@@ -417,6 +541,11 @@ public class RecipeWidget extends Widget {
return tooltip;
}
if (isPauseMarkerMouseOver(mouseX, mouseY)) {
tooltip.add(NEIClientUtils.translate("recipe.pinned"));
return tooltip;
}
final List<String> buttonTooltip = forEachButtons(
button -> button.contains(mouseX, mouseY) ? button.handleTooltip(tooltip) : null,
null);
@@ -425,7 +554,9 @@ public class RecipeWidget extends Widget {
return buttonTooltip;
}
return this.handler.handleTooltip(guiRecipe, tooltip, getRecipeIndex());
try (GuiRecipe.CompatibilityHacks hack = new GuiRecipe.CompatibilityHacks(guiRecipe, this)) {
return this.handler.handleTooltip(guiRecipe, tooltip, getRecipeIndex());
}
}
@Override
@@ -477,30 +608,6 @@ public class RecipeWidget extends Widget {
tooltip.add(GuiDraw.TOOLTIP_HANDLER + GuiDraw.getTipLineId(this.acceptsTooltip));
}
final Point outputKey = cycling == null && hovered != null && this.group.size() > 1
? this.group.getSlotKey(hovered)
: null;
if (outputKey == null || !NEIClientConfig.showCycledIngredientsTooltip()) {
this.variantsTooltip = null;
this.variantsSlotKey = null;
} else if (!outputKey.equals(this.variantsSlotKey)) {
final List<ItemStack> variants = this.group.getOutputVariants(outputKey);
this.variantsSlotKey = outputKey;
this.variantsTooltip = variants.size() > 1
? new ItemsTooltipLineHandler(NEIClientUtils.translate("recipe.group"), variants, false, 4)
: null;
if (this.variantsTooltip != null) {
this.variantsTooltip.setActiveStack(hovered.item);
}
}
if (this.variantsTooltip != null) {
tooltip.add(GuiDraw.TOOLTIP_HANDLER + GuiDraw.getTipLineId(this.variantsTooltip));
}
return tooltip;
}
@@ -559,7 +666,7 @@ public class RecipeWidget extends Widget {
return guiRecipe != null && this.handler.mouseScrolled(guiRecipe, scroll, getRecipeIndex());
}
protected boolean scrollPermutations(int scroll, PositionedStack hovered) {
private boolean scrollPermutations(int scroll, PositionedStack hovered) {
final Point slotKey = this.group.getSlotKey(hovered);
final List<ItemStack> items = this.group.getPermutations(slotKey);
@@ -567,46 +674,23 @@ public class RecipeWidget extends Widget {
return false;
}
final List<PositionedStack> slots = new ArrayList<>(this.group.size());
final int current = Math.max(0, PermutationsCycler.indexOf(items, hovered.item));
final int direction = scroll > 0 ? -1 : 1;
for (int member = 0; member < this.group.size(); member++) {
slots.add(this.group.getSlot(member, slotKey));
}
final int anchor = getCursorMember();
final int currentItem = PermutationsCycler.indexOf(items, hovered.item);
final List<int[]> variants = new ArrayList<>();
int current = 0;
for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
final ItemStack stack = items.get(itemIndex);
int member = containsStack(slots.get(this.activeMember), stack) ? this.activeMember : -1;
for (int offset = 0; member == -1 && offset < slots.size(); offset++) {
if (containsStack(slots.get((anchor + offset) % slots.size()), stack)) {
member = (anchor + offset) % slots.size();
}
}
for (int step = 1; step <= items.size(); step++) {
final ItemStack stack = items.get(Math.floorMod(current + direction * step, items.size()));
final int member = this.group.findMember(slotKey, stack, this.activeMember);
if (member != -1) {
if (itemIndex == currentItem) {
current = variants.size();
}
variants.add(new int[] { itemIndex, member });
switchMember(member, stack);
return true;
}
}
if (variants.isEmpty()) {
return false;
}
final int[] next = variants.get(((current - scroll) % variants.size() + variants.size()) % variants.size());
switchMember(next[1], items.get(next[0]));
return true;
return false;
}
protected boolean scrollMembers(int scroll) {
private boolean scrollMembers(int scroll) {
final int size = this.group.size();
if (size <= 1) {
@@ -618,14 +702,19 @@ public class RecipeWidget extends Widget {
return true;
}
protected void switchMember(int member, ItemStack stack) {
private void switchMember(int member, ItemStack stack) {
final Map<Point, ItemStack> rendered = new HashMap<>();
setPaused(false);
for (PositionedStack pStack : getCyclingStacks()) {
rendered.put(this.group.getSlotKey(pStack), pStack.item);
}
setActiveMember(member);
syncCycle();
this.group.refresh(member);
this.badgeCache.clear();
for (PositionedStack pStack : getCyclingStacks()) {
final ItemStack renderStack = containsStack(pStack, stack) ? stack
@@ -633,6 +722,8 @@ public class RecipeWidget extends Widget {
if (containsStack(pStack, renderStack)) {
pStack.setPermutationToRender(renderStack);
} else {
updatePermutationsFor(pStack);
}
}
@@ -640,10 +731,9 @@ public class RecipeWidget extends Widget {
notifyPermutationsChanged();
}
protected boolean tickCycle() {
private boolean tickCycle() {
if (!NEIClientUtils.shiftKey() && (this.cycleticks++ / TICKS_PER_CYCLE) != this.lastcycle
|| this.lastcycle == -1) {
if ((this.cycleticks++ / TICKS_PER_CYCLE) != this.lastcycle) {
this.lastcycle = this.cycleticks / TICKS_PER_CYCLE;
return true;
}
@@ -651,20 +741,46 @@ public class RecipeWidget extends Widget {
return false;
}
protected void nextMember() {
final List<Integer> allowed = this.group.getAllowedMembers();
private boolean isCycleFrozen(int mouseX, int mouseY) {
this.memberCursor = (this.memberCursor + 1) % allowed.size();
setActiveMember(allowed.get(this.memberCursor));
if (this.paused || NEIClientUtils.shiftKey()) {
return true;
}
return NEIClientConfig.freezeCycleOnHover() && !this.showAsWidget
&& contains(mouseX, mouseY)
&& (getPositionedStackMouseOver(mouseX, mouseY) != null
|| forEachButtons(button -> button.contains(mouseX, mouseY) ? true : null, false));
}
protected int getCursorMember() {
final List<Integer> allowed = this.group.getAllowedMembers();
return allowed.get(Math.max(0, this.memberCursor) % allowed.size());
private void applyCycle() {
if (!this.paused) {
final List<Integer> allowed = this.group.getAllowedMembers();
setActiveMember(allowed.get(this.cycle % allowed.size()));
}
this.group.refresh(this.activeMember);
updatePermutations();
}
protected void updatePermutations(int cycle) {
this.slotCycle = cycle;
private int getPermutationIndex() {
return this.cycle / this.group.getAllowedMembers().size();
}
// keep the permutation index, move the member part of the counter to the active member
private void syncCycle() {
final List<Integer> allowed = this.group.getAllowedMembers();
final int allowedIndex = allowed.indexOf(this.activeMember);
if (allowedIndex >= 0) {
this.cycle = this.cycle / allowed.size() * allowed.size() + allowedIndex;
}
}
private void updatePermutations() {
// badges depend on the rendered item
this.badgeCache.clear();
for (PositionedStack pStack : getCyclingStacks()) {
updatePermutationsFor(pStack);
@@ -674,7 +790,7 @@ public class RecipeWidget extends Widget {
notifyPermutationsChanged();
}
protected void updateTooltipActiveStack() {
private void updateTooltipActiveStack() {
if (this.acceptsTooltip != null && this.acceptsTooltip.tooltipGUID instanceof Point slotKey) {
final PositionedStack pStack = this.group.getSlot(this.activeMember, slotKey);
@@ -684,29 +800,20 @@ public class RecipeWidget extends Widget {
}
}
if (this.variantsTooltip != null) {
final PositionedStack pStack = this.group.getOutputSlot(this.activeMember, this.variantsSlotKey);
if (pStack != null) {
this.variantsTooltip.setActiveStack(pStack.item);
}
}
}
protected void updatePermutationsFor(PositionedStack pStack) {
private void updatePermutationsFor(PositionedStack pStack) {
final int index = this.group
.getCycledPermutationIndex(this.activeMember, this.group.getSlotKey(pStack), getPermutationIndex());
if (!this.renderPermutations.contains(pStack) || this.renderPermutations.get(pStack).size() > 1) {
final ItemStack stack = this.renderPermutations.getCycled(pStack, this.slotCycle);
if (stack != null) {
pStack.setPermutationToRender(stack);
}
if (index >= 0) {
pStack.setPermutationToRender(index);
}
applyPinnedIngredient(pStack);
}
protected void setActiveMember(int member) {
private void setActiveMember(int member) {
if (member != this.activeMember) {
this.activeMember = member;
@@ -718,22 +825,21 @@ public class RecipeWidget extends Widget {
}
protected boolean containsStack(PositionedStack pStack, ItemStack stack) {
return pStack != null && stack != null
&& PermutationsCycler.indexOf(pStack.getFilteredPermutations(), stack) != -1;
private boolean containsStack(PositionedStack pStack, ItemStack stack) {
return pStack != null && this.group.hasPermutation(this.activeMember, this.group.getSlotKey(pStack), stack);
}
protected List<ItemStack> getPermutations(PositionedStack pStack) {
private List<ItemStack> getPermutations(PositionedStack pStack) {
return this.group.getPermutations(this.group.getSlotKey(pStack));
}
protected void notifyPermutationsChanged() {
private void notifyPermutationsChanged() {
for (GuiRecipeButton button : getRecipeButtonsIfInit()) {
button.onPermutationsChanged();
}
}
protected <R> R forEachButtons(Function<GuiRecipeButton, R> callback, R defaultValue) {
private <R> R forEachButtons(Function<GuiRecipeButton, R> callback, R defaultValue) {
for (GuiRecipeButton button : getRecipeButtonsIfInit()) {
button.xPosition += this.x;
@@ -769,7 +875,7 @@ public class RecipeWidget extends Widget {
return cycling != null ? cycling : getOutputStackMouseOver(mx, my);
}
protected PositionedStack getOutputStackMouseOver(int mx, int my) {
private PositionedStack getOutputStackMouseOver(int mx, int my) {
final int yShift = this.handlerInfo.getYShift();
for (PositionedStack pStack : getOutputs()) {
@@ -781,16 +887,10 @@ public class RecipeWidget extends Widget {
return null;
}
protected PositionedStack getCyclingStackMouseOver(int mx, int my) {
private PositionedStack getCyclingStackMouseOver(int mx, int my) {
final int yShift = this.handlerInfo.getYShift();
for (PositionedStack pStack : getInputs()) {
if (pStack.contains(mx - this.x, my - this.y - yShift)) {
return pStack;
}
}
for (PositionedStack pStack : getCatalysts()) {
for (PositionedStack pStack : getCyclingStacks()) {
if (pStack.contains(mx - this.x, my - this.y - yShift)) {
return pStack;
}
@@ -805,7 +905,7 @@ public class RecipeWidget extends Widget {
return pStack != null ? pStack.item : null;
}
protected GuiRecipe<?> getGuiRecipe() {
private GuiRecipe<?> getGuiRecipe() {
final GuiContainer guiContainer = NEIClientUtils.getGuiContainer();
if (guiContainer instanceof GuiRecipe<?>guiRecipe) {
@@ -815,18 +915,10 @@ public class RecipeWidget extends Widget {
return null;
}
protected List<PositionedStack> getInputs() {
return this.group.getInputs(this.activeMember);
}
protected List<PositionedStack> getOutputs() {
return this.group.getOutputs(this.activeMember);
}
protected List<PositionedStack> getCatalysts() {
return this.group.getCatalysts(this.activeMember);
}
protected List<PositionedStack> getCyclingStacks() {
return this.group.getCyclingStacks(this.activeMember);
}
@@ -16,6 +16,7 @@ import cpw.mods.fml.relauncher.SideOnly;
public class GuiHelper {
private static final Stack<int[]> scissorFrameStack = new Stack<>();
private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer(16);
public static void useScissor(int x, int y, int width, int height, Runnable codeBlock) {
pushScissorFrame(x, y, width, height);
@@ -36,7 +37,7 @@ public class GuiHelper {
return currentTopFrame;
}
private static void pushScissorFrame(int x, int y, int width, int height) {
public static void pushScissorFrame(int x, int y, int width, int height) {
int[] matrix = prepareMatrix(x, y, width, height);
int[] parentScissor = peekFirstScissorOrFullScreen();
int parentX = parentScissor[0];
@@ -110,7 +111,7 @@ public class GuiHelper {
private static int[] prepareMatrix(int x, int y, int w, int h) {
// Get the current translation component to support the Gui being drawn at any position on the screen
final FloatBuffer matBuf = BufferUtils.createFloatBuffer(16);
final FloatBuffer matBuf = MATRIX_BUFFER;
GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, matBuf);
final float xScale = matBuf.get(0);
final float yScale = matBuf.get(5);
@@ -53,28 +53,26 @@ public class ScrollBar {
private int trackPaddingBottom = 0;
private DrawableResource thumbTexture;
private int thumbSliceStart;
private int thumbSliceEnd;
private DrawableResource trackTexture;
private int trackSliceStart;
private int trackSliceEnd;
public static ScrollBar defaultScrollBar() {
return new ScrollBar();
}
public static ScrollBar defaultVerticalBar() {
return new ScrollBar().setTrackWidth(8)
.setTrackTexture(new DrawableBuilder("nei:textures/nei_sprites.png", 29, 95, 8, 11).build(), 3, 4)
.setThumbTexture(new DrawableBuilder("nei:textures/nei_sprites.png", 22, 96, 6, 9).build(), 3, 2)
return new ScrollBar().setTrackWidth(8).setTrackTexture(
new DrawableBuilder("nei:textures/nei_sprites.png", 29, 95, 8, 11).setSlices(0, 0, 3, 4).build())
.setThumbTexture(
new DrawableBuilder("nei:textures/nei_sprites.png", 22, 96, 6, 9).setSlices(0, 0, 3, 2).build())
.setThumbPadding(1, 1, 1, 1);
}
public static ScrollBar defaultHorizontalBar() {
return new ScrollBar().setTrackWidth(8).setOverflowType(OverflowType.SCROLL)
.setTrackTexture(new DrawableBuilder("nei:textures/nei_sprites.png", 0, 97, 11, 8).build(), 3, 4)
.setThumbTexture(new DrawableBuilder("nei:textures/nei_sprites.png", 12, 98, 9, 6).build(), 3, 2)
.setTrackTexture(
new DrawableBuilder("nei:textures/nei_sprites.png", 0, 97, 11, 8).setSlices(3, 4, 0, 0).build())
.setThumbTexture(
new DrawableBuilder("nei:textures/nei_sprites.png", 12, 98, 9, 6).setSlices(3, 2, 0, 0).build())
.setThumbPadding(1, 1, 1, 1);
}
@@ -126,17 +124,13 @@ public class ScrollBar {
return getTrackWidth();
}
public ScrollBar setThumbTexture(DrawableResource texture, int sliceStart, int sliceEnd) {
public ScrollBar setThumbTexture(DrawableResource texture) {
this.thumbTexture = texture;
this.thumbSliceStart = sliceStart;
this.thumbSliceEnd = sliceEnd;
return this;
}
public ScrollBar setTrackTexture(DrawableResource texture, int sliceStart, int sliceEnd) {
public ScrollBar setTrackTexture(DrawableResource texture) {
this.trackTexture = texture;
this.trackSliceStart = sliceStart;
this.trackSliceEnd = sliceEnd;
return this;
}
@@ -379,13 +373,13 @@ public class ScrollBar {
final boolean disabled = getVisibleSize(container) >= getActualSize(container);
if (this.trackTexture != null) {
drawTexture(this.trackTexture, this.trackSliceStart, this.trackSliceEnd, trackBounds, false, opacity);
drawTexture(this.trackTexture, trackBounds, false, opacity);
} else {
drawTrackRect(container, trackBounds, opacity);
}
if (this.thumbTexture != null) {
drawTexture(this.thumbTexture, this.thumbSliceStart, this.thumbSliceEnd, thumbBounds, disabled, opacity);
drawTexture(this.thumbTexture, thumbBounds, disabled, opacity);
} else {
drawThumbRect(container, thumbBounds, selected, opacity);
}
@@ -417,8 +411,7 @@ public class ScrollBar {
NEIClientUtils.drawRect(bounds.x, bounds.y, bounds.w, bounds.h, thumbColor);
}
private void drawTexture(DrawableResource texture, int sliceStart, int sliceEnd, Rectangle4i bounds,
boolean disabled, float opacity) {
private void drawTexture(DrawableResource texture, Rectangle4i bounds, boolean disabled, float opacity) {
final float c = disabled ? 0.65f : 1f;
GL11.glColor4f(c, c, c, opacity);
@@ -426,9 +419,9 @@ public class ScrollBar {
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
if (this.scrollType == ScrollType.HORIZONTAL) {
texture.draw(bounds.x, bounds.y, bounds.w, texture.getHeight(), sliceStart, sliceEnd, 0, 0);
texture.draw(bounds.x, bounds.y, bounds.w, texture.getHeight());
} else {
texture.draw(bounds.x, bounds.y, texture.getWidth(), bounds.h, 0, 0, sliceStart, sliceEnd);
texture.draw(bounds.x, bounds.y, texture.getWidth(), bounds.h);
}
}
@@ -7,6 +7,7 @@ import java.util.Map;
import java.util.function.Predicate;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.GL11;
import codechicken.lib.gui.GuiDraw;
@@ -249,6 +250,8 @@ public class ScrollContainer extends WidgetContainer {
final boolean canScrollHorizontal = canScrollHorizontal();
if (canScrollVertical || canScrollHorizontal) {
dragScrollBars(mousex, mousey, canScrollVertical, canScrollHorizontal);
GuiHelper.useScissor(
this.x + this.paddingInlineStart,
this.y + this.paddingBlockStart,
@@ -273,6 +276,20 @@ public class ScrollContainer extends WidgetContainer {
}
private void dragScrollBars(int mousex, int mousey, boolean canScrollVertical, boolean canScrollHorizontal) {
if (!Mouse.isButtonDown(0)) {
return;
}
if (canScrollVertical) {
this.verticalScrollBar.onMouseDragged(this, mousex, mousey, 0, 0);
}
if (canScrollHorizontal) {
this.horizontalScrollBar.onMouseDragged(this, mousex, mousey, 0, 0);
}
}
protected void drawContent(int mousex, int mousey) {
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
GL11.glPushMatrix();
@@ -14,6 +14,9 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraftforge.oredict.OreDictionary;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import codechicken.nei.ItemList.AllMultiItemFilter;
import codechicken.nei.ItemList.AnyMultiItemFilter;
import codechicken.nei.ItemList.NegatedItemFilter;
@@ -197,9 +200,9 @@ public class ItemStackFilterParser {
}
private static ItemFilter getStrictIdentifierFilter(String rule) {
ItemStack item = NEIServerUtils.getModdedItem(rule, null);
Supplier<ItemStack> item = Suppliers.memoize(() -> NEIServerUtils.getModdedItem(rule, null));
return stack -> OreDictionary.itemMatches(item, stack, false);
return stack -> OreDictionary.itemMatches(item.get(), stack, false);
}
protected static ItemFilter getStringIdentifierFilter(String rule) {
@@ -56,18 +56,24 @@ public class NEIKeyboardUtils {
return keyText.toString();
}
public static String getKeyName(int keyBind) {
keyBind = keyHashMap.getOrDefault(keyBind, keyBind);
StringJoiner keyText = new StringJoiner(" + ");
String hashText = getHashName(keyBind);
int keyID = unhash(keyBind);
/**
* Mouse buttons are negative key codes, so the hash can not be packed into them and is passed separately.
*/
public static String getKeyName(int keyCode, int hash) {
if (hash == 0 && keyHashMap.containsKey(keyCode)) {
hash = keyHashMap.get(keyCode);
keyCode = Keyboard.CHAR_NONE;
}
final StringJoiner keyText = new StringJoiner(" + ");
final String hashText = getHashName(hash);
if (!hashText.isEmpty()) {
keyText.add(hashText);
}
if (keyID != Keyboard.CHAR_NONE || hashText.isEmpty()) {
keyText.add(keyID < 0 ? NEIMouseUtils.getKeyName(keyID + 100) : Keyboard.getKeyName(keyID));
if (keyCode != Keyboard.CHAR_NONE || hashText.isEmpty()) {
keyText.add(keyCode < 0 ? NEIMouseUtils.getKeyName(keyCode + 100) : Keyboard.getKeyName(keyCode));
}
return keyText.toString();
@@ -121,6 +121,12 @@ RETURN
list m_handleMouseInput
ALOAD 0
GETFIELD net/minecraft/client/gui/inventory/GuiContainer.manager : Lcodechicken/nei/guihook/GuiContainerManager;
INVOKEVIRTUAL codechicken/nei/guihook/GuiContainerManager.handleMouseHotkey ()Z
IFEQ LCONT
RETURN
LCONT
ALOAD 0
INVOKESPECIAL net/minecraft/client/gui/GuiScreen.func_146274_d ()V
ALOAD 0
GETFIELD net/minecraft/client/gui/inventory/GuiContainer.manager : Lcodechicken/nei/guihook/GuiContainerManager;
+19 -1
View File
@@ -48,6 +48,7 @@ nei.bookmark.recipe.toggle_collapse=Toggle Collapse/Expand
nei.bookmark.recipe.chat_link=Send Recipe to Chat
nei.bookmark.crafting_chain=Crafting Chain
nei.bookmark.crafting_chain.input=Ingredients
nei.bookmark.crafting_chain.missing=Missing Items
nei.bookmark.crafting_chain.needed=Required Crafts
nei.bookmark.tree.title=Crafting Tree
nei.bookmark.tree.empty=No crafting chain to display
@@ -289,6 +290,7 @@ nei.options.inventory.bookmarks.showRecipeMarkerMode=Show Recipe Marker
nei.options.inventory.bookmarks.showRecipeMarkerMode.0=None
nei.options.inventory.bookmarks.showRecipeMarkerMode.1=Text
nei.options.inventory.bookmarks.showRecipeMarkerMode.2=Background
nei.options.inventory.bookmarks.maxVerticalTooltipRows=Max Vertical Tooltip Rows (0 - disable)
nei.options.inventory.bookmarks.showRecipeHandlerIcon=Show Handler Icon
nei.options.inventory.bookmarks.showRecipeHandlerIcon.true=Yes
nei.options.inventory.bookmarks.showRecipeHandlerIcon.false=No
@@ -306,6 +308,9 @@ nei.options.inventory.bookmarks.bookmarkItemsWithRecipe.false=No
nei.options.inventory.bookmarks.bookmarkRecipeWithCount=Bookmark recipe with count
nei.options.inventory.bookmarks.bookmarkRecipeWithCount.true=Yes
nei.options.inventory.bookmarks.bookmarkRecipeWithCount.false=No
nei.options.inventory.bookmarks.craftingTreeZoomWithoutModifier=Tree Zoom without Ctrl
nei.options.inventory.bookmarks.craftingTreeZoomWithoutModifier.true=Yes
nei.options.inventory.bookmarks.craftingTreeZoomWithoutModifier.false=No
nei.options.inventory.favorites=Favorites
nei.options.inventory.favorites.enabled=Visibility
nei.options.inventory.favorites.enabled.true=Visible
@@ -424,6 +429,9 @@ nei.options.inventory.autocrafting.false=Disabled
nei.options.inventory.guirecipe.shiftOverlayRecipe=Require holding shift to move items when overlaying recipe
nei.options.inventory.guirecipe.shiftOverlayRecipe.true=Yes
nei.options.inventory.guirecipe.shiftOverlayRecipe.false=No
nei.options.inventory.guirecipe.freezeCycleOnHover=Stop cycling the recipe while hovering over an ingredient or a button
nei.options.inventory.guirecipe.freezeCycleOnHover.true=Yes
nei.options.inventory.guirecipe.freezeCycleOnHover.false=No
nei.options.inventory.widgetsenabled.true=NEI Enabled
nei.options.inventory.widgetsenabled.false=NEI Disabled
nei.options.inventory.cheatmode.0=Recipe Mode
@@ -513,6 +521,13 @@ nei.options.inventory.itemzoom.neiOnly.tip=Zoom items only from the JEI ingredie
nei.options.inventory.itemzoom.onlySolid=Show Only Solid Items
nei.options.inventory.itemzoom.onlySolid.true=Enabled
nei.options.inventory.itemzoom.onlySolid.false=Disabled
nei.options.inventory.itemzoom.preferredSide=Preferred Side
nei.options.inventory.itemzoom.preferredSide.0=Auto
nei.options.inventory.itemzoom.preferredSide.1=Left
nei.options.inventory.itemzoom.preferredSide.2=Right
nei.options.inventory.itemzoom.preferredSide.0.tip=Prefer whichever side currently has fewer items in its panel.
nei.options.inventory.itemzoom.preferredSide.1.tip=Always prefer the left side, unless the cursor is there.
nei.options.inventory.itemzoom.preferredSide.2.tip=Always prefer the right side, unless the cursor is there.
nei.options.inventory.itemzoom.helpText=Display Help Text
nei.options.inventory.itemzoom.helpText.true=Enabled
nei.options.inventory.itemzoom.helpText.false=Disabled
@@ -614,6 +629,7 @@ nei.debug.RecipeHandler.Width=Width
nei.debug.RecipeHandler.ShowFavorites=Show Favorites
nei.debug.RecipeHandler.ShowOverlay=Show Overlays
nei.debug.RecipeHandler.ShowBadge=Show Badge
nei.debug.RecipeHandler.DuplicatesOnly=Duplicates Only
nei.debug.RecipeHandler.AllowOverflowY=Allow Overflow Y
nei.debug.RecipeHandler.AllowOverflowX=Allow Overflow X
nei.debug.RecipeHandler.Override=Override
@@ -622,6 +638,7 @@ nei.debug.RecipeHandler.checkbox.off=Off
nei.debug.RecipeHandler.yes=Yes
nei.debug.RecipeHandler.no=No
nei.debug.RecipeHandler.reset=Reset
nei.debug.RecipeHandler.RegisteredBy=Registered by: %s
nei.recipe.page=Page %s/%s
nei.recipe.tooltip=Recipes
@@ -682,14 +699,15 @@ nei.recipe.concurrent.usage=Concurrent Usage - Total
nei.recipe.concurrent.crafting=Concurrent Crafting - Total
nei.recipe.accepts=Accepts following
nei.recipe.accepts.scroll=Change Item
nei.recipe.group=Variants
nei.recipe.group.scroll=Change Variant (%s/%s)
nei.recipe.pinned=Click to resume cycling
nei.recipe.overlay=Recipe Overlay
nei.recipe.overlay.missing=Missing following
nei.recipe.overlay.mismatch=Mismatch Crafting Grid
nei.recipe.overlay.overlayRecipe=Overlay Recipe
nei.recipe.overlay.fillCraftingGrid=Fill Crafting Grid
nei.recipe.overlay.fillCraftingGridOnce=Fill Crafting Grid (single recipe)
nei.recipe.overlay.bookmarkRecipe=Save with Recipe
nei.recipe.overlay.bookmarkRecipeAndCount=Save with Recipe & Count
nei.recipe.overlay.sendRecipeToChat=Send Recipe to Chat
@@ -197,6 +197,9 @@ nei.options.inventory.bookmarks.ignorePotionOverlap.false=Нет
nei.options.inventory.bookmarks.worldSpecific=Закладки для конкретного мира
nei.options.inventory.bookmarks.worldSpecific.true=Да
nei.options.inventory.bookmarks.worldSpecific.false=Нет
nei.options.inventory.bookmarks.craftingTreeZoomWithoutModifier=Масштаб дерева без Ctrl
nei.options.inventory.bookmarks.craftingTreeZoomWithoutModifier.true=Да
nei.options.inventory.bookmarks.craftingTreeZoomWithoutModifier.false=Нет
nei.options.inventory.guirecipe=Интерфейс рецептов
nei.options.inventory.guirecipe.jeiStyleTabs=Вкладки в стиле JEI
nei.options.inventory.guirecipe.jeiStyleTabs.true=Да
@@ -262,6 +265,9 @@ nei.options.inventory.optimize_gui_overlap_computation.false=Нет
nei.options.inventory.guirecipe.shiftOverlayRecipe=Требовать удержание Shift для перемещения предметов при отображении рецепта
nei.options.inventory.guirecipe.shiftOverlayRecipe.true=Да
nei.options.inventory.guirecipe.shiftOverlayRecipe.false=Нет
nei.options.inventory.guirecipe.freezeCycleOnHover=Останавливать перелистывание рецепта при наведении на предмет или кнопку
nei.options.inventory.guirecipe.freezeCycleOnHover.true=Да
nei.options.inventory.guirecipe.freezeCycleOnHover.false=Нет
nei.options.inventory.widgetsenabled.true=NEI включен
nei.options.inventory.widgetsenabled.false=NEI выключен
nei.options.inventory.cheatmode.0=Режим рецептов
@@ -329,6 +335,13 @@ nei.options.inventory.itemzoom.neiOnly.tip=Увеличивать предмет
nei.options.inventory.itemzoom.onlySolid=Показывать только твердые предметы
nei.options.inventory.itemzoom.onlySolid.true=Включено
nei.options.inventory.itemzoom.onlySolid.false=Отключено
nei.options.inventory.itemzoom.preferredSide=Предпочитаемая сторона
nei.options.inventory.itemzoom.preferredSide.0=Авто
nei.options.inventory.itemzoom.preferredSide.1=Слева
nei.options.inventory.itemzoom.preferredSide.2=Справа
nei.options.inventory.itemzoom.preferredSide.0.tip=Выбирать сторону, где сейчас меньше предметов в панели.
nei.options.inventory.itemzoom.preferredSide.1.tip=Всегда предпочитать левую сторону, если там нет курсора.
nei.options.inventory.itemzoom.preferredSide.2.tip=Всегда предпочитать правую сторону, если там нет курсора.
nei.options.inventory.itemzoom.helpText=Показывать текст помощи
nei.options.inventory.itemzoom.helpText.true=Включено
nei.options.inventory.itemzoom.helpText.false=Отключено
@@ -419,11 +432,13 @@ nei.recipe.profiler.usage=Профилирование использовани
nei.recipe.concurrent.usage=Одновременное использование - всего
nei.recipe.concurrent.crafting=Одновременный крафт - всего
nei.recipe.accepts=Принимает следующее
nei.recipe.pinned=Нажмите, чтобы продолжить перебор
nei.recipe.overlay=отображение рецепта
nei.recipe.overlay.missing=Отсутствует следующее
nei.recipe.overlay.mismatch=Несоответствие сетки крафта
nei.recipe.overlay.fillCraftingGrid=Заполнить сетку крафта
nei.recipe.overlay.fillCraftingGridOnce=Заполнить сетку крафта (один рецепт)
nei.recipe.overlay.bookmarkRecipe=Сохранить с рецептом
nei.recipe.overlay.sendRecipeToChat=Отправить рецепт в чат
Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

@@ -0,0 +1,134 @@
package codechicken.nei;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import codechicken.nei.api.IConfigureNEI;
class PluginLoaderTest {
@ParameterizedTest
@ValueSource(booleans = { false, true })
void loadsPluginsAndLogsFailuresWithTheGameClassLoader(boolean parallel) throws Exception {
ClassLoader workerClassLoader = new ClassLoader(null) {};
Set<Thread> workers = ConcurrentHashMap.newKeySet();
ForkJoinPool pool = new ForkJoinPool(2, owner -> {
ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(owner);
worker.setContextClassLoader(workerClassLoader);
workers.add(worker);
return worker;
}, null, false);
Logger logger = mock(Logger.class);
doAnswer(invocation -> {
assertSame(IConfigureNEI.class.getClassLoader(), Thread.currentThread().getContextClassLoader());
return null;
}).when(logger).error(eq("Failed to Load {}"), anyString(), any(Throwable.class));
List<Class<?>> plugins = new ArrayList<>();
for (int i = 0; i < 16; i++) {
plugins.add(HealthyPlugin.class);
plugins.add(ConstructorFailurePlugin.class);
plugins.add(ConfigurationFailurePlugin.class);
}
List<IConfigureNEI> loaded = new CopyOnWriteArrayList<>();
try {
pool.submit(() -> PluginLoader.load(parallel ? plugins.parallelStream() : plugins.stream(), loaded, logger))
.get(10, TimeUnit.SECONDS);
assertEquals(16, loaded.size());
verify(logger, times(32)).error(eq("Failed to Load {}"), anyString(), any(Throwable.class));
workers.forEach(worker -> assertSame(workerClassLoader, worker.getContextClassLoader()));
} finally {
pool.shutdownNow();
}
}
@Test
void restoresTheClassLoaderEvenIfErrorLoggingThrows() {
Thread thread = Thread.currentThread();
ClassLoader previousClassLoader = thread.getContextClassLoader();
ClassLoader workerClassLoader = new ClassLoader(null) {};
Logger logger = mock(Logger.class);
doThrow(new LinkageError("Logging failed")).when(logger)
.error(eq("Failed to Load {}"), anyString(), any(Throwable.class));
try {
thread.setContextClassLoader(workerClassLoader);
assertThrows(
LinkageError.class,
() -> PluginLoader.load(Stream.of(ConfigurationFailurePlugin.class), new ArrayList<>(), logger));
assertSame(workerClassLoader, thread.getContextClassLoader());
} finally {
thread.setContextClassLoader(previousClassLoader);
}
}
public static class HealthyPlugin implements IConfigureNEI {
public HealthyPlugin() {
checkClassLoader();
}
@Override
public void loadConfig() {
checkClassLoader();
}
private static void checkClassLoader() {
if (Thread.currentThread().getContextClassLoader() != IConfigureNEI.class.getClassLoader()) {
throw new IllegalStateException("Plugin cannot see the game classloader");
}
}
@Override
public String getName() {
return "Test plugin";
}
@Override
public String getVersion() {
return "1";
}
}
public static class ConstructorFailurePlugin extends HealthyPlugin {
public ConstructorFailurePlugin() {
throw new IllegalStateException("Constructor failed");
}
}
public static class ConfigurationFailurePlugin extends HealthyPlugin {
@Override
public void loadConfig() {
super.loadConfig();
throw new IllegalStateException("Configuration failed");
}
}
}
@@ -64,6 +64,7 @@ public class SearchTokenParserTest {
new ModeValue(1, "Extended mode"), new ModeValue(2, "Regex mode"), new ModeValue(3, "Extended+ mode") };
private static final ModeValue[] extendedPlusPatternMode = { patternModes[3] };
private static final ModeValue[] extendedPatternModes = { patternModes[1], patternModes[3] };
private static final ModeValue[] nonGrammarPatternModes = { patternModes[0], patternModes[1], patternModes[2] };
private static final SearchParserProvider defaultParserProvider = parserProvider(
'\0',
@@ -119,6 +120,10 @@ public class SearchTokenParserTest {
private static final List<Arguments> allModesTestProvider = combinationsProvider(spaceModes, patternModes);
private static final List<Arguments> nonGrammarAllSpaceModesTestProvider = combinationsProvider(
spaceModes,
nonGrammarPatternModes);
private static final List<Arguments> extendedPlusAllSpaceModesTestProvider = combinationsProvider(
spaceModes,
extendedPlusPatternMode);
@@ -236,6 +241,36 @@ public class SearchTokenParserTest {
assertTrue(matchedItems.contains(itemA));
}
@DisplayName("Negated prefix (dash)")
@ParameterizedTest(name = "{argumentsWithNames}")
@FieldSource("allModesTestProvider")
public void testNegatedPrefixDash(int spaceMode, int patternMode) {
SearchTokenParser searchParser = new SearchTokenParser();
searchParser.addProvider(defaultParserProvider);
searchParser
.addProvider(parserProvider('?', "custom", SearchTokenParser.SearchMode.PREFIX, customCreateFilter));
Set<ItemStack> matchedItems = testFilterAgainstAllItems(spaceMode, patternMode, searchParser, "-?A");
assertEquals(5, matchedItems.size());
assertFalse(matchedItems.contains(itemA));
}
@DisplayName("Negated prefix (bang)")
@ParameterizedTest(name = "{argumentsWithNames}")
@FieldSource("nonGrammarAllSpaceModesTestProvider")
public void testNegatedPrefixBang(int spaceMode, int patternMode) {
SearchTokenParser searchParser = new SearchTokenParser();
searchParser.addProvider(defaultParserProvider);
searchParser
.addProvider(parserProvider('?', "custom", SearchTokenParser.SearchMode.PREFIX, customCreateFilter));
Set<ItemStack> matchedItems = testFilterAgainstAllItems(spaceMode, patternMode, searchParser, "!?A");
assertEquals(5, matchedItems.size());
assertFalse(matchedItems.contains(itemA));
}
@DisplayName("Space mode")
@ParameterizedTest(name = "{argumentsWithNames}")
@FieldSource("spaceIsSpaceAllModesTestProvider")