可以,而且按 MineColonies 目前的代码结构,这个功能实际上只需要改 **`Restaur...

Erstellt am: 10. August 2026

Beantwortet mit GPT-5.6 Thinking von Chat01

Frage

可以,而且按 MineColonies 目前的代码结构,这个功能实际上只需要改 RestaurantMenuModuleWindow,JEI 那边基本不用再写新的适配。

当前餐厅“食物”页面对应:

text
src/main/java/com/minecolonies/core/client/gui/modules/building/ RestaurantMenuModuleWindow.java

这个窗口里已经有两个你关心的列表:

  • menuList:已经加入餐厅菜单的食物
  • resourceList:可以添加到菜单的食物

两边每一行都已经把对应的 ItemStack 塞进了 ItemIcon。(GitHub)

更重要的是,MineColonies 已经存在现成的 JEI 接口

java
Compatibility.jeiProxy.showRecipes(Collection<ItemStack>)

它内部已经使用 RecipeIngredientRole.OUTPUT 打开 JEI,所以语义正好就是“这个物品怎么制作”。(GitHub)


1. 最小实现方案

在:

java
RestaurantMenuModuleWindow.java

增加两个 import:

java
import 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; }

因为这个文件本身已经:

java
import java.util.*;

所以 List.of(...) 不需要额外 import。


2. 为什么这个方法能知道你点的是哪个物品?

这跟 BlockUI 的实现有关。

BlockUI 的所有 GUI 元素最终都继承自 PanePane 中本身存在:

java
protected static Pane lastClickedPane;

而且每一个 Pane 收到鼠标点击时都会执行:

java
public boolean click(final double mx, final double my) { setLastClickedPane(this); return handleClick(mx - x, my - y); }

因此点击餐厅中的物品图标时,最后真正收到点击的是那个 ItemIcon,于是:

java
lastClickedPane instanceof ItemIcon

就能拿到它。(GitHub)

ItemIcon 本身又已经保存了对应的 ItemStack,并提供:

java
public ItemStack getItem()

所以直接:

java
final ItemStack stack = itemIcon.getItem();

就取得当前鼠标点击的食物。(GitHub)

整个流程就是:

text
Ctrl + 左键 RestaurantMenuModuleWindow.click() BlockUI View.click() 找到鼠标下最深层 Pane ItemIcon.click() lastClickedPane = 当前 ItemIcon itemIcon.getItem() ItemStack Compatibility.jeiProxy.showRecipes(...) JEI 配方界面

3. 为什么不要在这里直接 import JEI

例如你可能原本想这么写:

java
import mezz.jei.api.recipe.RecipeIngredientRole; import mezz.jei.api.constants.VanillaTypes; import mezz.jei.api.runtime.IJeiRuntime;

然后自己调用:

java
jei.getRecipesGui().show(...);

这里其实不推荐

原因是 MineColonies 把 JEI 作为可选兼容处理,已经专门设计了:

java
IJeiProxy

默认实现:

java
public interface IJeiProxy { default boolean isLoaded() { return false; } default boolean showRecipes(final Collection<ItemStack> stacks) { return false; } }

而:

java
Compatibility.jeiProxy

默认就是一个空实现;JEI 真正加载之后,MineColonies 的 JEIPlugin 才把它替换成真正的 JEI 实现。(GitHub)

所以你写:

java
Compatibility.jeiProxy.showRecipes(...)

有两个好处:

text
安装 JEI 打开 JEI 配方 没安装 JEI 返回 false 什么都不发生 不会因为餐厅 GUI 直接引用 JEI 类而出问题

这就是这个项目原本设计好的兼容层。


4. MineColonies 甚至已经替你写好了真正的 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()


5. 为什么用 OUTPUT,而不是 INPUT

JEI 的 focus 有不同角色。

这里 MineColonies 已经使用:

java
RecipeIngredientRole.OUTPUT

意义是:

text
我点击:面包 OUTPUT focus 查找“输出面包”的配方 小麦 → 面包

这正符合“这个食物怎么做”。

反过来,如果使用:

java
RecipeIngredientRole.INPUT

语义就会变成:

text
这个物品能拿来做什么?

例如点小麦可能显示:

text
小麦 → 面包 小麦 → 蛋糕 小麦 → ...

JEI 官方 API 的 focus factory 就是通过 RecipeIngredientRole + ingredient type + ingredient 创建这种查询。(GitHub)


6. isRestaurantFoodIcon() 为什么有必要

这是一个容易踩坑的地方。

RestaurantMenuModuleWindow 里除了:

java
menuList resourceList

还有一个:

java
ingredientslist

而它们里面的物品图标都使用 ItemIcon。当前代码里,已加入菜单的食物通过:

java
rowPane.findPaneOfTypeByID( RESOURCE_ICON, ItemIcon.class ).setItem(resource);

赋值;右边的可选择食物也是同样做法。下方原材料统计也同样放了 ItemIcon。(GitHub)

所以如果你只判断:

java
lastClickedPane instanceof ItemIcon

结果就会变成:

text
Ctrl + 点击食物 → JEI Ctrl + 点击食材统计 → JEI

实际上这个功能也不坏,甚至可能更方便。

如果你希望所有物品图标都支持 JEI

那代码还能再简单一点,直接删除:

java
if (!isRestaurantFoodIcon(itemIcon)) { return handled; }

以及整个:

java
private boolean isRestaurantFoodIcon(...)

这样餐厅页面的:

  • 已选食物
  • 可选食物
  • 食材需求统计

全部支持 Ctrl + 左键 → JEI

我反而比较推荐这种行为。

因为下面的食材列表本来就是告诉玩家:

餐厅大概需要这些原材料

此时 Ctrl+点胡萝卜、面包、调味品继续查它的配方,非常自然。


7. 我建议最终先写成这个版本

如果让我实际给 MineColonies 提这个 PR,我第一版会保持非常小:

java
import 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)


8. 后续可以继续做成更完整的 JEI 交互

这个简单版本验证成功后,其实可以把它抽象成 MineColonies 全局功能:

text
所有 MineColonies GUI 中的 ItemIcon Ctrl + 左键 → 查看 recipes / how to make Ctrl + 右键 → 查看 uses / what can this make

甚至不用每一个建筑窗口重复写。

例如:

text
餐厅食物 仓库最低库存 建造请求 工人请求物品 铁匠/木匠配方 大学研究材料 快递员请求 建筑所需资源

都可以统一接到 Compatibility.jeiProxy

不过第一步我建议只改 RestaurantMenuModuleWindow,先验证 BlockUI → ItemIconItemStack → 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版本

Gedanke

Searched 1 website

Worked for 4m 39s

Antwort

可以。minecolonies-1.20.1-1.1.1197 可以用 KubeJS 实现这个功能,但不能把你上面的 Java @Override click() 原样翻译成 KubeJS。更合适的方式是:

KubeJS startup_scripts → Forge GUI 鼠标事件 → 读取 BlockUI 的 lastClickedPaneItemIcon.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)

为什么 KubeJS 能做

1.1.1197RestaurantMenuModuleWindow 确实仍然把餐厅食物放进 BlockUI 的 ItemIcon

java
rowPane.findPaneOfTypeByID(RESOURCE_ICON, ItemIcon.class) .setItem(resource);

(GitHub)

ItemIcon 有公开的:

java
public ItemStack getItem()

(GitHub)

同时 1.1.1197Compatibility.jeiProxypublic static,并且 showRecipes(Collection<ItemStack>) 已经存在。JEI 插件的实际实现也确实使用 RecipeIngredientRole.OUTPUT 后调用 JEI Recipes GUI,因此它就是“查看这个物品怎么制作”。(GitHub)

问题只有一个:BlockUI 的

java
protected static Pane lastClickedPane;

没有公开 getterPane.click() 会更新它,但 setter 又是 private,所以 KubeJS 需要用一点 Java reflection 来读取这个字段。BlockUI 1.20.1 的 1.0.193 源码中这个结构是明确存在的。(GitHub)

KubeJS 实现

建议放在:

text
kubejs/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 明确规定 PreScreen.mouseClicked() 之前触发,Post 在正常鼠标处理之后触发。这样到 Post 阶段时 BlockUI 已经执行过 Pane.click(),正好可以读取它刚刚写入的 lastClickedPane。(GitHub)

这个版本与修改 MineColonies Java 源码的区别

你的 Java 版本:

text
RestaurantMenuModuleWindow.click() super.click() lastClickedPane JEI

KubeJS 版本则是:

text
Forge 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,而是:

js
getDeclaredField('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() 向上检查两个列表,我可以直接给你改成那个严格版本。

Diesen Q&A teilen