jei运行时检查

This commit is contained in:
GaLicn 2025-09-27 23:54:03 +08:00
parent f727b67ba6
commit f222410f95
10 changed files with 451 additions and 351 deletions

3
.gitignore vendored
View File

@ -25,4 +25,5 @@ run-data
repo
othermods
othermods
crashreport

View File

@ -24,7 +24,7 @@ license="All Rights Reserved"
modId="extendedae_plus" #mandatory
# The version number of the mod
version="1.21.1-1.4.2" #mandatory
version="1.21.1-1.4.4" #mandatory
# A display name for the mod
displayName="ExtendedAE-Plus" #mandatory

View File

@ -32,7 +32,7 @@ mod_name=ExtendedAE-Plus
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved
# The mod version. See https://semver.org/
mod_version=1.21.1-1.4.2
mod_version=1.21.1-1.4.3
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -9,23 +9,35 @@ import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.neoforged.neoforge.client.gui.ConfigurationScreen;
import net.neoforged.neoforge.client.gui.IConfigScreenFactory;
import net.neoforged.fml.ModList;
// This class will not load on dedicated servers. Accessing client side code from here is safe.
@Mod(value = ExtendedAEPlus.MODID, dist = Dist.CLIENT)
// You can use EventBusSubscriber to automatically register all static methods in the class annotated with @SubscribeEvent
@EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT)
public class ExtendedAEPlusClient {
public ExtendedAEPlusClient(ModContainer container) {
// Allows NeoForge to create a config screen for this mod's configs.
// The config screen is accessed by going to the Mods screen > clicking on your mod > clicking on config.
// Do not forget to add translations for your config options to the en_us.json file.
container.registerExtensionPoint(IConfigScreenFactory.class, ConfigurationScreen::new);
}
public ExtendedAEPlusClient(ModContainer container) {
// Allows NeoForge to create a config screen for this mod's configs.
// The config screen is accessed by going to the Mods screen > clicking on your mod > clicking on config.
// Do not forget to add translations for your config options to the en_us.json file.
container.registerExtensionPoint(IConfigScreenFactory.class, ConfigurationScreen::new);
}
@SubscribeEvent
static void onClientSetup(FMLClientSetupEvent event) {
// Some client setup code
ExtendedAEPlus.LOGGER.info("HELLO FROM CLIENT SETUP");
ExtendedAEPlus.LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
}
@SubscribeEvent
static void onClientSetup(FMLClientSetupEvent event) {
// Some client setup code
ExtendedAEPlus.LOGGER.info("HELLO FROM CLIENT SETUP");
ExtendedAEPlus.LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
// Register JEI-dependent input handlers only when JEI is present
if (ModList.get().isLoaded("jei")) {
try {
Class<?> bootstrap = Class.forName("com.extendedae_plus.integration.jei.JeiClientBootstrap");
java.lang.reflect.Method m = bootstrap.getMethod("register");
m.invoke(null);
} catch (Throwable t) {
ExtendedAEPlus.LOGGER.warn("Failed to register JEI client listeners: {}", t.toString());
}
}
}
}

View File

