Fix Old Handlers Hack (#1061)

This commit is contained in:
slprime authored and GitHub committed 2026-10-04 22:07:41 +03:00
1 parent c020ecbf1f
commit 2844ea35bc
2 files changed
+107 -70

No files matched your search

@@ -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;
@@ -295,73 +295,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;
}
}
@@ -698,10 +743,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)) {
@@ -749,11 +792,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
@@ -766,10 +805,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
@@ -821,9 +858,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);
@@ -869,9 +904,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;
}
@@ -526,7 +526,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
@@ -550,7 +552,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