包:com.huidu.farmersdelight.api.gui
这个包是容器 GUI 背后的共享引擎:查看者一侧是字符网格布局,工作站一侧是扁平存储。配方书是同一套形状 —— RecipeBookLayout 继承 GuiLayout —— 所以一个格子在两边含义一致。
本页涉及的类型:
| 类型 | 形态 | @ApiStatus |
|---|---|---|
GuiLayout | 接口,只读视图 | @OverrideOnly |
GuiLayouts | 静态门面,私有构造 | — |
GuiSlotGroup | record | — |
GuiWriteBack | 静态门面,私有构造 | — |
ContainerGuiKernel | final,引擎 | — |
ContainerGuiKernel.Controller | 工作站实现的接口 | — |
GuiItems | 静态门面,装饰物品构建器 | — |
@ApiStatus.OverrideOnly
public interface GuiLayout {
String BACKGROUND = "background";
int rows();
default int size(); // rows() * 9
default boolean contains(int rawSlot); // 0 <= rawSlot < size()
boolean isFunctional(int slot);
default int[] functionalSlots(); // 升序,新数组
int[] slotsOf(String type); // 升序,新数组;未知或 null 时为空数组
default int firstSlotOf(String type); // 没有则 -1
}
legend 类型为 background 的格子 —— 以及网格之外的格子 —— 是装饰;其余格子都是功能格,也就是属于布局内容、可以被寻址的格子。
功能格不等于可写回。 工作站上那些只用于显示的格子(进度条、输出槽、成品预览)在这里是功能格,但 GUI 仍然绝不能把查看者的值写回去。isFunctional 回答的是「这个格子装内容吗」,不是「查看者能改它吗」;写回判定属于 GuiWriteBack / Controller.mayCommit。
之所以把 isFunctional 作为原始方法,是因为各实现的内部结构不同 —— 工作站读取器从图标表回答,解析出来的视图扫描预先算好的格子数组。functionalSlots、slotsOf、firstSlotOf 都是派生的,并且每次返回新数组,所以调用方可以留存、排序或修改拿到的东西而不影响布局。热路径应当在每 tick 之外调用一次 functionalSlots() 并缓存。传入未知或 null 类型返回空数组,绝不返回 null。
GuiLayouts 是 gui.yml 布局读取器的通用那一半,不碰 CraftEngine、不碰服务端:它只读 rows、layout 与 legend,诊断也只是纯数据变换。
public static GuiLayout parse(ConfigurationSection section); // section 为 null 时返回 null
public static String[] cellTypes(int rows, List<String> layout, Map<Character, String> legend);
public static int warnUnknownCharacters(Logger logger, String configPath, int rows,
List<String> layout, Map<Character, String> legend);
parse 读取 rows(默认 3,至少 1)、layout 与 legend(单字符键),返回的视图只回答 GuiLayout —— 标题、物品以及工作站专属的槽位角色仍留在调用方自己的配置类里。
cellTypes 返回归一化后的网格:rows * 9 项、按槽位顺序、永不为 null,而且每次调用都是新数组。任何「不是画出来的、且 legend 有可用条目的格子」都读作 background:
null 的行;null 或空白。超出 rows 的行与超出第九列的字符会被忽略;rows 小于 1 得到空数组。
GuiConfig.getSlotType 的一处不一致通过自己的配置类读同一张网格的工作站还有一份字面查询。两者是有意不同的:
GuiConfig.getSlotType(slot) 对网格没有画出的格子(缺失的行,或超出该行长度的列)返回 null,并且原样返回 legend 的值 —— 所以写得没有值的 legend 键会保持 null;GuiLayouts.cellTypes / parse 会归一化:同样的格子与条目都变成 background。两者在「画出来的、legend 条目为非空白类型」的每个格子上都一致;只在 legend 把画出的字符映射到 null 或空白字符串的地方不同。也正因如此,配方书自己的原始方法 RecipeBookLayout.slotType(int) 采用与 cellTypes 相同的归一化,三者随后对每个画出的格子口径一致。
记录三类网格问题并返回记录了几条:
rows 不一致;它是自包含的:接收 logger 与配置路径,而不是去抓插件,所以附属可以用自己的 logger 上报,不涉及静态查找也不涉及语言键。null 的 logger 或 layout 什么都不记,空白字符不是问题,路径为空白或 null 时报作 gui.yml。
public record GuiSlotGroup(String type, int storeStart, int count, @Nullable String iconType) {
public int storeIndex(int offset); // storeStart + offset
}
容器 GUI 一侧是配置出来的格子,另一侧是扁平的存储值数组;这个 record 就是两者的衔接。type 选出格子(layout.slotsOf(type)),storeStart 是第一个格子对应的存储下标,count 限制其中有多少个承载值 —— 布局画出的同类型格子多于存储容量的部分,就是纯装饰格。
iconType 指定「存储值为空时画进该格的占位物」,当空格子直接清空时为 null。物品本身来自 controller(Controller.icon(iconType)),因为只有工作站知道它的占位物从哪来。
每个容器 GUI 都给查看者独立的快照库存;ticker 或其他查看者可能在视图打开期间修改权威库存,所以写回前必须校验绘制基线。盲目提交会把工作站已经花掉的物品还回来,或者把期间收到的物品抹掉 —— 物品不是变成两份,就是凭空消失。
public static boolean mayCommit(@Nullable ItemStack stored, @Nullable ItemStack paintedBaseline);
public static boolean mayCommit(boolean storedEmpty, boolean paintedEmpty, boolean similar,
int storedAmount, int paintedAmount);
这条规则是写回策略的唯一来源:
null 与空气是同一件事(空格子);isSimilar 且数量相等)→ 允许提交;拒绝不是「保留查看者手里那份」。 返回 false 意味着调用方必须把这个格子按存储重绘;静默保留 GUI 里的值,正是这道守卫要防的复制或丢失。内核已经替你完成了这次重绘。
比较是纯数据的:不读世界、不克隆、任何线程都能跑。只接收值的那个重载是为了让这条规则在没有运行服务端时也能被断言。
public ContainerGuiKernel(Controller controller, GuiSlotGroup... groups);
public boolean hasLayout();
public boolean fill(Inventory view);
public boolean refreshAll(Inventory view);
public boolean refreshAll(Inventory view, IntPredicate skipPending);
public boolean refreshSlot(Inventory view, int rawSlot);
public boolean syncAll(Inventory view);
public boolean syncSlot(Inventory view, int rawSlot);
public int[] commitOnly(Inventory view);
public boolean commitOnly(Inventory view, int rawSlot);
public void repaintFrom(Inventory view, int[] rawSlots, IntFunction<ItemStack> storedByIndex);
public int storeIndex(int rawSlot); // 该槽不装内容时返回 -1
内核负责每个容器 GUI 都要重复的那部分:把配置格子映射到存储下标、按每个 view 保存独立的绘制基线副本,成功提交后推进该 view 的基线、拒绝过期写回并把被拒的格子按存储重绘、以及「编辑仍在途中」的跳过规则。
fill 先给布局不使用的每个格子画背景,再画各组存储值(空格子画占位物)。refreshAll 把所有配置格子按存储重绘;refreshAll(view, skipPending) 跳过 skipPending 接受的格子 —— 编辑仍在途中的格子拥有玩家即将取走的那份值,按存储重绘会让物品显示两次并被发放两次。refreshSlot 重绘一个配置格子,空格子恢复占位物;不是配置格子的槽位保持不动。syncAll 提交守卫允许的每个配置格子,重绘它拒绝的格子,并在之后调用一次 markDirty()。syncSlot 是单格版本,用于原版延迟的点击与拖拽同步。守卫拒绝的格子按存储重绘;不是配置格子的槽位保持不动(并返回 true)。false,调用方可以退回硬编码网格;此时 storeIndex 返回 -1。public interface Controller {
@Nullable GuiLayout layout(); // null = 所有引擎方法都报告「什么都没做」
int slotCount();
@Nullable ItemStack stored(int storeIndex); // 权威值;不得记录任何东西
void store(int storeIndex, @Nullable ItemStack item);
Object storeLock(); // 真正的监视器对象,绝不为 null
void markDirty();
default @Nullable ItemStack icon(@Nullable String iconType); // null
default @Nullable ItemStack backdrop(); // null
default boolean isPlaceholder(ItemStack stack, @Nullable String iconType); // false
default boolean mayCommit(@Nullable ItemStack stored, @Nullable ItemStack paintedBaseline);
}
stored 必须是纯读取。每次绘制内核都会调用它,提交时还会再调用一次;在那里记录东西(一个「上次所见」字段、一个脏标记)等于让绘制变成对存储的修改。store 负责克隆与归一化:它收到的可能是格子里原来的任何东西,包括占位物位置传来的 null,必须克隆自己保留的内容,并且存 null 而不是空 stack。存储必须保留副本,不能与 view 的可变 stack 共享引用。storeLock() 真的会被使用:内核在每次访问存储以及读写每格绘制记录时都会获取它。它必须是真正的对象、绝不为 null,而且是可重入的 —— 常见调用形态(点击处理器把整段读-改-写包在同一把锁里)不受影响。markDirty() 在提交改变了存储时被调用,好让工作站把方块实体标脏。内核每次 syncAll 调用一次,syncSlot 只在真正提交时调用。mayCommit 默认走 GuiWriteBack.mayCommit。只给提交策略确实不同的工作站(例如带符号的增量)才覆盖它 —— 更弱的覆盖正是一个容器开始复制物品的方式。这是 Folia 上不能跳过的一段。
fill、refreshAll、refreshSlot 会读取存储并写实时 view;syncAll、syncSlot 和 commitOnly 会读取实时 view 并访问存储。调用线程必须同时拥有查看者和存储的区域。commitOnly 只省略回绘,仍会调用 view.getItem;不能直接把它派发到另一区域。repaintFrom 使用已复制的存储快照,只在查看者 EntityScheduler 上执行;须确认仍打开原 view,并使用同一个持久 kernel 记录基线。storeLock() 只保护插件数据,不授予 Folia 归属权限。跨区域编辑需由调用方实现完整事务,或在交互前拒绝并关闭视图。FD 厨锅和 CAB 容器采用后者;已接受的点击同步持久化,取消的拖拽在下一 tick 校验 view、归属和光标后才提交。GuiLayout layout = GuiLayouts.parse(section); // rows / layout / legend
if (layout != null) {
ContainerGuiKernel kernel = new ContainerGuiKernel(controller,
new GuiSlotGroup("bait", 0, 1, "bait_icon"),
new GuiSlotGroup("catch", 1, 3, null));
kernel.fill(view); // 同时拥有查看者与存储的区域
// ... 之后,在同一归属区域内,持有 storeLock() 完成读-改-写:
kernel.syncSlot(view, rawSlot);
}
GuiItems.build(section) / GuiItems.build(section, placeholders) 按 FarmersDelight 自己 GUI 使用的同一套 items: 段形状构建装饰物品(物品 id、material、custom-model-data、item-model、name / lore 走 MiniMessage 与字形标签,以及可翻译的 name-key / lore-keys 对),所以附属的装饰也能像家族里其它文本一样本地化。
本页转载自 FarmersDelight Wiki,按 GPL-3.0 许可分发。正文未改动,仅对链接与排版做了适配。