diff --git a/.gitignore b/.gitignore index 9b070cd..7583cea 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ run-data repo -othermods \ No newline at end of file +othermods +crashreport \ No newline at end of file diff --git a/build/generated/sources/modMetadata/META-INF/neoforge.mods.toml b/build/generated/sources/modMetadata/META-INF/neoforge.mods.toml index 1ad0ea3..d8c7372 100644 --- a/build/generated/sources/modMetadata/META-INF/neoforge.mods.toml +++ b/build/generated/sources/modMetadata/META-INF/neoforge.mods.toml @@ -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 diff --git a/gradle.properties b/gradle.properties index ebafd76..7c01284 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/java/com/extendedae_plus/ExtendedAEPlusClient.java b/src/main/java/com/extendedae_plus/ExtendedAEPlusClient.java index f324adb..beb68c3 100644 --- a/src/main/java/com/extendedae_plus/ExtendedAEPlusClient.java +++ b/src/main/java/com/extendedae_plus/ExtendedAEPlusClient.java @@ -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()); + } + } + } } diff --git a/src/main/java/com/extendedae_plus/client/InputEvents.java b/src/main/java/com/extendedae_plus/client/InputEvents.java index 48c81e5..755e988 100644 --- a/src/main/java/com/extendedae_plus/client/InputEvents.java +++ b/src/main/java/com/extendedae_plus/client/InputEvents.java @@ -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 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> 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> 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 + 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> 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) {} + } + } } diff --git a/src/main/java/com/extendedae_plus/integration/jei/JeiClientBootstrap.java b/src/main/java/com/extendedae_plus/integration/jei/JeiClientBootstrap.java new file mode 100644 index 0000000..3b8c7c4 --- /dev/null +++ b/src/main/java/com/extendedae_plus/integration/jei/JeiClientBootstrap.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java b/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java index d5f2de3..c53d4b8 100644 --- a/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java +++ b/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java @@ -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> 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> 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 String getTypedIngredientDisplayName(ITypedIngredient 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> 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 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> typedOpt = rt.getIngredientManager() - .createTypedIngredient(NeoForgeTypes.FLUID_STACK, fluidStack); - typedOpt.ifPresent(typed -> { - IngredientBookmark 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 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) {} + } } diff --git a/src/main/java/com/extendedae_plus/mixin/ExtendedAEPlusMixinPlugin.java b/src/main/java/com/extendedae_plus/mixin/ExtendedAEPlusMixinPlugin.java new file mode 100644 index 0000000..aefb88e --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ExtendedAEPlusMixinPlugin.java @@ -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 myTargets, Set otherTargets) { } + + @Override + public List 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) { } +} \ No newline at end of file diff --git a/src/main/java/com/extendedae_plus/mixin/jei/EncodingHelperMixin.java b/src/main/java/com/extendedae_plus/mixin/jei/EncodingHelperMixin.java index eb563d3..51ab800 100644 --- a/src/main/java/com/extendedae_plus/mixin/jei/EncodingHelperMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/jei/EncodingHelperMixin.java @@ -27,8 +27,8 @@ public class EncodingHelperMixin { private static void epp$addJeiIngredientPriorities(MEStorageMenu menu, Comparator comparator, CallbackInfoReturnable> cir){ Map result = cir.getReturnValue(); AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE); - List> list = JeiRuntimeProxy.getBookmarkList(); - for (ITypedIngredient ingredient : list) { + List> list = (List>) (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())); } diff --git a/src/main/resources/extendedae_plus.mixins.json b/src/main/resources/extendedae_plus.mixins.json index dfda552..e3a794a 100644 --- a/src/main/resources/extendedae_plus.mixins.json +++ b/src/main/resources/extendedae_plus.mixins.json @@ -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",