Compare commits

...
3 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
29 changed files with 489 additions and 99 deletions

No files matched your search

@@ -219,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")
+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;
@@ -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;
@@ -1030,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();
@@ -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();
@@ -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
@@ -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;
@@ -58,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;
@@ -77,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 {
@@ -911,6 +912,12 @@ public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGu
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());
@@ -918,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) {
@@ -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
@@ -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;
}
}
@@ -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
@@ -346,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() {
@@ -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;
@@ -697,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;
}
@@ -721,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();
@@ -731,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
@@ -64,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() {
@@ -29,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));
}
@@ -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,7 +21,6 @@ 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;
@@ -39,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;
@@ -285,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);
}
@@ -332,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,
@@ -512,6 +512,11 @@ public class DebugHandlerWidget extends Widget implements IContainerTooltipHandl
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) {
@@ -456,25 +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);
setPaused(false);
return true;
}
}
return false;
}
@Override
public void lastKeyTyped(int keyID, char keyChar) {
final Point mouse = GuiDraw.getMousePosition();
@@ -490,6 +471,27 @@ 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) {
@@ -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;
@@ -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
@@ -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");
}
}
}