可以,而且按 MineColonies 目前的代码结构,这个功能实际上只需要改 **`Restaur...
Created on: August 10, 2026
Answered using GPT-5.6 Thinking by Chat01
Created on: August 10, 2026
Answered using GPT-5.6 Thinking by Chat01
可以,而且按 MineColonies 目前的代码结构,这个功能实际上只需要改 RestaurantMenuModuleWindow,JEI 那边基本不用再写新的适配。
当前餐厅“食物”页面对应:
textsrc/main/java/com/minecolonies/core/client/gui/modules/building/ RestaurantMenuModuleWindow.java
这个窗口里已经有两个你关心的列表:
menuList:已经加入餐厅菜单的食物resourceList:可以添加到菜单的食物两边每一行都已经把对应的 ItemStack 塞进了 ItemIcon。(GitHub)
更重要的是,MineColonies 已经存在现成的 JEI 接口:
javaCompatibility.jeiProxy.showRecipes(Collection<ItemStack>)
它内部已经使用 RecipeIngredientRole.OUTPUT 打开 JEI,所以语义正好就是“这个物品怎么制作”。(GitHub)
在:
javaRestaurantMenuModuleWindow.java
增加两个 import:
javaimport com.minecolonies.api.compatibility.Compatibility; import net.minecraft.client.gui.screens.Screen;
然后在这个类中加入下面两个方法:
java@Override public boolean click(final double mx, final double my) { // 防止拿到上一次点击留下的 Pane lastClickedPane = null; // 先让 BlockUI 正常处理这次点击。 // 点击 ItemIcon 时,它会把自己记录到 lastClickedPane。 final boolean handled = super.click(mx, my); // 只处理 Ctrl + 左键 if (!Screen.hasControlDown()) { return handled; } // 只处理物品图标 if (!(lastClickedPane instanceof final ItemIcon itemIcon)) { return handled; } // 只允许餐厅的“已选食物”和“可选食物”两个列表, // 不处理下面的 ingredientslist。 if (!isRestaurantFoodIcon(itemIcon)) { return handled; } final ItemStack stack = itemIcon.getItem(); if (stack == null || stack.isEmpty()) { return handled; } // MineColonies 已经封装好的 JEI 接口: // 按 OUTPUT 查询,也就是“如何制作这个物品” if (Compatibility.jeiProxy.showRecipes(List.of(stack.copy()))) { return true; } return handled; } /** * 判断点击的 ItemIcon 是否属于餐厅食物列表。 */ private boolean isRestaurantFoodIcon(final Pane pane) { for (Pane current = pane; current != null; current = current.getParent()) { if (current == menuList || current == resourceList) { return true; } } return false; }
因为这个文件本身已经:
javaimport java.util.*;
所以 List.of(...) 不需要额外 import。
这跟 BlockUI 的实现有关。
BlockUI 的所有 GUI 元素最终都继承自 Pane。Pane 中本身存在:
javaprotected static Pane lastClickedPane;
而且每一个 Pane 收到鼠标点击时都会执行:
javapublic boolean click(final double mx, final double my) { setLastClickedPane(this); return handleClick(mx - x, my - y); }
因此点击餐厅中的物品图标时,最后真正收到点击的是那个 ItemIcon,于是:
javalastClickedPane instanceof ItemIcon
就能拿到它。(GitHub)
ItemIcon 本身又已经保存了对应的 ItemStack,并提供:
javapublic ItemStack getItem()
所以直接:
javafinal ItemStack stack = itemIcon.getItem();
就取得当前鼠标点击的食物。(GitHub)
整个流程就是:
textCtrl + 左键 ↓ RestaurantMenuModuleWindow.click() ↓ BlockUI View.click() ↓ 找到鼠标下最深层 Pane ↓ ItemIcon.click() ↓ lastClickedPane = 当前 ItemIcon ↓ itemIcon.getItem() ↓ ItemStack ↓ Compatibility.jeiProxy.showRecipes(...) ↓ JEI 配方界面
例如你可能原本想这么写:
javaimport mezz.jei.api.recipe.RecipeIngredientRole; import mezz.jei.api.constants.VanillaTypes; import mezz.jei.api.runtime.IJeiRuntime;
然后自己调用:
javajei.getRecipesGui().show(...);
这里其实不推荐。
原因是 MineColonies 把 JEI 作为可选兼容处理,已经专门设计了:
javaIJeiProxy
默认实现:
javapublic interface IJeiProxy { default boolean isLoaded() { return false; } default boolean showRecipes(final Collection<ItemStack> stacks) { return false; } }
而:
javaCompatibility.jeiProxy
默认就是一个空实现;JEI 真正加载之后,MineColonies 的 JEIPlugin 才把它替换成真正的 JEI 实现。(GitHub)
所以你写:
javaCompatibility.jeiProxy.showRecipes(...)
有两个好处:
text安装 JEI ↓ 打开 JEI 配方 没安装 JEI ↓ 返回 false ↓ 什么都不发生 ↓ 不会因为餐厅 GUI 直接引用 JEI 类而出问题
这就是这个项目原本设计好的兼容层。
当前 JEIPlugin 中对应实现实际上就是:
java@Override public boolean showRecipes(final Collection<ItemStack> stacks) { final IJeiRuntime jei = JEIPlugin.this.jei; if (jei != null && !stacks.isEmpty()) { final IFocusFactory focusFactory = jei.getJeiHelpers().getFocusFactory(); final List<IFocus<?>> focuses = stacks.stream() .<IFocus<?>>map(stack -> focusFactory.createFocus( RecipeIngredientRole.OUTPUT, VanillaTypes.ITEM_STACK, stack)) .toList(); jei.getRecipesGui().show(focuses); return true; } return false; }
也就是说你提出的功能:
Ctrl + 鼠标左键一个食物 → 打开 JEI → 查看这个食物的做法
JEI 部分实际上已经 100% 存在。(GitHub)
你现在缺的只是:
餐厅界面 → 捕获 Ctrl+左键 → 把当前 ItemStack 传给
showRecipes()
JEI 的 focus 有不同角色。
这里 MineColonies 已经使用:
javaRecipeIngredientRole.OUTPUT
意义是:
text我点击:面包 OUTPUT focus ↓ 查找“输出面包”的配方 ↓ 小麦 → 面包
这正符合“这个食物怎么做”。
反过来,如果使用:
javaRecipeIngredientRole.INPUT
语义就会变成:
text这个物品能拿来做什么?
例如点小麦可能显示:
text小麦 → 面包 小麦 → 蛋糕 小麦 → ...
JEI 官方 API 的 focus factory 就是通过 RecipeIngredientRole + ingredient type + ingredient 创建这种查询。(GitHub)
isRestaurantFoodIcon() 为什么有必要这是一个容易踩坑的地方。
RestaurantMenuModuleWindow 里除了:
javamenuList resourceList
还有一个:
javaingredientslist
而它们里面的物品图标都使用 ItemIcon。当前代码里,已加入菜单的食物通过:
javarowPane.findPaneOfTypeByID( RESOURCE_ICON, ItemIcon.class ).setItem(resource);
赋值;右边的可选择食物也是同样做法。下方原材料统计也同样放了 ItemIcon。(GitHub)
所以如果你只判断:
javalastClickedPane instanceof ItemIcon
结果就会变成:
textCtrl + 点击食物 → JEI Ctrl + 点击食材统计 → JEI
实际上这个功能也不坏,甚至可能更方便。
那代码还能再简单一点,直接删除:
javaif (!isRestaurantFoodIcon(itemIcon)) { return handled; }
以及整个:
javaprivate boolean isRestaurantFoodIcon(...)
这样餐厅页面的:
全部支持 Ctrl + 左键 → JEI。
我反而比较推荐这种行为。
因为下面的食材列表本来就是告诉玩家:
餐厅大概需要这些原材料
此时 Ctrl+点胡萝卜、面包、调味品继续查它的配方,非常自然。
如果让我实际给 MineColonies 提这个 PR,我第一版会保持非常小:
javaimport com.minecolonies.api.compatibility.Compatibility; import net.minecraft.client.gui.screens.Screen;
然后:
java@Override public boolean click(final double mx, final double my) { lastClickedPane = null; final boolean handled = super.click(mx, my); if (!Screen.hasControlDown() || !(lastClickedPane instanceof final ItemIcon itemIcon)) { return handled; } final ItemStack stack = itemIcon.getItem(); if (stack == null || stack.isEmpty()) { return handled; } return Compatibility.jeiProxy.showRecipes(List.of(stack.copy())) || handled; }
这只有十几行核心代码。
效果就是:
text正常左键食物 → 保持 MineColonies 原行为 Ctrl + 左键食物图标 → JEI:显示该食物的制作配方 Ctrl + 左键食材图标 → JEI:显示该食材的制作配方 没安装 JEI → 不打开任何东西,也不会崩
而且不需要修改 JEIPlugin、不需要新建 JEI Category,也不需要自己保存 IJeiRuntime,因为 MineColonies 现在已经把这些工作做好了。(GitHub)
这个简单版本验证成功后,其实可以把它抽象成 MineColonies 全局功能:
text所有 MineColonies GUI 中的 ItemIcon Ctrl + 左键 → 查看 recipes / how to make Ctrl + 右键 → 查看 uses / what can this make
甚至不用每一个建筑窗口重复写。
例如:
text餐厅食物 仓库最低库存 建造请求 工人请求物品 铁匠/木匠配方 大学研究材料 快递员请求 建筑所需资源
都可以统一接到 Compatibility.jeiProxy。
不过第一步我建议只改 RestaurantMenuModuleWindow,先验证 BlockUI → ItemIcon → ItemStack → JEI 整条链路,这是最小、最好调试、也最适合作为第一个 commit 的实现。当前 BlockUI 的点击分发和 lastClickedPane 机制支持这种做法。(GitHub)
如果你是在直接修改 MineColonies 源码/准备给官方提交 PR,上面这版就非常适合;如果你实际想做的是一个独立 addon mod,不修改 MineColonies 本体,实现方式会改成 Mixin/GUI event 注入,我可以直接按你的 Minecraft 版本(1.20.1 或 1.21.1)+ Forge/NeoForge 给你写出完整的 addon 工程结构和 Mixin 代码。
上述实现可以使用kubejs吗?兼容minecolonies-1.20.1-1.1.1197版本
可以。minecolonies-1.20.1-1.1.1197 可以用 KubeJS 实现这个功能,但不能把你上面的 Java @Override click() 原样翻译成 KubeJS。更合适的方式是:
KubeJS startup_scripts → Forge GUI 鼠标事件 → 读取 BlockUI 的 lastClickedPane → ItemIcon.getItem() → MineColonies 的 Compatibility.jeiProxy.showRecipes()。
我核对的是你指定的 1.1.1197 源码,不是现在的 version/main。这个版本确实是 Forge 1.20.1,要求 BlockUI 1.20.1-1.0.190-snapshot 或以上,并把 JEI 15.1.0.19+ 列为可选依赖。(CurseForge)
1.1.1197 的 RestaurantMenuModuleWindow 确实仍然把餐厅食物放进 BlockUI 的 ItemIcon:
javarowPane.findPaneOfTypeByID(RESOURCE_ICON, ItemIcon.class) .setItem(resource);
(GitHub)
而 ItemIcon 有公开的:
javapublic ItemStack getItem()
(GitHub)
同时 1.1.1197 的 Compatibility.jeiProxy 是 public static,并且 showRecipes(Collection<ItemStack>) 已经存在。JEI 插件的实际实现也确实使用 RecipeIngredientRole.OUTPUT 后调用 JEI Recipes GUI,因此它就是“查看这个物品怎么制作”。(GitHub)
问题只有一个:BlockUI 的
javaprotected static Pane lastClickedPane;
没有公开 getter。Pane.click() 会更新它,但 setter 又是 private,所以 KubeJS 需要用一点 Java reflection 来读取这个字段。BlockUI 1.20.1 的 1.0.193 源码中这个结构是明确存在的。(GitHub)
建议放在:
textkubejs/startup_scripts/minecolonies_restaurant_jei.js
这是客户端功能。如果你有独立服务器,不要把这个客户端 GUI 脚本部署到 dedicated server。
js// kubejs/startup_scripts/minecolonies_restaurant_jei.js // // Minecraft 1.20.1 Forge // MineColonies 1.1.1197 // KubeJS 2001.x // // Ctrl + 左键 MineColonies 餐厅里的 ItemIcon // -> 打开 JEI,查看该物品的制作配方 const $Screen = Java.loadClass( 'net.minecraft.client.gui.screens.Screen' ) const $Compatibility = Java.loadClass( 'com.minecolonies.api.compatibility.Compatibility' ) const $Collections = Java.loadClass( 'java.util.Collections' ) const $Pane = Java.loadClass( 'com.ldtteam.blockui.Pane' ) // ------------------------------------------------------- // 取得 Pane.lastClickedPane // // BlockUI 中这个字段是 protected static,没有公开 getter, // 所以这里通过 reflection 读取。 // ------------------------------------------------------- const lastClickedPaneField = $Pane.class.getDeclaredField('lastClickedPane') lastClickedPaneField.setAccessible(true) // ------------------------------------------------------- // Ctrl + 左键之前清空 lastClickedPane // // 对应你 Java 版本中的: // // lastClickedPane = null; // // 防止极端情况下读取到上一次点击留下来的 Pane。 // ------------------------------------------------------- ForgeEvents.onEvent( 'net.minecraftforge.client.event.ScreenEvent$MouseButtonPressed$Pre', event => { if (event.getButton() != 0) { return } if (!$Screen.hasControlDown()) { return } lastClickedPaneField.set(null, null) } ) // ------------------------------------------------------- // 点击完成之后读取 BlockUI 最终点到的 Pane // ------------------------------------------------------- ForgeEvents.onEvent( 'net.minecraftforge.client.event.ScreenEvent$MouseButtonPressed$Post', event => { // 只处理 Ctrl + 左键 if (event.getButton() != 0) { return } if (!$Screen.hasControlDown()) { return } // BlockUI 在处理 click() 时已经更新 lastClickedPane const pane = lastClickedPaneField.get(null) if (pane == null) { return } // 只处理 ItemIcon // // 不需要直接 import ItemIcon, // 用类名判断可以减少脚本侧的 Java 类型处理。 if ( String(pane.getClass().getName()) != 'com.ldtteam.blockui.controls.ItemIcon' ) { return } // --------------------------------------------------- // 限制为 RestaurantMenuModuleWindow // // 否则所有 MineColonies BlockUI ItemIcon // 都会获得 Ctrl+左键 -> JEI 功能。 // --------------------------------------------------- const window = pane.getWindow() if (window == null) { return } if ( String(window.getClass().getName()) != 'com.minecolonies.core.client.gui.modules.building.RestaurantMenuModuleWindow' ) { return } // 取得 ItemStack const stack = pane.getItem() if (stack == null || stack.isEmpty()) { return } // --------------------------------------------------- // 调 MineColonies 自己的 JEI compatibility layer // // 不直接引用 mezz.jei.* // 没有 JEI 时代理返回 false。 // --------------------------------------------------- $Compatibility.jeiProxy.showRecipes( $Collections.singletonList(stack.copy()) ) } )
KubeJS 1.20.1 Forge 的 ForgeEvents 正好支持这么做:它只在 startup_scripts 环境注册,并允许通过 Forge Event 类名字符串注册 listener;而且这种 listener 修改后不能热重载,需要重启游戏。(GitHub)
这里选 MouseButtonPressed.Post 也不是偶然。Forge 1.20.x 明确规定 Pre 在 Screen.mouseClicked() 之前触发,Post 在正常鼠标处理之后触发。这样到 Post 阶段时 BlockUI 已经执行过 Pane.click(),正好可以读取它刚刚写入的 lastClickedPane。(GitHub)
你的 Java 版本:
textRestaurantMenuModuleWindow.click() ↓ super.click() ↓ lastClickedPane ↓ JEI
KubeJS 版本则是:
textForge MouseButtonPressed.Pre ↓ 清空 lastClickedPane Minecraft / BlockUI 正常处理点击 ↓ Pane.click() ↓ lastClickedPane = ItemIcon Forge MouseButtonPressed.Post ↓ 反射读取 lastClickedPane ↓ pane.getItem() ↓ Compatibility.jeiProxy.showRecipes() ↓ JEI
所以功能上可以做到几乎完全一样,而且不需要修改 MineColonies jar,也不需要 Mixin。
有一个需要特别强调的地方:这里最“脆弱”的不是 MineColonies 的 JEI API,而是:
jsgetDeclaredField('lastClickedPane')
因为这是在访问 BlockUI 的内部实现。你指定的 MineColonies 1.1.1197 要求 BlockUI 1.0.190-snapshot+;我同时核对了 BlockUI 1.20.1 的 1.0.193,这个字段、Pane.click()、getWindow() 都仍然是上述结构。(CurseForge)
如果你的整合包固定 MineColonies 1.1.1197 和对应的 BlockUI 版本,我认为 KubeJS 方案很合适。 如果以后经常升级 BlockUI,那么一个 10~20 行的 Mixin/accessor addon 会比反射更稳。
另外,上面的脚本目前按照你前面更推荐的行为处理:餐厅窗口中的所有 ItemIcon 都支持 Ctrl+左键,因此已选食物、可选食物、下方食材图标都会打开 JEI。如果你只想允许 menuList + resourceList,也可以继续在 KubeJS 里沿 pane.getParent() 向上检查两个列表,我可以直接给你改成那个严格版本。