@ -2,133 +2,163 @@ package com.extendedae_plus.client;
import appeng.api.stacks.GenericStack;
import appeng.client.gui.me.common.MEStorageScreen;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.integration.jei.JeiRuntimeProxy;
import com.extendedae_plus.mixin.ae2.accessor.MEStorageScreenAccessor;
import com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor;
import com.extendedae_plus.network.OpenCraftFromJeiC2SPacket;
import com.extendedae_plus.network.PullFromJeiOrCraftC2SPacket;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import mezz.jei.api.ingredients.ITypedIngredient;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.world.item.ItemStack;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.ScreenEvent;
import net.neoforged.neoforge.network.PacketDistributor;
import org.lwjgl.glfw.GLFW;
import java.lang.reflect.Method;
import java.util.Optional;
@EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT)
public final class InputEvents {
private InputEvents() {}
private InputEvents() {}
// 临时适配在缺少 AE2 JEI 辅助类时仅尝试从 JEI 提供的原生 ItemStack 获取否则不处理
private static GenericStack toGenericStack(ITypedIngredient<?> typed) {
try {
Optional<ItemStack> maybe = typed.getItemStack();
if (maybe.isPresent()) {
ItemStack is = maybe.get();
// 尝试使用 AE2 的通用构造若不可用则返回 null
try {
return GenericStack.fromItemStack(is);
} catch (Throwable ignored) {
return null;
}
}
} catch (Throwable ignored) {
}
return null;
}
private static Optional<?> getIngredientUnderMouse() {
try {
Class<?> cls = Class.forName("com.extendedae_plus.integration.jei.JeiRuntimeProxy");
Method m = cls.getMethod("getIngredientUnderMouse");
Object r = m.invoke(null);
return (Optional<?>) r;
} catch (Throwable ignored) {
return Optional.empty();
}
}
@SubscribeEvent
public static void onMouseButtonPre(ScreenEvent.MouseButtonPressed.Pre event) {
// 优先处理Shift + 左键拉取或下单
if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_LEFT && Screen.hasShiftDown()) {
double mouseX = event.getMouseX();
double mouseY = event.getMouseY();
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse(mouseX, mouseY);
if (hovered.isEmpty()) {
hovered = JeiRuntimeProxy.getIngredientUnderMouse();
}
if (hovered.isPresent()) {
// JEI 作弊模式开启则放行给 JEI 处理Shift+左键=一组
if (JeiRuntimeProxy.isJeiCheatModeEnabled()) {
return;
}
ITypedIngredient<?> typed = hovered.get();
GenericStack stack = toGenericStack(typed);
if (stack != null) {
// 发送到服务端若网络有库存则拉取一组到空槽否则若可合成则打开下单界面
PacketDistributor.sendToServer(new PullFromJeiOrCraftC2SPacket(stack));
// 消费此次点击避免 JEI/原版对左键的其它处理
event.setCanceled(true);
return;
}
}
}
private static Optional<?> getIngredientUnderMouse(double mouseX, double mouseY) {
try {
Class<?> cls = Class.forName("com.extendedae_plus.integration.jei.JeiRuntimeProxy");
Method m = cls.getMethod("getIngredientUnderMouse", double.class, double.class);
Object r = m.invoke(null, mouseX, mouseY);
return (Optional<?>) r;
} catch (Throwable ignored) {
return Optional.empty();
}
}
// 中键打开 AE 下单界面保持原有功能
if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_MIDDLE) {
// 优先在 JEI 配方界面基于坐标获取若无再从覆盖层/书签获取
double mouseX = event.getMouseX();
double mouseY = event.getMouseY();
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse(mouseX, mouseY);
if (hovered.isEmpty()) {
hovered = JeiRuntimeProxy.getIngredientUnderMouse();
}
if (hovered.isEmpty()) return;
private static boolean isJeiCheatModeEnabled() {
try {
Class<?> cls = Class.forName("com.extendedae_plus.integration.jei.JeiRuntimeProxy");
Method m = cls.getMethod("isJeiCheatModeEnabled");
Object r = m.invoke(null);
return r instanceof Boolean b && b;
} catch (Throwable ignored) {
return false;
}
}
ITypedIngredient<?> typed = hovered.get();
// JEI 作弊模式开启则放行给 JEI 处理中键=一组
if (JeiRuntimeProxy.isJeiCheatModeEnabled()) {
return;
}
GenericStack stack = toGenericStack(typed);
if (stack == null) return;
private static String getTypedIngredientDisplayName(Object typed) {
try {
Class<?> cls = Class.forName("com.extendedae_plus.integration.jei.JeiRuntimeProxy");
Method m = cls.getMethod("getTypedIngredientDisplayName", Object.class);
Object r = m.invoke(null, typed);
return r instanceof String s ? s : "";
} catch (Throwable ignored) {
return "";
}
}
// 发送到服务端让其验证并打开 CraftAmountMenu
PacketDistributor.sendToServer(new OpenCraftFromJeiC2SPacket(stack));
// 在缺少 AE2 JEI 辅助类时仅尝试从 JEI 提供的原生 ItemStack 获取否则不处理
private static GenericStack toGenericStack(Object typed) {
try {
// typed.getItemStack(): Optional<ItemStack>
Method getItemStack = typed.getClass().getMethod("getItemStack");
Object maybe = getItemStack.invoke(typed);
if (maybe instanceof Optional<?> opt && opt.isPresent()) {
Object val = opt.get();
if (val instanceof ItemStack is) {
try {
return GenericStack.fromItemStack(is);
} catch (Throwable ignored) {
return null;
}
}
}
} catch (Throwable ignored) {
}
return null;
}
// 消费此次点击避免 JEI/原版对中键的其它处理
event.setCanceled(true);
}
}
@SubscribeEvent
public static void onMouseButtonPre(ScreenEvent.MouseButtonPressed.Pre event) {
// 优先处理Shift + 左键拉取或下单
if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_LEFT && Screen.hasShiftDown()) {
double mouseX = event.getMouseX();
double mouseY = event.getMouseY();
Optional<?> hovered = getIngredientUnderMouse(mouseX, mouseY);
if (hovered.isEmpty()) {
hovered = getIngredientUnderMouse();
}
if (hovered.isPresent()) {
if (isJeiCheatModeEnabled()) {
return;
}
Object typed = hovered.get();
GenericStack stack = toGenericStack(typed);
if (stack != null) {
PacketDistributor.sendToServer(new PullFromJeiOrCraftC2SPacket(stack));
event.setCanceled(true);
return;
}
}
}
@SubscribeEvent
public static void onKeyPressedPre(ScreenEvent.KeyPressed.Pre event) {
if (event.getKeyCode() != GLFW.GLFW_KEY_F) return;
// 中键打开 AE 下单界面保持原有功能
if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_MIDDLE) {
double mouseX = event.getMouseX();
double mouseY = event.getMouseY();
Optional<?> hovered = getIngredientUnderMouse(mouseX, mouseY);
if (hovered.isEmpty()) {
hovered = getIngredientUnderMouse();
}
if (hovered.isEmpty()) return;
// 仅当鼠标确实悬停在 JEI 配料上时触发
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse();
if (hovered.isEmpty()) return;
if (isJeiCheatModeEnabled()) {
return;
}
Object typed = hovered.get();
GenericStack stack = toGenericStack(typed);
if (stack == null) return;
ITypedIngredient<?> typed = hovered.get();
PacketDistributor.sendToServer(new OpenCraftFromJeiC2SPacket(stack));
event.setCanceled(true);
}
}
// 通用获取显示名称兼容物品/流体等
String name = JeiRuntimeProxy.getTypedIngredientDisplayName(typed);
if (name == null || name.isEmpty()) return;
@SubscribeEvent
public static void onKeyPressedPre(ScreenEvent.KeyPressed.Pre event) {
if (event.getKeyCode() != GLFW.GLFW_KEY_F) return;
// 写入 AE2 终端的搜索框
var screen = Minecraft.getInstance().screen;
if (screen instanceof MEStorageScreen<?> me) {
try {
MEStorageScreenAccessor acc = (MEStorageScreenAccessor) (Object) me;
acc.eap$getSearchField().setValue(name);
acc.eap$setSearchText(name); // 同步到 Repo 并刷新
event.setCanceled(true);
return;
} catch (Throwable ignored) {
}
}else if (screen instanceof GuiExPatternTerminal<?> gpt) {
try {
GuiExPatternTerminalAccessor acc = (GuiExPatternTerminalAccessor) gpt;
acc.getSearchField().setValue(name);
event.setCanceled(true);
}catch (Throwable ignored) {}
}
}
Optional<?> hovered = getIngredientUnderMouse();
if (hovered.isEmpty()) return;
Object typed = hovered.get();
String name = getTypedIngredientDisplayName(typed);
if (name == null || name.isEmpty()) return;
var screen = Minecraft.getInstance().screen;
if (screen instanceof MEStorageScreen<?> me) {
try {
MEStorageScreenAccessor acc = (MEStorageScreenAccessor) (Object) me;
acc.eap$getSearchField().setValue(name);
acc.eap$setSearchText(name);
event.setCanceled(true);
return;
} catch (Throwable ignored) {
}
}else if (screen instanceof GuiExPatternTerminal<?> gpt) {
try {
GuiExPatternTerminalAccessor acc = (GuiExPatternTerminalAccessor) gpt;
acc.getSearchField().setValue(name);
event.setCanceled(true);
}catch (Throwable ignored) {}
}
}
}

