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

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ run-data
repo repo
othermods othermods
crashreport

View File

@ -24,7 +24,7 @@ license="All Rights Reserved"
modId="extendedae_plus" #mandatory modId="extendedae_plus" #mandatory
# The version number of the mod # 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 # A display name for the mod
displayName="ExtendedAE-Plus" #mandatory 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. # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved mod_license=All Rights Reserved
# The mod version. See https://semver.org/ # 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. # 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. # This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html # See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -9,6 +9,7 @@ import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.neoforged.neoforge.client.gui.ConfigurationScreen; import net.neoforged.neoforge.client.gui.ConfigurationScreen;
import net.neoforged.neoforge.client.gui.IConfigScreenFactory; 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. // This class will not load on dedicated servers. Accessing client side code from here is safe.
@Mod(value = ExtendedAEPlus.MODID, dist = Dist.CLIENT) @Mod(value = ExtendedAEPlus.MODID, dist = Dist.CLIENT)
@ -27,5 +28,16 @@ public class ExtendedAEPlusClient {
// Some client setup code // Some client setup code
ExtendedAEPlus.LOGGER.info("HELLO FROM CLIENT SETUP"); ExtendedAEPlus.LOGGER.info("HELLO FROM CLIENT SETUP");
ExtendedAEPlus.LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName()); 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,43 +2,85 @@ package com.extendedae_plus.client;
import appeng.api.stacks.GenericStack; import appeng.api.stacks.GenericStack;
import appeng.client.gui.me.common.MEStorageScreen; 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.ae2.accessor.MEStorageScreenAccessor;
import com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor; import com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor;
import com.extendedae_plus.network.OpenCraftFromJeiC2SPacket; import com.extendedae_plus.network.OpenCraftFromJeiC2SPacket;
import com.extendedae_plus.network.PullFromJeiOrCraftC2SPacket; import com.extendedae_plus.network.PullFromJeiOrCraftC2SPacket;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal; import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import mezz.jei.api.ingredients.ITypedIngredient;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.ScreenEvent; import net.neoforged.neoforge.client.event.ScreenEvent;
import net.neoforged.neoforge.network.PacketDistributor; import net.neoforged.neoforge.network.PacketDistributor;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.lang.reflect.Method;
import java.util.Optional; import java.util.Optional;
@EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT)
public final class InputEvents { public final class InputEvents {
private InputEvents() {} private InputEvents() {}
// 临时适配在缺少 AE2 JEI 辅助类时仅尝试从 JEI 提供的原生 ItemStack 获取否则不处理 private static Optional<?> getIngredientUnderMouse() {
private static GenericStack toGenericStack(ITypedIngredient<?> typed) {
try { try {
Optional<ItemStack> maybe = typed.getItemStack(); Class<?> cls = Class.forName("com.extendedae_plus.integration.jei.JeiRuntimeProxy");
if (maybe.isPresent()) { Method m = cls.getMethod("getIngredientUnderMouse");
ItemStack is = maybe.get(); Object r = m.invoke(null);
// 尝试使用 AE2 的通用构造若不可用则返回 null return (Optional<?>) r;
} catch (Throwable ignored) {
return Optional.empty();
}
}
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();
}
}
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;
}
}
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 "";
}
}
// 在缺少 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 { try {
return GenericStack.fromItemStack(is); return GenericStack.fromItemStack(is);
} catch (Throwable ignored) { } catch (Throwable ignored) {
return null; return null;
} }
} }
}
} catch (Throwable ignored) { } catch (Throwable ignored) {
} }
return null; return null;
@ -50,21 +92,18 @@ public final class InputEvents {
if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_LEFT && Screen.hasShiftDown()) { if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_LEFT && Screen.hasShiftDown()) {
double mouseX = event.getMouseX(); double mouseX = event.getMouseX();
double mouseY = event.getMouseY(); double mouseY = event.getMouseY();
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse(mouseX, mouseY); Optional<?> hovered = getIngredientUnderMouse(mouseX, mouseY);
if (hovered.isEmpty()) { if (hovered.isEmpty()) {
hovered = JeiRuntimeProxy.getIngredientUnderMouse(); hovered = getIngredientUnderMouse();
} }
if (hovered.isPresent()) { if (hovered.isPresent()) {
// JEI 作弊模式开启则放行给 JEI 处理Shift+左键=一组 if (isJeiCheatModeEnabled()) {
if (JeiRuntimeProxy.isJeiCheatModeEnabled()) {
return; return;
} }
ITypedIngredient<?> typed = hovered.get(); Object typed = hovered.get();
GenericStack stack = toGenericStack(typed); GenericStack stack = toGenericStack(typed);
if (stack != null) { if (stack != null) {
// 发送到服务端若网络有库存则拉取一组到空槽否则若可合成则打开下单界面
PacketDistributor.sendToServer(new PullFromJeiOrCraftC2SPacket(stack)); PacketDistributor.sendToServer(new PullFromJeiOrCraftC2SPacket(stack));
// 消费此次点击避免 JEI/原版对左键的其它处理
event.setCanceled(true); event.setCanceled(true);
return; return;
} }
@ -73,27 +112,22 @@ public final class InputEvents {
// 中键打开 AE 下单界面保持原有功能 // 中键打开 AE 下单界面保持原有功能
if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_MIDDLE) { if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_MIDDLE) {
// 优先在 JEI 配方界面基于坐标获取若无再从覆盖层/书签获取
double mouseX = event.getMouseX(); double mouseX = event.getMouseX();
double mouseY = event.getMouseY(); double mouseY = event.getMouseY();
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse(mouseX, mouseY); Optional<?> hovered = getIngredientUnderMouse(mouseX, mouseY);
if (hovered.isEmpty()) { if (hovered.isEmpty()) {
hovered = JeiRuntimeProxy.getIngredientUnderMouse(); hovered = getIngredientUnderMouse();
} }
if (hovered.isEmpty()) return; if (hovered.isEmpty()) return;
ITypedIngredient<?> typed = hovered.get(); if (isJeiCheatModeEnabled()) {
// JEI 作弊模式开启则放行给 JEI 处理中键=一组
if (JeiRuntimeProxy.isJeiCheatModeEnabled()) {
return; return;
} }
Object typed = hovered.get();
GenericStack stack = toGenericStack(typed); GenericStack stack = toGenericStack(typed);
if (stack == null) return; if (stack == null) return;
// 发送到服务端让其验证并打开 CraftAmountMenu
PacketDistributor.sendToServer(new OpenCraftFromJeiC2SPacket(stack)); PacketDistributor.sendToServer(new OpenCraftFromJeiC2SPacket(stack));
// 消费此次点击避免 JEI/原版对中键的其它处理
event.setCanceled(true); event.setCanceled(true);
} }
} }
@ -102,23 +136,19 @@ public final class InputEvents {
public static void onKeyPressedPre(ScreenEvent.KeyPressed.Pre event) { public static void onKeyPressedPre(ScreenEvent.KeyPressed.Pre event) {
if (event.getKeyCode() != GLFW.GLFW_KEY_F) return; if (event.getKeyCode() != GLFW.GLFW_KEY_F) return;
// 仅当鼠标确实悬停在 JEI 配料上时触发 Optional<?> hovered = getIngredientUnderMouse();
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse();
if (hovered.isEmpty()) return; if (hovered.isEmpty()) return;
ITypedIngredient<?> typed = hovered.get(); Object typed = hovered.get();
String name = getTypedIngredientDisplayName(typed);
// 通用获取显示名称兼容物品/流体等
String name = JeiRuntimeProxy.getTypedIngredientDisplayName(typed);
if (name == null || name.isEmpty()) return; if (name == null || name.isEmpty()) return;
// 写入 AE2 终端的搜索框
var screen = Minecraft.getInstance().screen; var screen = Minecraft.getInstance().screen;
if (screen instanceof MEStorageScreen<?> me) { if (screen instanceof MEStorageScreen<?> me) {
try { try {
MEStorageScreenAccessor acc = (MEStorageScreenAccessor) (Object) me; MEStorageScreenAccessor acc = (MEStorageScreenAccessor) (Object) me;
acc.eap$getSearchField().setValue(name); acc.eap$getSearchField().setValue(name);
acc.eap$setSearchText(name); // 同步到 Repo 并刷新 acc.eap$setSearchText(name);
event.setCanceled(true); event.setCanceled(true);
return; return;
} catch (Throwable ignored) { } 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; 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.minecraft.world.item.ItemStack;
import net.neoforged.fml.ModList; import net.neoforged.fml.ModList;
import net.neoforged.neoforge.fluids.FluidStack; import net.neoforged.neoforge.fluids.FluidStack;
@ -23,238 +12,248 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
* 线程安全地缓存并访问 JEI Runtime * 线程安全地缓存并访问 JEI Runtime纯反射避免在未安装 JEI 时触发类加载
*/ */
public final class JeiRuntimeProxy { public final class JeiRuntimeProxy {
private static volatile IJeiRuntime RUNTIME; // 持有为 Object避免类加载
private static volatile Object RUNTIME;
private JeiRuntimeProxy() {} private JeiRuntimeProxy() {}
static void setRuntime(IJeiRuntime runtime) { static void setRuntime(Object runtime) {
RUNTIME = runtime; RUNTIME = runtime;
} }
@Nullable @Nullable
public static IJeiRuntime get() { public static Object get() {
return RUNTIME; return RUNTIME;
} }
public static Optional<ITypedIngredient<?>> getIngredientUnderMouse() { public static Optional<?> getIngredientUnderMouse() {
IJeiRuntime rt = RUNTIME; Object rt = RUNTIME;
if (rt == null) return Optional.empty(); if (rt == null) return Optional.empty();
try {
IIngredientListOverlay list = rt.getIngredientListOverlay(); Method getIngredientListOverlay = rt.getClass().getMethod("getIngredientListOverlay");
Object list = getIngredientListOverlay.invoke(rt);
if (list != null) { if (list != null) {
var ing = list.getIngredientUnderMouse(); Method m = list.getClass().getMethod("getIngredientUnderMouse");
if (ing.isPresent()) return ing.map(i -> (ITypedIngredient<?>) i); Object opt = m.invoke(list);
if (opt instanceof Optional<?> o && o.isPresent()) return o;
} }
IBookmarkOverlay bm = rt.getBookmarkOverlay(); Method getBookmarkOverlay = rt.getClass().getMethod("getBookmarkOverlay");
Object bm = getBookmarkOverlay.invoke(rt);
if (bm != null) { if (bm != null) {
var ing = bm.getIngredientUnderMouse(); Method m = bm.getClass().getMethod("getIngredientUnderMouse");
if (ing.isPresent()) return ing.map(i -> (ITypedIngredient<?>) i); Object opt = m.invoke(bm);
if (opt instanceof Optional<?> o && o.isPresent()) return o;
} }
} catch (Throwable ignored) {}
return Optional.empty(); return Optional.empty();
} }
/** public static Optional<?> getIngredientUnderMouse(double mouseX, double mouseY) {
* JEI 配方界面区域内基于屏幕坐标查询鼠标下的配料优先物品其次流体 Object rt = RUNTIME;
*/ if (rt == null) return Optional.empty();
public static Optional<ITypedIngredient<?>> getIngredientUnderMouse(double mouseX, double mouseY) { try {
IJeiRuntime rt = RUNTIME; Method getRecipesGui = rt.getClass().getMethod("getRecipesGui");
if (rt == null || rt.getRecipesGui() == null) return Optional.empty(); Object gui = getRecipesGui.invoke(rt);
if (gui == null) return Optional.empty();
var ingredientManager = rt.getIngredientManager(); 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);
var item = rt.getRecipesGui().getIngredientUnderMouse(VanillaTypes.ITEM_STACK) Method getUnder = gui.getClass().getMethod("getIngredientUnderMouse", itemType.getClass());
.flatMap(v -> ingredientManager.createTypedIngredient(VanillaTypes.ITEM_STACK, v)) Object valueOpt = getUnder.invoke(gui, itemType);
.map(x -> (ITypedIngredient<?>) x); if (!(valueOpt instanceof Optional<?> value) || value.isEmpty()) return Optional.empty();
if (item.isPresent()) return Optional.of(item.get()); 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(); return Optional.empty();
} }
/**
* 检测 JEI 是否开启了作弊模式给物品
* 使用 JEI 内部开关 JEI 未初始化或异常则返回 false
*/
public static boolean isJeiCheatModeEnabled() { public static boolean isJeiCheatModeEnabled() {
try { try {
// 使用完全限定名以避免在源码缺失时的编译依赖问题 Class<?> internal = Class.forName("mezz.jei.common.Internal");
return mezz.jei.common.Internal.getClientToggleState().isCheatItemsEnabled(); 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) { } catch (Throwable t) {
return false; return false;
} }
} }
/** public static String getTypedIngredientDisplayName(Object typed) {
* 将文本写入 JEI 的搜索过滤框 Object rt = RUNTIME;
* 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 版本或在启动阶段尚未就绪
}
}
/**
* 通用获取 JEI 悬浮配料的本地化显示名称适配物品/流体等
* 若无法安全获取则返回空字符串
*/
public static <T> String getTypedIngredientDisplayName(ITypedIngredient<T> typed) {
IJeiRuntime rt = RUNTIME;
if (rt == null || typed == null) return ""; if (rt == null || typed == null) return "";
try { try {
var manager = rt.getIngredientManager(); Object manager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
var helper = manager.getIngredientHelper(typed.getType()); Method getType = typed.getClass().getMethod("getType");
// JEI IIngredientHelper#getDisplayName 返回 Component新版本 String旧版本 Object type = getType.invoke(typed);
// 统一转为字符串使用 toString() 兜底 Method getHelper = manager.getClass().getMethod("getIngredientHelper", type.getClass());
Object display = helper.getDisplayName(typed.getIngredient()); 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 ""; if (display == null) return "";
// 新版net.minecraft.network.chat.Component try {
if (display instanceof net.minecraft.network.chat.Component comp) { Class<?> comp = Class.forName("net.minecraft.network.chat.Component");
String s = comp.getString(); if (comp.isInstance(display)) {
return s == null ? "" : s; Method getString = comp.getMethod("getString");
} Object s = getString.invoke(display);
String s = display.toString(); return s == null ? "" : s.toString();
return s == null ? "" : s;
} catch (Throwable ignored) {
} }
} catch (Throwable ignored) {}
return display.toString();
} catch (Throwable ignored) {}
return ""; return "";
} }
/** public static List<?> getBookmarkList() {
* 获取JEI书签列表 Object rt = RUNTIME;
*/
public static List<? extends ITypedIngredient<?>> getBookmarkList() {
IJeiRuntime rt = RUNTIME;
if (rt == null) return Collections.emptyList(); if (rt == null) return Collections.emptyList();
IBookmarkOverlay bookmarkOverlay = rt.getBookmarkOverlay(); try {
if (bookmarkOverlay instanceof BookmarkOverlayAccessor accessor) { Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
BookmarkList bookmarkList = accessor.eap$getBookmarkList(); if (overlay == null) return Collections.emptyList();
return bookmarkList.getElements().stream().map(IElement::getTypedIngredient).toList(); 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 Collections.emptyList();
} }
/**
* 将物品添加到 JEI 书签
*/
public static void addBookmark(ItemStack stack) { public static void addBookmark(ItemStack stack) {
IJeiRuntime rt = RUNTIME; Object rt = RUNTIME;
if (rt == null || stack == null || stack.isEmpty()) return; if (rt == null || stack == null || stack.isEmpty()) return;
IBookmarkOverlay overlay = rt.getBookmarkOverlay();
if (overlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList list = accessor.eap$getBookmarkList();
try { try {
var typedOpt = rt.getIngredientManager().createTypedIngredient(VanillaTypes.ITEM_STACK, stack); Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
typedOpt.ifPresent(typed -> { if (overlay == null) return;
IngredientBookmark<ItemStack> bookmark = IngredientBookmark.create(typed, rt.getIngredientManager()); Field f = overlay.getClass().getDeclaredField("bookmarkList");
list.add(bookmark); // add 内部会自动保存到配置 f.setAccessible(true);
}); Object list = f.get(overlay);
} catch (Throwable ignored) {} 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) {}
}
public static void addBookmark(FluidStack fluidStack) { public static void addBookmark(FluidStack fluidStack) {
IJeiRuntime rt = RUNTIME; Object rt = RUNTIME;
if (rt == null) return; if (rt == null) return;
try {
IBookmarkOverlay overlay = rt.getBookmarkOverlay(); Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
if (overlay instanceof BookmarkOverlayAccessor accessor) { if (overlay == null) return;
BookmarkList list = accessor.eap$getBookmarkList(); Field f = overlay.getClass().getDeclaredField("bookmarkList");
Optional<ITypedIngredient<FluidStack>> typedOpt = rt.getIngredientManager() f.setAccessible(true);
.createTypedIngredient(NeoForgeTypes.FLUID_STACK, fluidStack); Object list = f.get(overlay);
typedOpt.ifPresent(typed -> { Object manager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
IngredientBookmark<FluidStack> bookmark = IngredientBookmark.create(typed, rt.getIngredientManager()); Object fluidType = Class.forName("mezz.jei.api.neoforge.NeoForgeTypes").getField("FLUID_STACK").get(null);
list.add(bookmark); // add 内部会自动保存到配置 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) {}
}
/**
* 如果存在 Mekanism/appmek则将 Mekanism 化学堆栈添加到 JEI 书签
*/
public static void addBookmark(Object chemicalStack) { public static void addBookmark(Object chemicalStack) {
if (!ModList.get().isLoaded("mekanism") && !ModList.get().isLoaded("appmek")) return; if (!ModList.get().isLoaded("mekanism") && !ModList.get().isLoaded("appmek")) return;
Object rt = RUNTIME;
IJeiRuntime rt = RUNTIME; if (rt == null || chemicalStack == null) return;
if (rt == null) return;
IBookmarkOverlay overlay = rt.getBookmarkOverlay();
if (overlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList list = accessor.eap$getBookmarkList();
try { try {
if (chemicalStack == null) return; Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
if (overlay == null) return;
// Determine Mekanism JEI ingredient type constant by runtime class name Field f = overlay.getClass().getDeclaredField("bookmarkList");
String clsName = chemicalStack.getClass().getName(); f.setAccessible(true);
Object list = f.get(overlay);
Object manager = rt.getClass().getMethod("getIngredientManager").invoke(rt);
String mekanismJeiClass = "mekanism.client.recipe_viewer.jei.MekanismJEI"; String mekanismJeiClass = "mekanism.client.recipe_viewer.jei.MekanismJEI";
Class<?> jeiCls = Class.forName(mekanismJeiClass); Class<?> jeiCls = Class.forName(mekanismJeiClass);
Field typeField = null; Field typeField = null;
if ("mekanism.api.chemical.ChemicalStack".equals(clsName)) { if ("mekanism.api.chemical.ChemicalStack".equals(chemicalStack.getClass().getName())) {
typeField = jeiCls.getField("TYPE_CHEMICAL"); typeField = jeiCls.getField("TYPE_CHEMICAL");
} }
if (typeField == null) return; if (typeField == null) return;
Object typeConst = typeField.get(null); Object typeConst = typeField.get(null);
Method createTyped = manager.getClass().getMethod("createTypedIngredient", typeConst.getClass(), Object.class);
// Use ingredient manager reflectively to create a typed ingredient Object typedOpt = createTyped.invoke(manager, typeConst, chemicalStack);
Object ingredientManager = rt.getIngredientManager(); if (typedOpt instanceof Optional<?> opt && opt.isPresent()) {
Method createTypedIngredient = ingredientManager.getClass().getMethod("createTypedIngredient", IIngredientType.class, Object.class); Object typed = opt.get();
Object opt = createTypedIngredient.invoke(ingredientManager, typeConst, chemicalStack); Class<?> ibCls = Class.forName("mezz.jei.gui.bookmarks.IngredientBookmark");
if (!(opt instanceof Optional<?> typedOpt)) return; Method create = null;
if (typedOpt.isPresent()) { for (Method m : ibCls.getMethods()) {
Object typed = typedOpt.get(); if (m.getName().equals("create") && m.getParameterCount() == 2) { create = m; break; }
// Find a compatible static create(...) method on IngredientBookmark where }
// the second parameter is assignable from the actual ingredientManager instance. if (create != null) {
Method createMethod = null; Object bookmark = create.invoke(null, typed, manager);
for (Method m : IngredientBookmark.class.getMethods()) { list.getClass().getMethod("add", ibCls).invoke(list, bookmark);
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();
} }
} }
} catch (Throwable ignored) {}
} }
/**
* JEI 书签移除物品
*/
public static void removeBookmark(ItemStack stack) { public static void removeBookmark(ItemStack stack) {
IJeiRuntime rt = RUNTIME; Object rt = RUNTIME;
if (rt == null || stack == null || stack.isEmpty()) return; if (rt == null || stack == null || stack.isEmpty()) return;
IBookmarkOverlay overlay = rt.getBookmarkOverlay();
if (overlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList list = accessor.eap$getBookmarkList();
try { try {
var typedOpt = rt.getIngredientManager().createTypedIngredient(VanillaTypes.ITEM_STACK, stack); Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
typedOpt.ifPresent(typed -> { if (overlay == null) return;
IngredientBookmark<ItemStack> bookmark = IngredientBookmark.create(typed, rt.getIngredientManager()); Field f = overlay.getClass().getDeclaredField("bookmarkList");
list.remove(bookmark); 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) {} } 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,7 +27,7 @@ public class EncodingHelperMixin {
private static void epp$addJeiIngredientPriorities(MEStorageMenu menu, Comparator<GridInventoryEntry> comparator, CallbackInfoReturnable<Map<AEKey, Integer>> cir){ private static void epp$addJeiIngredientPriorities(MEStorageMenu menu, Comparator<GridInventoryEntry> comparator, CallbackInfoReturnable<Map<AEKey, Integer>> cir){
Map<AEKey, Integer> result = cir.getReturnValue(); Map<AEKey, Integer> result = cir.getReturnValue();
AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE); AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE);
List<? extends ITypedIngredient<?>> list = JeiRuntimeProxy.getBookmarkList(); List<? extends ITypedIngredient<?>> list = (List<? extends ITypedIngredient<?>>) (List<?>) JeiRuntimeProxy.getBookmarkList();
for (ITypedIngredient<?> ingredient : list) { for (ITypedIngredient<?> ingredient : list) {
ingredient.getIngredient(VanillaTypes.ITEM_STACK).ifPresent(itemStack -> result.put(AEItemKey.of(itemStack), index.getAndDecrement())); 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())); ingredient.getIngredient(NeoForgeTypes.FLUID_STACK).ifPresent(fluidStack -> result.put(AEFluidKey.of(fluidStack), index.getAndDecrement()));

View File

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