Repository navigation
GUI API 指南
English | 简体中文
KineticCore 的客户端 GUI API 让附属不必直接接触原版 Screen、GuiGraphics 和输入回调就能制作界面。附属只描述页面,由 KineticCore 承载,并负责画布缩放、4K 与 GUI 缩放、裁剪、提示框、焦点、右键菜单、对话框、草稿、层级、滚动条和输入分发。同一份附属 GUI 代码可以运行在 1.20.1、1.21.1 和 26.1.2 上:各 Minecraft 版本在渲染和输入上的变化都由 KineticCore 内部处理。
除特别说明外,所有类型都在 dev.xyat.kineticcore.api.client.gui 下。
- 不要 import
dev.xyat.kineticcore.internal.*:它不属于 API,随时可能变化。 - GUI 类继承
KineticPage、KineticContainerPage、KineticHudEditorPage、KineticCustomControl或KineticRowList,不要继承Screen、AbstractContainerScreen或任何原版控件。 - 页面代码不接收
GuiGraphics、Font、PoseStack或原始的 GLFW 按键、鼠标按钮数字,也不把ResourceLocation当作贴图句柄。改用KineticGraphics、KineticText、KineticTexture和输入记录。把ResourceLocation当作业务或注册表 ID(配方类型、物品 ID、配置键)没有问题。 - 玩家可见的文字一律通过
KineticI18n.translatable(...),语言键同时写进zh_cn和en_us。不要使用Component.translatable(...)。 - 只有 Mixin 例外:
mixin包中的代码可以使用原版GuiGraphics,因为它运行在原版方法内部。但同样不能 importinternal,颜色从KineticTheme.current()获取。
构建可以强制检查这些规则,见 §17。
public final class MyPage extends KineticPage {
private boolean enabled;
public MyPage() {
super(KineticI18n.translatable("gui.mymod.my_page.title"));
setPausesGame(false);
}
@Override
protected void build(KineticUi ui) { // 打开、调整窗口大小、从子页面返回和 rebuild() 时运行
ui.toggle(20, 40, 120)
.value(enabled)
.labels(KineticI18n.translatable("gui.mymod.on"), KineticI18n.translatable("gui.mymod.off"))
.onChange(v -> enabled = v)
.build();
ui.button(20, 300, 80).text(KineticI18n.translatable("gui.mymod.back")).onClick(this::close).build();
}
@Override
protected void renderBackground(KineticGraphics g, int mouseX, int mouseY, float partialTick) {
KineticTheme.panel(g, 10, 10, width() - 20, height() - 20);
g.centeredText(title(), width() / 2, 18, KineticTheme.current().text(), true);
}
}在游戏代码中用 KineticGui.open(new MyPage()) 打开,在另一个页面中用 openChild(new MyPage()) 打开;关闭子页面会自动回到父页面。
build 必须可以重复执行。 调整窗口大小、从子页面返回以及调用 rebuild() 后它都会再次运行。所有状态都放在字段里,由 build 读取状态来创建控件。保存控件的字段必须在每次 build 中重新赋值。
| 用途 | 成员 |
|---|---|
| 尺寸与标题 |
width()、height()、title()
|
| 绘制 |
renderBackground(g, mx, my, pt)、renderForeground(g, mx, my, pt)(画在控件之上) |
| 提示框 |
renderTooltips(mx, my),在其中调用 showTooltip(c)、showTooltip(list[, maxWidth])、showFormattedTooltip(seq)、showItemTooltip(stack)
|
| 导航 |
openChild(page)、close()(先询问 onCloseRequested)、navigateBack()(跳过询问)、rebuild()
|
| 暂停 | 在构造函数中调用 setPausesGame(boolean);页面默认暂停游戏 |
| 焦点 |
focus(c)、blur(c)、clearFocus()、isFocused(c)、focusedControl()
|
| 覆盖层 |
openContextMenu(x, y, items[, width])、closeContextMenu()、openDialog(title, message, yes, no, onYes, onNo)、overlayBlocksInput()
|
| 草稿(未保存的修改) |
configureDraft、configureStandaloneDraft、reserveStandaloneDraft、commitDraft、discardDraft、hasUnsavedEdits;可以在构造函数中调用 |
| 裁剪 |
g.clipped(l, t, r, b, () -> …) 或 g.scissor(...) / g.endScissor(),使用页面坐标 |
| 实体预览缩放 | registerPreviewZoomArea(KineticEntityPreview, key, x, y, w, h) |
| 布局信息 |
layoutMetrics()、isCompactLayout()、isPortraitLayout()、isUltrawideLayout()
|
在网络处理器中用 KineticGui.currentPage(MyPage.class)(可能为 null)获取已打开的页面,用 KineticGui.closeScreen() 关闭界面。KineticGui.captureNavigationParent() 配合 KineticGui.openChild(page, parent) 可以在服务端回包后再打开页面,返回目标在发送请求时就已记录。
-
PageLayout.CANVAS(默认):最大 640×360 的固定虚拟画布,按窗口缩放。width()/height()返回画布尺寸,所有回调收到的都是画布坐标。在构造函数中用useCanvas(w, h, margin)修改设计尺寸或安全边距。 -
PageLayout.NATIVE(super(title, PageLayout.NATIVE)):使用 GUI 缩放后的屏幕坐标,width()/height()返回缩放后的屏幕尺寸。 -
KineticContainerPage<M>:回调收到 UI 坐标,只有renderScreenOverlay收到屏幕坐标。 - 常量:
KineticPage.CANVAS_WIDTH(640)、CANVAS_HEIGHT(360)、SAFE_MARGIN(6)、CONTROL_HEIGHT(16)、ITEM_BUTTON_HEIGHT(38)、CARD_BUTTON_HEIGHT(26)。 - 所有坐标都由宿主转换,页面自己不做画布与屏幕之间的换算。
-
层级: 支持
.layer(n)的构建器(按钮、开关、标签栏以及所有列表和网格)会把控件画在普通控件之上。要把自己的内容画在物品层之上,在g.isolated(...)中调用g.raise(n)。
输入以 api.client.gui.input 中的不可变记录送达:
| 记录 | 字段与辅助方法 |
|---|---|
MouseInput |
x()、y()、button()(MouseButton:LEFT/RIGHT/MIDDLE/…)、rawButton()、modifiers()、isLeft()、isRight()、isMiddle()、hasShift()、hasControl()、hasAlt()、inside(x, y, w, h)
|
MouseDragInput |
x()、y()、button()、rawButton()、deltaX()、deltaY()
|
ScrollInput |
x()、y()、deltaX()、deltaY()、inside(...)
|
KeyInput |
keyCode()、scanCode()、modifiers()、is(KineticKeyBindings.Key)、isEscape()、isEnter()(包括小键盘回车)、hasShift()、hasControl()、hasAlt()
|
CharInput |
codePoint()、modifiers()、text()
|
页面回调在消费了事件时返回 true:
| 回调 | 何时运行 |
|---|---|
onMouseClickCapture(MouseInput) |
在控件之前;用于必须优先于控件的操作(拖动手柄、关闭弹窗) |
onMouseClick(MouseInput) |
在没有控件消费这次点击之后 |
onMouseRelease(MouseInput) |
总会调用,在控件之前;释放事件仍会送达控件 |
onMouseDrag(MouseDragInput) |
在控件之前;只在你自己的拖动进行中时返回 true
|
onMouseScroll(ScrollInput) |
在控件之后 |
onMouseMove(x, y) |
每次移动 |
onKeyPress、onKeyRelease、onCharTyped
|
在获得焦点的控件之前;不处理的按键返回 false
|
onTick() |
每个客户端 tick |
onRemoved() |
宿主界面被移除时 |
onCloseRequested() |
按 Escape 或调用 close() 时;处理了就返回 true(例如弹出“有未保存的修改”对话框) |
判断按键用 input.isEnter()、input.isEscape() 或 input.is(KineticKeyBindings.Key.X),不要用 GLFW 原始数字。页面之外有些上下文仍会传入原始的 int button(原版或第三方界面事件、KineticClientEvents.MouseButtonContext、Mixin),这时用 KineticMouseButtons.isPrimary/isSecondary/isMiddle(button) 判断,或用 MouseButton.of(button) 转换一次。
控件只能由 build 收到的 KineticUi 创建。所有构建器都支持 tooltip(Component)、tooltip(Supplier<Component>)、enabled(boolean) 和 visible(boolean),最后以 .build() 结束并返回控件。
| 控件 | 构建器 | 返回 |
|---|---|---|
| 按钮 |
ui.button(x, y, w).text(t).onClick(r);.compact()、.card()、.layer(1)
|
KineticButton |
| 物品按钮 | ui.itemButton(x, y, w, stack).text(t).onClick(r) |
KineticItemButton |
| 开关 | ui.toggle(x, y, w).value(b).labels(on, off).validator(v).onChange(c) |
KineticToggle |
| 循环按钮 | ui.cycleButton(x, y, w, options).index(i).onChange(c) |
KineticCycleButton |
| 颜色按钮 / 色块 |
ui.colorButton(x, y, w, rgb).text(t).onClick(r) / ui.colorSwatch(x, y, rgb).onClick(r)
|
KineticColorButton / KineticColorSwatch
|
| 滑块 | ui.slider(x, y, w).label(l).range(min, max, step).value(v).onChange(c) |
KineticSlider |
| 文本框 | ui.textField(x, y, w).label(l).placeholder(p).value(v).maxLength(n).validator(v).onChange(c) |
KineticTextField |
| 多行文本 | ui.textArea(x, y, w, h).label(l).value(v).maxLength(n).onChange(c) |
KineticTextArea |
| 数字框 | ui.numberField(x, y, w, NumberType.INT / LONG / DECIMAL).allowNegative(b).range(min, max).value(n).onChange(c) |
KineticNumberField |
| 自动补全 |
ui.autoComplete(x, y, w, dictionary).value(v).onChange(c).onSelect(s);数字用 ui.numberAutoComplete(x, y, w, type, dictionary)
|
KineticAutoCompleteField / KineticNumberAutoCompleteField
|
| 下拉框 | ui.dropdown(x, y, w, options).selected(value).onChange(c) |
KineticDropdown |
| 标签栏 |
ui.tabBar(x, y, w, labels).tooltips(t).selected(i).onSelect(c);.vertical()
|
KineticTabBar |
| 可滚动标签条 | ui.tabStrip(x, y, w, tabs).pinnedLeading(p).selected(i).arrows(prev, next).onSelect(c) |
KineticTabStrip |
| 列表 |
ui.selectionList、itemSelectionList、actionList、itemActionList、multiActionList、toggleList、toggleActionList、multiToggleList,参数 (x, y, w, h, items),加 .selected(i)、.scrollOffset(rows)、.onSelect、.onAction、.onToggle
|
Kinetic*List(api.client.gui.widget.list) |
| ID 列表 | 在 selectionList 和 toggleList 上调用 .textRows(),以 14 像素条纹纯文字行代替按钮显示,用于群系、伤害类型、属性等 ID。API 统一保留 2 像素行间隔,点击间隔不会选中条目。黄色边框表示当前选择,绿色边框表示已选的多选行;选中时保留组件原有文字颜色。 |
同上列表 |
| 物品网格 | ui.itemGrid(x, y, w, h, density, items).scrollOffset(rows).onClick(c) |
KineticItemGrid |
| 自己的控件 | ui.add(control) |
该控件 |
| 移除控件 | ui().remove(control) |
物品网格间距。 ItemGridDensity.COMPACT 使用 22 像素格子,格子之间留 2 像素,网格内边距为 2 像素。背景复用一张完整的 8×8 棋盘格贴图,横纵方向使用相同缩放倍率;贴图包含 8 行、8 列逐像素交替的色块,不是 2×2 大色块。正方形格子内部完整铺满,不保留额外黑色内衬。只绘制完整贴图,长方形区域的剩余空间留作内边距,不允许为填满长方形而拉伸或裁切。保留原有视口大小,列数由视口宽度决定。静态物品图标必须完整放入槽位,必要时减少行列数量;滚动视口可正常遮挡边缘的物品。原版容器与 JEI 配方背景保留自己的槽位。
控件的方法名。 控件接口使用的方法名从不与原版控件方法重名,因为 1.20.1 的正式 JAR 会把重名的方法改名:controlX()、controlY()、controlWidth()、controlHeight()、moveControlX/Y、resizeControlWidth/Height、moveTo(x, y)、controlVisible()/setControlVisible、controlHovered()、controlFocused()、contains(x, y);文本框使用 textValue()/setTextValue、onTextChange、limitTextLength、filterText、formatText、cursorIndex()/setCursorIndex;滑块使用 setSliderValue。isEnabled/setEnabled/setActive 和 setTooltip(...) 保持常用名称。
选中颜色。 黄色表示当前选择(setSelected)。绿色表示多选控件中已选的项:开关列表中处于开启的行,以及调用 KineticButton.setPicked(true) 的按钮,以及右键菜单中勾选的 MenuItem.toggle 行;只能选一项的菜单使用 MenuItem.choice,其当前选项为黄色。光标所在的行或控件显示蓝色悬停边框。
输入框文字颜色由核心统一处理,不要自己绘制:
| 状态 | 颜色 |
|---|---|
| 为空且未聚焦 | 占位文字为纯白(占位文字中的颜色样式和 § 代码会被忽略) |
| 值与默认值相同 | 青色 |
| 其他值 | 绿色 |
| 无效值 | 红色 |
| 自定义规则 |
.valueColor(value -> …) 返回的颜色(返回 null 时按上面几行处理) |
用 .defaultText(...) / .defaultValue(...) 设置默认值;值在构建之后才载入时用 .firstShownTextAsDefault()。没有默认值时,输入的任何内容都算作已修改。
KineticGraphics(api.client.gui.render)是页面、自定义控件、HUD 渲染和渲染事件唯一收到的绘制对象。
| 需求 | 调用 |
|---|---|
| 矩形与线 |
g.fill(x1, y1, x2, y2, argb)、g.fillGradient(...)、g.outline(x, y, w, h, argb)、g.hLine、g.vLine
|
| 文字 |
g.text(text, x, y, argb[, shadow])(不传 shadow 时没有阴影)、g.centeredText(...)、g.wrappedText(text, x, y, maxW, argb)、g.scrollingText / scrollingTextCentered / scrollingTextRight(...)
|
| 物品 |
g.item(stack, x, y[, alpha])、g.fakeItem(stack, x, y)、g.itemDecorations(stack, x, y[, text])
|
| 贴图 |
static final KineticTexture TEX = KineticTexture.of(ns, path[, texW, texH]);,然后 g.texture(TEX, x, y, u, v, w, h[, argb]) 或缩放版 g.texture(TEX, x, y, w, h, u, v, rw, rh)
|
| 效果图标 | g.effectIcon(effect, x, y, size) |
| 变换 |
g.push() / g.pop()、g.isolated(() -> …)、g.translate(x, y)、g.scale(sx, sy)、g.rotate(deg)、g.raise(n)
|
| 裁剪 |
g.clipped(l, t, r, b, runnable)、g.scissor(...) / g.endScissor()
|
| 测量 |
g.textWidth(x)、g.lineHeight();绘制之外用 KineticText.width / lineHeight / trim / ellipsize / wrap / wrappedHeight / scrollOffset
|
| 实体预览 |
KineticEntityPreview.create(),再 preview.render(g, entity 或 id, key, x, y, w, h, hovered)
|
| HUD 尺寸 |
KineticClientRuntime.guiScaledWidth() / guiScaledHeight()
|
附属 GUI 代码不使用 RenderSystem,上面每个绘制方法都会自己设置好状态。
英文通常比中文宽得多,所以凡是放在固定空间里的文字(输入框前的标签、行内单元格、卡片标题、按钮旁的标题等),都必须带宽度限制绘制:
-
g.scrollingText(text, x, y, maxW, argb, shadow):放得下时正常绘制;超宽时裁剪到maxW并匀速左右往返滚动。scrollingTextCentered、scrollingTextRight分别对应居中和右对齐。 -
maxW是到下一个元素之前的可用空间,而不是文字本身的宽度:控件前的标签取控件X - 标签X - 间距。 - 可能多行的文字用
g.wrappedText,并按KineticText.wrappedHeight预留高度。 - 自绘的图文混排内容,在
g.clipped(...)内按KineticText.scrollOffset(内容宽, 视口宽)平移,滚动节奏相同。 - 普通的
g.text只用于长度可控的文字(数字、固定的短符号),或独占整行的文字。
ui() 提供的按钮、输入框、标签条、列表和对话框已经对自己的文字这样处理。
KineticTheme 用来绘制主题元素:panel、panelAlt、surface、stateSurface、button(g, x, y, w, h, text, hovered, active, error)、separator、gridFrame、itemSlot、itemGrid、selectionFlash、stateOutline、indicatorOutline/Fill/Color、colorSwatch、checkerboard、shadow、alphaText、hovering(...)、muted(text)。调色板颜色从 KineticTheme.current() 获取:text()、mutedText()、danger()、background() 等。
KineticRowList<T> 负责平滑滚动、带悬停提示的主题滚动条、中键跳转与选中闪烁、滚轮、拖动滑块、裁剪、斑马纹行背景以及上下键选择。你只需绘制行的内容。
private final class TypeList extends KineticRowList<Row> {
TypeList() { super(16, 68, 612, 240, 24); } // x、y、宽、高、行高
@Override
protected void renderRow(KineticGraphics g, Row row, int index, int x, int y, int w, int h,
boolean hovered, boolean selected) {
g.item(row.icon(), x + 5, y + 4);
g.text(KineticText.trim(row.name(), 320), x + 28, y + 8, KineticTheme.current().text());
}
@Override
protected boolean onRowClick(Row row, int index, MouseInput input) {
if (!input.isLeft()) return false;
select(index); // 触发 setOnSelect 回调,并保持该行可见
return true;
}
@Override protected Component rowTooltip(Row row, int index) { return row.tooltip(); }
@Override protected Component emptyText() { return KineticI18n.translatable("gui.mymod.empty"); }
}
// 在 build() 中:list = ui.add(new TypeList()); list.setItems(rows); list.setOnSelect(i -> …);其他成员:items()、setItems、selectedIndex()、selectedItem()、setSelectedIndex(i)(不触发回调)、select(i)(触发回调)、scrollOffset()、setScrollOffset(rows)、scrollTo(i)、visibleRows()、rowAt(mx, my)、rowsWidth()、rowTop(index)、mouseX()/mouseY();重写 renderRowBackground 可以修改斑马纹背景。
行内按钮在 renderRow 中用 KineticTheme.button(...) 绘制(悬停状态由 mouseX()/mouseY() 计算),在 onRowClick 中借助 rowTop(index) 和 input.inside(...) 处理点击。如果每一行都是“文字 + 开关 + 操作”,直接使用内置的 toggleActionList / actionList / multiActionList。
手写的滚动区域使用 KineticScrollController:update(total, visible) 或 updateRange(max, total, visible)、render(g, …)、beginDrag(mx, my, input.button(), x, y, w, h, minThumb)、drag、release(input.button())、scroll(deltaY)、scrollTo(offset)。滑块提示和中键跳转需要一个跳转目标:
scroll.bindSelection(() -> selectedIndex >= 0 ? selectedIndex : lastClicked,
index -> index / COLUMNS - VISIBLE_ROWS / 2); // 条目索引 → 滚动偏移
// 绘制每个条目之后:
scroll.renderSelectionFlash(g, index, cellX, cellY, CELL, CELL);一起滚动的真实控件使用视口(仅 CANVAS 页面);视口之外的控件会被裁剪且不接收输入,行数变化时需要 rebuild():
KineticUi rows = ui.scrollViewport(LIST_X, LIST_Y, LIST_X + LIST_W, LIST_Y + LIST_H, () -> scroll.smoothOffset() * ROW_H);
for (int i = 0; i < entries.size(); i++) {
int index = i;
rows.button(x, LIST_Y + i * ROW_H + 6, 30).text(UP).onClick(() -> move(index, -1)).build();
}public final class TrashButton extends KineticCustomControl {
private static final KineticTexture ICON = KineticTexture.of("mymod", "textures/gui/trash.png", 16, 16);
private final Runnable action;
public TrashButton(int x, int y, Runnable action) { super(x, y, 18, 18); this.action = action; }
@Override
protected void render(KineticGraphics g, int mx, int my, float pt) {
KineticTheme.stateSurface(g, controlX(), controlY(), controlWidth(), controlHeight(), KineticTheme.Surface.PANEL, false, controlHovered(), false);
g.texture(ICON, controlX() + 1, controlY() + 1, 0, 0, 16, 16);
}
@Override
protected boolean onMouseClick(MouseInput in) {
if (!in.isLeft()) return false;
playClickSound();
action.run();
return true;
}
}
// 页面中:ui.add(new TrashButton(10, 10, this::clear)).setTooltip(KineticI18n.translatable("…"));回调:render(必须实现)、onMouseClick(只在范围内触发)、onMouseRelease、onMouseDrag、onMouseScroll、onKeyPress/onCharTyped(获得焦点时;isFocusable() 返回 true)、onFocusChanged、onTick、narration。
构成页面一部分的面板和标签页模块接收页面的 KineticUi(以及页面本身,用于 showTooltip、openChild、rebuild),并在页面的 build 中创建自己的控件。所有控件都由宿主绘制和分发输入,不要手动绘制控件或转发事件。
public final class DummyPage extends KineticContainerPage<DummyMenu> {
public DummyPage(DummyMenu menu, Component title) { super(menu, title); setImageSize(176, 222); }
@Override protected void build(KineticUi ui) { … }
@Override protected void renderContainerBackground(KineticGraphics g, int mx, int my, float pt) { … }
}
// 客户端初始化:
KineticClientMenus.register(MyMenus.DUMMY, DummyPage::new);容器页面额外提供 menu()、hoveredSlot()、leftPos()、topPos()、imageWidth()、imageHeight()、setImageSize(w, h)、setTitleLabelPosition(x, y) 和 setInventoryLabelPosition(x, y)。renderTooltips 默认显示悬停槽位的物品提示;重写时调用 super.renderTooltips(mx, my) 保留它。菜单类型通过 KineticMenuTypes 注册。
继承 KineticHudEditorPage。它是不暂停游戏的 NATIVE 页面,会自己创建保存、重置、取消按钮:
public final class WalletHudEditorPage extends KineticHudEditorPage {
public WalletHudEditorPage() { super(KineticI18n.translatable("screen.mymod.hud_editor")); }
@Override protected int elementWidth() { return Hud.width(); }
@Override protected int elementHeight() { return Hud.height(); }
@Override protected HudLayout initialLayout(int sw, int sh) { return new HudLayout(Config.x(), Config.y(), Config.scale()); }
@Override protected HudLayout defaultLayout(int sw, int sh) { return new HudLayout(sw - elementWidth() - 2, sh - elementHeight() - 2, 1.0D); }
@Override protected void renderElement(KineticGraphics g, int x, int y, int mx, int my) { Hud.render(g, x, y); }
@Override protected void save(HudLayout layout) { Config.setLayout(layout.x(), layout.y(), layout.scale()); KTConfigApi.refreshOpenScreens(); }
}可选重写:minimumScale()、instruction()、positionText(HudLayout)(配置保存的是相对某个角的偏移时使用)、renderBackdrop、scalable()(不可缩放的元素)。辅助方法:currentLayout()、scaledElementWidth()、scaledElementHeight()、resetLayout(),以及在背包里摆放元素的编辑器使用的 KineticHudEditorPage.renderInventoryReference(g, width(), height(), mx, my)。HUD 本身用 KineticClientEvents.onHudRender(stage, (g, pt) -> …) 绘制。
KineticClientEvents.onScreenInitAfter(ctx -> {
if (!(ctx.screen() instanceof InventoryScreen)) return;
ctx.addButton(x, y, 20, KineticI18n.translatable("…"), action); // KineticButton
ctx.addControl(new TrashButton(x, y, action)); // 任意 KineticCustomControl
KineticTextField box = ctx.addTextField(x, y, 80); // 可编辑的文本框
});- 这样添加的按钮和自定义控件是覆盖层:画在宿主界面之后,比宿主界面先收到鼠标事件,并且从不占用键盘焦点(在聊天界面中也安全)。
addTextField例外,它作为普通成员加入界面,因为输入文字需要焦点。 - 每当界面重新初始化时它们都会被清除,你的处理器会再次添加。用
ctx.removeControl(control)移除单个控件。 - 这里的自定义控件没有 tick 和按键回调,需要每帧同步的状态放在
KineticClientEvents.onScreenRenderBefore中处理。 -
findExistingListener(type)、removeFirstExistingListener(type)、addListener和removeListener只用于调整宿主界面已有的控件。你添加的任何东西都通过addButton/addTextField/addControl。 - Mixin 只用来修改原版行为。必须绘制 Kinetic 视觉效果的 Mixin 用
KineticGraphicsInterop.wrap(graphics)包装绘制对象;需要原版绘制对象的第三方 API 可以用KineticGraphicsInterop.unwrap(g)取得。
private KineticCommandAssist assist;
@Override protected void build(KineticUi ui) {
KineticTextField input = ui.textField(44, 278, 552).maxLength(2048).value("/").build();
assist = KineticCommandAssist.attach(input, width(), height(), false, 10, value -> { /* 内容变化时 */ });
focus(input);
}
@Override protected void renderForeground(KineticGraphics g, int mx, int my, float pt) { assist.render(g, mx, my); }
@Override protected boolean onKeyPress(KeyInput in) { return assist.keyPress(in); }
@Override protected boolean onMouseClickCapture(MouseInput in) { return assist.mouseClick(in); }
@Override protected boolean onMouseScroll(ScrollInput in) { return assist.mouseScroll(in); }attach 会接管文本框的变化监听,所以把你自己的处理函数作为最后一个参数传入。
KineticSelectors 在当前页面或界面之上打开通用选择器,当前页面自动成为它们的父页面:
-
openItemSelector(cb)/openItemSelector(preset, cb)/openItemSelectorWithOptions(...) -
openEntitySelector(...)、openItemListEditor(...)、openNbtEditor(initial, onSave)、openColorPicker(title, rgb, onApply)、openPalette(...) -
openNbtEditor(initialText, problem, onSave)用同一编辑器编辑由你自己的规则检查的文本,例如 1.20.5+ 的物品数据组件([damage=5]),界面与 Forge 上的 NBT 编辑器一致。problem在可以保存时返回null,否则返回要显示的错误;空白文本保存为""。 -
KineticCommandListEditor.open(getter, setter, pageId, entryId, text);KineticCommandListEditor.action(...)为配置按钮提供Runnable -
KineticSelectors.registerInventorySource(...)向物品选择器的背包标签页添加物品来源(例如其他模组的饰品栏)
默认物品选择器保留原有视口:19 列、12 行,剩余物品可通过滚动浏览。列数按原视口决定,不会因此扩大面板。Forge 1.20.1、NeoForge 1.21.1 与 NeoForge 26.1.2 使用相同布局。
回调在选择器关闭时运行,之后父页面的 build 会再次运行。所以在回调中更新状态字段,让 build 重新创建控件。
| 需求 | 调用 |
|---|---|
| 打开配置索引 / 某个页面 / 某个模组的配置 |
KTConfigApi.openIndex()、openPage(pageId)、openPage(page)、openOwner(modId)
|
| 修改后刷新已打开的配置界面 | KTConfigApi.refreshOpenScreens() |
| 打开你自己页面的配置按钮 |
KTConfigApi.pageAction(XPage::new);打开另一个配置页用 KTConfigApi.configPageAction(() -> page)
|
| 模组列表中的“配置”按钮 |
KTConfigApi.installConfigHub(modId) 或 KTConfigApi.installConfigPage(modId, XPage::new)
|
配置页本身用 KTConfigPage.builder(...) 描述,并通过 KTConfigApi.register(...) 注册;服务端权威的数值使用 KTServerConfigSpec。
为已添加的 STRING_LIST 配置项调用 builder.listEditor(entryId, (title, initial, apply) -> ...),可绑定可视化规则编辑器。initial 是不可变草稿;仅点击“应用”时调用 apply.accept(updatedList),取消时不调用。最终保存、校验和服务器权限仍由配置页处理。KineticCore 26.10.10.1 起,单独配置页 openPage(...) 与模组总配置页 openOwner(...) 都支持此入口。
-
KineticClientEvents.onHudRender(stage, (KineticGraphics g, float pt) -> …)在 HUD 上绘制。 -
onScreenRenderBefore/After((screen, g, mx, my, pt) -> …)在任意界面前后绘制。事件签名中的Screen只用来识别原版界面;要判断是否是自己的界面,用KineticGui.currentPage(MyPage.class)。 -
ClientHooks.ResourceReloadUi.render(KineticGraphics, w, h)在资源重载时绘制。 - 带图标或自定义行的物品提示:
KineticItemTooltips.registerComponentFactory(type, data -> new MyComponent(...)),其中MyComponent implements KineticTooltipComponent(height()、width()、render(KineticGraphics, x, y))。 - 跟随世界位置的标签:在
KineticClientEvents.onLevelRender(...)中,KineticWorldRender.beginScreenOverlay(context)提供project(worldPos)和graphics()。 - 其他模组列表中鼠标下的物品(供复制物品使用):
KineticHoveredItems.register(...)。 - 显示紧凑状态效果的背包界面实现了
EffectAreaProvider,其他代码可以据此避开状态效果区域。
-
阴影。
g.text(text, x, y, color)没有阴影;原版文字带阴影的地方,第五个参数传true。 -
点击顺序。
onMouseClick在控件处理之后运行;必须在控件之前完成的操作放在onMouseClickCapture。 -
结构变化。 增删持有真实控件的行时需要调用
rebuild()。 -
暂停。 页面默认暂停游戏;在游戏中打开的编辑器通常需要
setPausesGame(false)。 -
Escape。 “有未保存的修改”对话框放在
onCloseRequested()中(返回true)。同样需要询问的返回按钮调用close(),不要用navigateBack()。 -
滚动。 使用
ScrollInput的in.deltaY()。 - 业务规则留在附属。 GUI 只决定如何显示和操作,不决定默认值、校验、保存时机、权限或网络包格式。
架构检查。 从 KineticCore 仓库复制 gradle/kinetic-addon-architecture.gradle,在附属的 build.gradle 中加上 apply from: 'gradle/kinetic-addon-architecture.gradle'。它会在 compileJava 之前扫描源码,Mixin 之外凡是违反 §1 规则的地方都会让构建失败:import internal/feature、继承原版界面或控件、Button.builder、new EditBox、new ConfirmScreen、Tooltip.create、addRenderableWidget/clearWidgets/this.init()、setScreen/MenuScreens.register/NetworkHooks.openScreen、import 已有 Kinetic 替代的原版 GUI 类型、Screen.hasShiftDown() 这类修饰键判断(改用 KineticClientRuntime.shiftModifierDown() 等)、Component.translatable 和贴图用的 ResourceLocation。每条失败都会给出文件、行号、规则和应使用的 API。确实需要的例外(例如必须用原版绘制的 JEI 分类)在 apply from 那一行之后按文件和规则声明:ext.kineticArchitectureAllow = ['src/main/java/.../ExampleCategory.java': ['vanilla-gui-type']];不再匹配任何内容的声明也会让检查失败。
最终 JAR 检查(1.20.1)。 1.20.1 的正式 JAR 会把与原版重名的方法改名,所以 compileJava 通过还不够。复制 gradle/KineticReobfCheck.java 和 gradle/kinetic-addon-reobf-verification.gradle,在 dependencies 之前应用这个脚本,并添加一个指向同一个 KineticCore JAR 的 kineticCoreReobf 依赖。之后 gradlew build 会运行 checkKineticReobfRefs:附属 JAR 中每个指向 KineticCore 的引用都必须能在发布的核心 JAR 中找到(-Pkineticcore_jar=<路径> 可以用本地构建的核心测试)。也可以手动运行:java gradle/KineticReobfCheck.java addon <kineticcore.jar> <addon.jar>。
每个页面的游戏内测试: 用 Escape 和返回按钮打开、关闭;调整窗口大小(检验 build 可重复执行);打开子页面或选择器再返回;用滚轮和拖动滑块滚动列表;鼠标在滑块上停留半秒(提示)以及中键点击滑块(跳转与闪烁);提示框、右键菜单和对话框;GUI 缩放 1–4 以及 4K 窗口。
编辑页的返回按钮优先放在左上角;现有顶栏放不下时放在左下角。按钮与 Esc 使用相同的返回行为,并保留未保存修改的确认提示。相邻标题需要限制可用宽度,控件之间至少保留 2 个 GUI 像素间距。
物品搜索索引提示在同一条消息末尾更新百分比,完成时改为完成提示和物品数量,不再显示百分比。相同 ID 的提示更新文字和显示期限,保留当前动画位置,不再重新淡入。Minecraft 尚未初始化时,客户端世界、界面、玩家和连接查询返回不可用状态,可安全用于附属启动阶段。
ui.itemButton 自动为 1.0 大小的物品图标绘制 API 棋盘格背景。自绘工作台卡片在绘制图标前调用 KineticTheme.itemSlot。玩家背包和原版工作台配方背景继续使用其原有槽位。返回按钮文字使用白色。
纯文字选择条在普通状态下也显示描边。条目之间保留 2 个 GUI 像素,选中时只改变描边,保留文本组件的原有颜色。