View File

@ -0,0 +1,10 @@
package com.extendedae_plus.integration.jei;
public final class JeiClientBootstrap {
private JeiClientBootstrap() {}
public static void register() {
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.InputEvents::onMouseButtonPre);
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.InputEvents::onKeyPressedPre);
}
}

View File

@ -1,16 +1,5 @@
package com.extendedae_plus.integration.jei;
import com.extendedae_plus.mixin.jei.accessor.BookmarkOverlayAccessor;
import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.ingredients.IIngredientType;
import mezz.jei.api.ingredients.ITypedIngredient;
import mezz.jei.api.neoforge.NeoForgeTypes;
import mezz.jei.api.runtime.IBookmarkOverlay;
import mezz.jei.api.runtime.IIngredientListOverlay;
import mezz.jei.api.runtime.IJeiRuntime;
import mezz.jei.gui.bookmarks.BookmarkList;
import mezz.jei.gui.bookmarks.IngredientBookmark;
import mezz.jei.gui.overlay.elements.IElement;
import net.minecraft.world.item.ItemStack;
import net.neoforged.fml.ModList;
import net.neoforged.neoforge.fluids.FluidStack;
@ -23,238 +12,248 @@ import java.util.List;
import java.util.Optional;
/**
* 线程安全地缓存并访问 JEI Runtime
* 线程安全地缓存并访问 JEI Runtime纯反射避免在未安装 JEI 时触发类加载
*/
public final class JeiRuntimeProxy {
private static volatile IJeiRuntime RUNTIME;
// 持有为 Object避免类加载
private static volatile Object RUNTIME;
private JeiRuntimeProxy() {}
private JeiRuntimeProxy() {}
static void setRuntime(IJeiRuntime runtime) {
RUNTIME = runtime;
}
static void setRuntime(Object runtime) {
RUNTIME = runtime;
}
@Nullable
public static IJeiRuntime get() {
return RUNTIME;
}
@Nullable
public static Object get() {
return RUNTIME;
}
public static Optional<ITypedIngredient<?>> getIngredientUnderMouse() {
IJeiRuntime rt = RUNTIME;
if (rt == null) return Optional.empty();
public static Optional<?> getIngredientUnderMouse() {
Object rt = RUNTIME;
if (rt == null) return Optional.empty();
try {
Method getIngredientListOverlay = rt.getClass().getMethod("getIngredientListOverlay");
Object list = getIngredientListOverlay.invoke(rt);
if (list != null) {
Method m = list.getClass().getMethod("getIngredientUnderMouse");
Object opt = m.invoke(list);
if (opt instanceof Optional<?> o && o.isPresent()) return o;
}
Method getBookmarkOverlay = rt.getClass().getMethod("getBookmarkOverlay");
Object bm = getBookmarkOverlay.invoke(rt);
if (bm != null) {
Method m = bm.getClass().getMethod("getIngredientUnderMouse");
Object opt = m.invoke(bm);
if (opt instanceof Optional<?> o && o.isPresent()) return o;
}
} catch (Throwable ignored) {}
return Optional.empty();
}
IIngredientListOverlay list = rt.getIngredientListOverlay();
if (list != null) {
var ing = list.getIngredientUnderMouse();
if (ing.isPresent()) return ing.map(i -> (ITypedIngredient<?>) i);
}
IBookmarkOverlay bm = rt.getBookmarkOverlay();
if (bm != null) {
var ing = bm.getIngredientUnderMouse();
if (ing.isPresent()) return ing.map(i -> (ITypedIngredient<?>) i);
}
return Optional.empty();
}
public static Optional<?> getIngredientUnderMouse(double mouseX, double mouseY) {
Object rt = RUNTIME;
if (rt == null) return Optional.empty();
try {
Method getRecipesGui = rt.getClass().getMethod("getRecipesGui");
Object gui = getRecipesGui.invoke(rt);
if (gui == null) return Optional.empty();
Object ingredientManager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
Class<?> vanillaTypes = Class.forName("mezz.jei.api.constants.VanillaTypes");
Object itemType = vanillaTypes.getField("ITEM_STACK").get(null);
Method getUnder = gui.getClass().getMethod("getIngredientUnderMouse", itemType.getClass());
Object valueOpt = getUnder.invoke(gui, itemType);
if (!(valueOpt instanceof Optional<?> value) || value.isEmpty()) return Optional.empty();
Method createTyped = ingredientManager.getClass().getMethod("createTypedIngredient", itemType.getClass(), Object.class);
Object typedOpt = createTyped.invoke(ingredientManager, itemType, value.get());
return typedOpt instanceof Optional<?> o ? o : Optional.empty();
} catch (Throwable ignored) {}
return Optional.empty();
}
/**
* JEI 配方界面区域内基于屏幕坐标查询鼠标下的配料优先物品其次流体
*/
public static Optional<ITypedIngredient<?>> getIngredientUnderMouse(double mouseX, double mouseY) {
IJeiRuntime rt = RUNTIME;
if (rt == null || rt.getRecipesGui() == null) return Optional.empty();
public static boolean isJeiCheatModeEnabled() {
try {
Class<?> internal = Class.forName("mezz.jei.common.Internal");
Object toggle = internal.getMethod("getClientToggleState").invoke(null);
Method isCheat = toggle.getClass().getMethod("isCheatItemsEnabled");
Object r = isCheat.invoke(toggle);
return r instanceof Boolean b && b;
} catch (Throwable t) {
return false;
}
}
var ingredientManager = rt.getIngredientManager();
public static String getTypedIngredientDisplayName(Object typed) {
Object rt = RUNTIME;
if (rt == null || typed == null) return "";
try {
Object manager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
Method getType = typed.getClass().getMethod("getType");
Object type = getType.invoke(typed);
Method getHelper = manager.getClass().getMethod("getIngredientHelper", type.getClass());
Object helper = getHelper.invoke(manager, type);
Method getIngredient = typed.getClass().getMethod("getIngredient");
Object ingredient = getIngredient.invoke(typed);
Object display = helper.getClass().getMethod("getDisplayName", ingredient.getClass()).invoke(helper, ingredient);
if (display == null) return "";
try {
Class<?> comp = Class.forName("net.minecraft.network.chat.Component");
if (comp.isInstance(display)) {
Method getString = comp.getMethod("getString");
Object s = getString.invoke(display);
return s == null ? "" : s.toString();
}
} catch (Throwable ignored) {}
return display.toString();
} catch (Throwable ignored) {}
return "";
}
// 支持物品通用且所有版本可用如需流体可后续按版本判断再扩展
var item = rt.getRecipesGui().getIngredientUnderMouse(VanillaTypes.ITEM_STACK)
.flatMap(v -> ingredientManager.createTypedIngredient(VanillaTypes.ITEM_STACK, v))
.map(x -> (ITypedIngredient<?>) x);
if (item.isPresent()) return Optional.of(item.get());
public static List<?> getBookmarkList() {
Object rt = RUNTIME;
if (rt == null) return Collections.emptyList();
try {
Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
if (overlay == null) return Collections.emptyList();
try {
Field f = overlay.getClass().getDeclaredField("bookmarkList");
f.setAccessible(true);
Object list = f.get(overlay);
Method getElements = list.getClass().getMethod("getElements");
Object elements = getElements.invoke(list);
if (elements instanceof List<?> l) {
// map(IElement::getTypedIngredient)
try {
Method getTyped = Class.forName("mezz.jei.gui.overlay.elements.IElement").getMethod("getTypedIngredient");
return l.stream().map(e -> {
try { return getTyped.invoke(e); } catch (Throwable ignored) { return null; }
}).filter(x -> x != null).toList();
} catch (Throwable ignored) {}
}
} catch (Throwable ignored) {}
} catch (Throwable ignored) {}
return Collections.emptyList();
}
return Optional.empty();
}
public static void addBookmark(ItemStack stack) {
Object rt = RUNTIME;
if (rt == null || stack == null || stack.isEmpty()) return;
try {
Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
if (overlay == null) return;
Field f = overlay.getClass().getDeclaredField("bookmarkList");
f.setAccessible(true);
Object list = f.get(overlay);
Object manager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
Object itemType = Class.forName("mezz.jei.api.constants.VanillaTypes").getField("ITEM_STACK").get(null);
Method createTyped = manager.getClass().getMethod("createTypedIngredient", itemType.getClass(), Object.class);
Object typedOpt = createTyped.invoke(manager, itemType, stack);
if (typedOpt instanceof Optional<?> opt && opt.isPresent()) {
Object typed = opt.get();
Class<?> ibCls = Class.forName("mezz.jei.gui.bookmarks.IngredientBookmark");
Method create = null;
for (Method m : ibCls.getMethods()) {
if (m.getName().equals("create") && m.getParameterCount() == 2) {
create = m; break;
}
}
if (create != null) {
Object bookmark = create.invoke(null, typed, manager);
list.getClass().getMethod("add", ibCls).invoke(list, bookmark);
}
}
} catch (Throwable ignored) {}
}
/**
* 检测 JEI 是否开启了作弊模式给物品
* 使用 JEI 内部开关 JEI 未初始化或异常则返回 false
*/
public static boolean isJeiCheatModeEnabled() {
try {
// 使用完全限定名以避免在源码缺失时的编译依赖问题
return mezz.jei.common.Internal.getClientToggleState().isCheatItemsEnabled();
} catch (Throwable t) {
return false;
}
}
public static void addBookmark(FluidStack fluidStack) {
Object rt = RUNTIME;
if (rt == null) return;
try {
Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
if (overlay == null) return;
Field f = overlay.getClass().getDeclaredField("bookmarkList");
f.setAccessible(true);
Object list = f.get(overlay);
Object manager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
Object fluidType = Class.forName("mezz.jei.api.neoforge.NeoForgeTypes").getField("FLUID_STACK").get(null);
Method createTyped = manager.getClass().getMethod("createTypedIngredient", fluidType.getClass(), Object.class);
Object typedOpt = createTyped.invoke(manager, fluidType, fluidStack);
if (typedOpt instanceof Optional<?> opt && opt.isPresent()) {
Object typed = opt.get();
Class<?> ibCls = Class.forName("mezz.jei.gui.bookmarks.IngredientBookmark");
Method create = null;
for (Method m : ibCls.getMethods()) {
if (m.getName().equals("create") && m.getParameterCount() == 2) { create = m; break; }
}
if (create != null) {
Object bookmark = create.invoke(null, typed, manager);
list.getClass().getMethod("add", ibCls).invoke(list, bookmark);
}
}
} catch (Throwable ignored) {}
}
/**
* 将文本写入 JEI 的搜索过滤框
* JEI runtime 不可用则静默返回
*/
public static void setIngredientFilterText(String text) {
IJeiRuntime rt = RUNTIME;
if (rt == null) return;
try {
rt.getIngredientFilter().setFilterText(text == null ? "" : text);
} catch (Throwable ignored) {
// 兼容不同 JEI 版本或在启动阶段尚未就绪
}
}
public static void addBookmark(Object chemicalStack) {
if (!ModList.get().isLoaded("mekanism") && !ModList.get().isLoaded("appmek")) return;
Object rt = RUNTIME;
if (rt == null || chemicalStack == null) return;
try {
Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
if (overlay == null) return;
Field f = overlay.getClass().getDeclaredField("bookmarkList");
f.setAccessible(true);
Object list = f.get(overlay);
Object manager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
String mekanismJeiClass = "mekanism.client.recipe_viewer.jei.MekanismJEI";
Class<?> jeiCls = Class.forName(mekanismJeiClass);
Field typeField = null;
if ("mekanism.api.chemical.ChemicalStack".equals(chemicalStack.getClass().getName())) {
typeField = jeiCls.getField("TYPE_CHEMICAL");
}
if (typeField == null) return;
Object typeConst = typeField.get(null);
Method createTyped = manager.getClass().getMethod("createTypedIngredient", typeConst.getClass(), Object.class);
Object typedOpt = createTyped.invoke(manager, typeConst, chemicalStack);
if (typedOpt instanceof Optional<?> opt && opt.isPresent()) {
Object typed = opt.get();
Class<?> ibCls = Class.forName("mezz.jei.gui.bookmarks.IngredientBookmark");
Method create = null;
for (Method m : ibCls.getMethods()) {
if (m.getName().equals("create") && m.getParameterCount() == 2) { create = m; break; }
}
if (create != null) {
Object bookmark = create.invoke(null, typed, manager);
list.getClass().getMethod("add", ibCls).invoke(list, bookmark);
}
}
} catch (Throwable ignored) {}
}
/**
* 通用获取 JEI 悬浮配料的本地化显示名称适配物品/流体等
* 若无法安全获取则返回空字符串
*/
public static <T> String getTypedIngredientDisplayName(ITypedIngredient<T> typed) {
IJeiRuntime rt = RUNTIME;
if (rt == null || typed == null) return "";
try {
var manager = rt.getIngredientManager();
var helper = manager.getIngredientHelper(typed.getType());
// JEI IIngredientHelper#getDisplayName 返回 Component新版本 String旧版本
// 统一转为字符串使用 toString() 兜底
Object display = helper.getDisplayName(typed.getIngredient());
if (display == null) return "";
// 新版net.minecraft.network.chat.Component
if (display instanceof net.minecraft.network.chat.Component comp) {
String s = comp.getString();
return s == null ? "" : s;
}
String s = display.toString();
return s == null ? "" : s;
} catch (Throwable ignored) {
}
return "";
}
/**
* 获取JEI书签列表
*/
public static List<? extends ITypedIngredient<?>> getBookmarkList() {
IJeiRuntime rt = RUNTIME;
if (rt == null) return Collections.emptyList();
IBookmarkOverlay bookmarkOverlay = rt.getBookmarkOverlay();
if (bookmarkOverlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList bookmarkList = accessor.eap$getBookmarkList();
return bookmarkList.getElements().stream().map(IElement::getTypedIngredient).toList();
}
return Collections.emptyList();
}
/**
* 将物品添加到 JEI 书签
*/
public static void addBookmark(ItemStack stack) {
IJeiRuntime rt = RUNTIME;
if (rt == null || stack == null || stack.isEmpty()) return;
IBookmarkOverlay overlay = rt.getBookmarkOverlay();
if (overlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList list = accessor.eap$getBookmarkList();
try {
var typedOpt = rt.getIngredientManager().createTypedIngredient(VanillaTypes.ITEM_STACK, stack);
typedOpt.ifPresent(typed -> {
IngredientBookmark<ItemStack> bookmark = IngredientBookmark.create(typed, rt.getIngredientManager());
list.add(bookmark); // add 内部会自动保存到配置
});
} catch (Throwable ignored) {}
}
}
public static void addBookmark(FluidStack fluidStack) {
IJeiRuntime rt = RUNTIME;
if (rt == null) return;
IBookmarkOverlay overlay = rt.getBookmarkOverlay();
if (overlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList list = accessor.eap$getBookmarkList();
Optional<ITypedIngredient<FluidStack>> typedOpt = rt.getIngredientManager()
.createTypedIngredient(NeoForgeTypes.FLUID_STACK, fluidStack);
typedOpt.ifPresent(typed -> {
IngredientBookmark<FluidStack> bookmark = IngredientBookmark.create(typed, rt.getIngredientManager());
list.add(bookmark); // add 内部会自动保存到配置
});
}
}
/**
* 如果存在 Mekanism/appmek则将 Mekanism 化学堆栈添加到 JEI 书签
*/
public static void addBookmark(Object chemicalStack) {
if (!ModList.get().isLoaded("mekanism") && !ModList.get().isLoaded("appmek")) return;
IJeiRuntime rt = RUNTIME;
if (rt == null) return;
IBookmarkOverlay overlay = rt.getBookmarkOverlay();
if (overlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList list = accessor.eap$getBookmarkList();
try {
if (chemicalStack == null) return;
// Determine Mekanism JEI ingredient type constant by runtime class name
String clsName = chemicalStack.getClass().getName();
String mekanismJeiClass = "mekanism.client.recipe_viewer.jei.MekanismJEI";
Class<?> jeiCls = Class.forName(mekanismJeiClass);
Field typeField = null;
if ("mekanism.api.chemical.ChemicalStack".equals(clsName)) {
typeField = jeiCls.getField("TYPE_CHEMICAL");
}
if (typeField == null) return;
Object typeConst = typeField.get(null);
// Use ingredient manager reflectively to create a typed ingredient
Object ingredientManager = rt.getIngredientManager();
Method createTypedIngredient = ingredientManager.getClass().getMethod("createTypedIngredient", IIngredientType.class, Object.class);
Object opt = createTypedIngredient.invoke(ingredientManager, typeConst, chemicalStack);
if (!(opt instanceof Optional<?> typedOpt)) return;
if (typedOpt.isPresent()) {
Object typed = typedOpt.get();
// Find a compatible static create(...) method on IngredientBookmark where
// the second parameter is assignable from the actual ingredientManager instance.
Method createMethod = null;
for (Method m : IngredientBookmark.class.getMethods()) {
if (!m.getName().equals("create")) continue;
Class<?>[] params = m.getParameterTypes();
if (params.length != 2) continue;
// first param should accept the typed ingredient
boolean firstOk = params[0].isAssignableFrom(typed.getClass()) || params[0].isAssignableFrom(ITypedIngredient.class);
boolean secondOk = params[1].isAssignableFrom(ingredientManager.getClass());
if (firstOk && secondOk) {
createMethod = m;
break;
}
}
if (createMethod != null) {
Object bookmark = createMethod.invoke(null, typed, ingredientManager);
if (bookmark != null) {
list.add((IngredientBookmark) bookmark);
}
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
/**
* JEI 书签移除物品
*/
public static void removeBookmark(ItemStack stack) {
IJeiRuntime rt = RUNTIME;
if (rt == null || stack == null || stack.isEmpty()) return;
IBookmarkOverlay overlay = rt.getBookmarkOverlay();
if (overlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList list = accessor.eap$getBookmarkList();
try {
var typedOpt = rt.getIngredientManager().createTypedIngredient(VanillaTypes.ITEM_STACK, stack);
typedOpt.ifPresent(typed -> {
IngredientBookmark<ItemStack> bookmark = IngredientBookmark.create(typed, rt.getIngredientManager());
list.remove(bookmark);
});
} catch (Throwable ignored) {}
}
}
public static void removeBookmark(ItemStack stack) {
Object rt = RUNTIME;
if (rt == null || stack == null || stack.isEmpty()) return;
try {
Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
if (overlay == null) return;
Field f = overlay.getClass().getDeclaredField("bookmarkList");
f.setAccessible(true);
Object list = f.get(overlay);
Object manager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
Object itemType = Class.forName("mezz.jei.api.constants.VanillaTypes").getField("ITEM_STACK").get(null);
Method createTyped = manager.getClass().getMethod("createTypedIngredient", itemType.getClass(), Object.class);
Object typedOpt = createTyped.invoke(manager, itemType, stack);
if (typedOpt instanceof Optional<?> opt && opt.isPresent()) {
Object typed = opt.get();
Class<?> ibCls = Class.forName("mezz.jei.gui.bookmarks.IngredientBookmark");
Method create = null;
for (Method m : ibCls.getMethods()) {
if (m.getName().equals("create") && m.getParameterCount() == 2) { create = m; break; }
}
if (create != null) {
Object bookmark = create.invoke(null, typed, manager);
list.getClass().getMethod("remove", ibCls).invoke(list, bookmark);
}
}
} catch (Throwable ignored) {}
}
}

View File

@ -0,0 +1,48 @@
package com.extendedae_plus.mixin;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import java.util.List;
import java.util.Set;
public class ExtendedAEPlusMixinPlugin implements IMixinConfigPlugin {
private static boolean isJeiPresent() {
try {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class.forName("mezz.jei.api.IModPlugin", false, cl);
return true;
} catch (Throwable ignored) {
return false;
}
}
@Override
public void onLoad(String mixinPackage) { }
@Override
public String getRefMapperConfig() { return null; }
@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
if (!isJeiPresent()) {
// Disable all JEI package mixins and any mixins that reference JEI-only helpers
if (mixinClassName.startsWith("com.extendedae_plus.mixin.jei")) return false;
if (mixinClassName.equals("com.extendedae_plus.mixin.ae2.menu.CraftConfirmMenuGoBackMixin")) return false;
}
return true;
}
@Override
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) { }
@Override
public List<String> getMixins() { return null; }
@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { }
@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { }
}

View File

@ -27,8 +27,8 @@ public class EncodingHelperMixin {
private static void epp$addJeiIngredientPriorities(MEStorageMenu menu, Comparator<GridInventoryEntry> comparator, CallbackInfoReturnable<Map<AEKey, Integer>> cir){
Map<AEKey, Integer> result = cir.getReturnValue();
AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE);
List<? extends ITypedIngredient<?>> list = JeiRuntimeProxy.getBookmarkList();
for (ITypedIngredient<?> ingredient : list) {
List<? extends ITypedIngredient<?>> list = (List<? extends ITypedIngredient<?>>) (List<?>) JeiRuntimeProxy.getBookmarkList();
for (ITypedIngredient<?> ingredient : list) {
ingredient.getIngredient(VanillaTypes.ITEM_STACK).ifPresent(itemStack -> result.put(AEItemKey.of(itemStack), index.getAndDecrement()));
ingredient.getIngredient(NeoForgeTypes.FLUID_STACK).ifPresent(fluidStack -> result.put(AEFluidKey.of(fluidStack), index.getAndDecrement()));
}

View File

@ -2,6 +2,7 @@
"required": true,
"package": "com.extendedae_plus.mixin",
"compatibilityLevel": "JAVA_21",
"plugin": "com.extendedae_plus.mixin.ExtendedAEPlusMixinPlugin",
"mixins": [
"advancedae.AdvPatternProviderLogicContainsRedirectMixin",
"advancedae.accessor.AdvPatternProviderLogicPatternsAccessor",
@ -9,7 +10,6 @@
"advancedae.helpers.AdvPatternProviderLogicAdvancedMixin",
"advancedae.helpers.AdvPatternProviderLogicDoublingMixin",
"advancedae.menu.AdvPatternProviderMenuAdvancedMixin",
"advancedae.menu.AdvPatternProviderMenuDoublingMixin",
"ae2.AEProcessingPatternMixin",
"ae2.CraftingCalculationMixin",
"ae2.CraftingCPUClusterMixin",