|
| 1 | +# Machine UI Editor:Runtime JSON 对接(现状 + 契约 + 限制) |
| 2 | + |
| 3 | +本文件描述 **Web Editor(Machine UI Editor)导出的 Runtime JSON** 在 PrototypeMachinery 模组侧的解释与构建方式。 |
| 4 | + |
| 5 | +这份文档的目标是: |
| 6 | + |
| 7 | +- 给出一个“可长期维护”的 **稳定契约**(字段名、默认值、兼容策略) |
| 8 | +- 解释当前实现已经支持什么,以及“看起来像支持但其实不支持”的坑 |
| 9 | +- 把 WebEditor 的 IR 与 Mod 侧的 Kotlin 实现对齐(避免文档飘在实现之上) |
| 10 | + |
| 11 | +## 已落地的实现(代码现状) |
| 12 | + |
| 13 | +### Runtime JSON 注册入口(ZenScript) |
| 14 | + |
| 15 | +- `mods.prototypemachinery.ui.UIRegistry.registerRuntimeJson(machineId, runtimeJson)` |
| 16 | +- `mods.prototypemachinery.ui.UIRegistry.registerRuntimeJsonWithPriority(machineId, runtimeJson, priority)` |
| 17 | + |
| 18 | +代码:`src/main/kotlin/integration/crafttweaker/zenclass/ui/UIRegistry.kt` |
| 19 | + |
| 20 | +> 这意味着:Web Editor 生成的 ZenScript(runtime-json 形式)已经可以直接在游戏里注册 UI。 |
| 21 | +
|
| 22 | +### Runtime JSON 解析器(Mod 侧) |
| 23 | + |
| 24 | +代码:`src/main/kotlin/impl/ui/runtime/MachineUiRuntimeJson.kt` |
| 25 | + |
| 26 | +当前解析器特性: |
| 27 | + |
| 28 | +- **容错**:未知 widget type 会被忽略(不会崩 UI) |
| 29 | +- **字段兼容**:多个同义字段名会被识别(例如 `w/width`、`h/height`) |
| 30 | +- **Tabs**:支持 `options.tabs[]`(新)与 legacy `backgroundA/backgroundB`(旧) |
| 31 | +- **条件字段**:支持 `visibleIf/enabledIf`(生成 `ConditionalDefinition` 包装) |
| 32 | +- **容器递归**:支持 `panel/row/column/grid/scroll_container` 以及嵌套 children |
| 33 | + |
| 34 | +### 条件包装器(visibleIf / enabledIf) |
| 35 | + |
| 36 | +定义:`ConditionalDefinition`(`src/main/kotlin/api/ui/definition/WidgetDefinitions.kt`) |
| 37 | + |
| 38 | +构建:`ConditionalWidgetFactory`(`src/main/kotlin/client/gui/builder/factory/ConditionalWidgetFactory.kt`) |
| 39 | + |
| 40 | +当前语义(重要): |
| 41 | + |
| 42 | +- `visibleIf` 与 `enabledIf` 都会被解析为 **bool 绑定/表达式** |
| 43 | +- 最终效果是:`wrapper.isEnabled = (visibleIf && enabledIf)` |
| 44 | + |
| 45 | +也就是说: |
| 46 | + |
| 47 | +- 目前 **没有“真正不渲染”的隐藏语义**;而是通过 enable/disable 走一条轻量路径 |
| 48 | +- `visibleIf` 与 `enabledIf` 在当前实现里不会产生“不同的 UI 效果”(都等价于 enable gate) |
| 49 | + |
| 50 | +### Tabs(多 Tab / 分页) |
| 51 | + |
| 52 | +定义:`TabDefinition` / `TabContainerDefinition`(`WidgetDefinitions.kt`) |
| 53 | + |
| 54 | +构建:`TabWidgetFactory`(`src/main/kotlin/client/gui/builder/factory/TabWidgetFactory.kt`) |
| 55 | + |
| 56 | +实现要点: |
| 57 | + |
| 58 | +- **切换是纯客户端行为**:不需要服务端同步 |
| 59 | +- 当 `tabPosition == "LEFT"` 时使用 `PagedWidget + PageButton`: |
| 60 | + - 非激活页不会渲染(性能与“所见即所得”更接近) |
| 61 | + - 前两个 tab 使用 DefaultMachineUI 风格图标贴图 |
| 62 | +- 其他位置(TOP/BOTTOM/RIGHT)目前走简单 fallback:按钮 + enable/disable 内容 |
| 63 | + |
| 64 | +### 变量绑定与表达式(Binding Expr) |
| 65 | + |
| 66 | +绑定系统入口: |
| 67 | + |
| 68 | +- ZenScript 注册:`src/main/kotlin/integration/crafttweaker/zenclass/ui/UIBindings.kt` |
| 69 | +- 运行时创建 SyncValue:`src/main/kotlin/client/gui/builder/UIBindings.kt` |
| 70 | + |
| 71 | +表达式实现:`src/main/kotlin/client/gui/builder/bindingexpr/UiBindingExpr.kt` |
| 72 | + |
| 73 | +当前已经支持: |
| 74 | + |
| 75 | +- **bool**:`not(...)`、`and(...)`、`or(...)`(支持嵌套) |
| 76 | +- **double**:`clamp(...)`、`norm(...)`(支持嵌套),并支持数值字面量(如 `0.5`) |
| 77 | + |
| 78 | +并且:表达式是 **只读**(不会产生 setter)。 |
| 79 | + |
| 80 | +## Runtime JSON 稳定契约(建议按此长期维护) |
| 81 | + |
| 82 | +### 顶层结构 |
| 83 | + |
| 84 | +Web Editor 的 runtime 导出(`web-editor/src/machine-ui/exporters/jsonExporter.ts`)输出结构: |
| 85 | + |
| 86 | +- `schemaVersion: 1` |
| 87 | +- `name: string` |
| 88 | +- `canvas: { width, height }` |
| 89 | +- `options?: { ... }` |
| 90 | +- `widgets?: Widget[]` |
| 91 | + |
| 92 | +其中 `guides/gridSize/showGuides` 等 editor-only 字段会被剥离。 |
| 93 | + |
| 94 | +### Tabs:options 字段 |
| 95 | + |
| 96 | +推荐使用新结构: |
| 97 | + |
| 98 | +- `options.tabs[]: { id: string, label?: string, texturePath?: string }` |
| 99 | +- `options.activeTabId?: string` |
| 100 | + |
| 101 | +兼容旧结构(仍会被识别): |
| 102 | + |
| 103 | +- `options.backgroundA.texturePath` / `options.backgroundB.texturePath` |
| 104 | +- `options.activeBackground: "A" | "B"` |
| 105 | + |
| 106 | +运行时选择初始 tab 的顺序(实现一致): |
| 107 | + |
| 108 | +1) `activeTabId` |
| 109 | +2) `activeBackground` |
| 110 | +3) `tabs[0]?.id` |
| 111 | +4) fallback:`"A"` |
| 112 | + |
| 113 | +### Widget 通用字段 |
| 114 | + |
| 115 | +绝大多数 widgets 支持以下通用字段: |
| 116 | + |
| 117 | +- `type: string` |
| 118 | +- `x, y: int` |
| 119 | +- `w/h` 或 `width/height: int` |
| 120 | +- `tabId?: string`:**仅在顶层 widgets 生效** |
| 121 | +- `visibleIf?: string` |
| 122 | +- `enabledIf?: string` |
| 123 | + |
| 124 | +关于 `tabId`: |
| 125 | + |
| 126 | +- `tabId` 为空/缺失:视为 **全局 widget**(所有 tab 可见) |
| 127 | +- `tabId` 非空:视为归属到某个 tab,仅在该 tab 页里渲染 |
| 128 | +- **嵌套 children 不参与 tab 分区**: |
| 129 | + - Mod 侧解析器对嵌套 children 关闭 tabId 读取(`allowTabTag=false`) |
| 130 | + - WebEditor runtime 导出会删除 nested child 的 `tabId`(防止“跨 tab 嵌套”造成语义歧义) |
| 131 | + |
| 132 | +### 条件字段:visibleIf / enabledIf |
| 133 | + |
| 134 | +两者都是 **bool 绑定 key 或表达式**(详见下节语法)。 |
| 135 | + |
| 136 | +当前实现的“实际效果”是: |
| 137 | + |
| 138 | +$$ |
| 139 | + \mathrm{widgetEnabled} = (\text{visibleIf} \lor \text{true}) \land (\text{enabledIf} \lor \text{true}) |
| 140 | +$$ |
| 141 | + |
| 142 | +(实现里把缺失视为 `true`。) |
| 143 | + |
| 144 | +## 绑定表达式语法(实现即文档) |
| 145 | + |
| 146 | +表达式字符串形如:`func(arg0;arg1;...)`。 |
| 147 | + |
| 148 | +- 参数分隔符是 `;`(避免与逗号/JSON 冲突) |
| 149 | +- 支持嵌套:`clamp(norm(foo;0;100);0;1)` |
| 150 | +- 未识别的 `func(...)` 会被当作“普通 key”处理(不会报错,但也不会按函数求值) |
| 151 | + |
| 152 | +### Bool 表达式 |
| 153 | + |
| 154 | +支持: |
| 155 | + |
| 156 | +- `not(x)` |
| 157 | +- `and(a;b;...)` |
| 158 | +- `or(a;b;...)` |
| 159 | + |
| 160 | +示例: |
| 161 | + |
| 162 | +- `visibleIf: "not(formed)"` |
| 163 | +- `enabledIf: "and(has_power;not(redstone_lock))"` |
| 164 | + |
| 165 | +### Double 表达式 |
| 166 | + |
| 167 | +支持: |
| 168 | + |
| 169 | +- 数值字面量:`0` / `0.5` / `-1` |
| 170 | +- `clamp(x;min;max)` |
| 171 | +- `norm(x;min;max)` |
| 172 | + |
| 173 | +示例: |
| 174 | + |
| 175 | +- `progressKey: "norm(energy;0;100000)"` |
| 176 | +- `progressKey: "clamp(raw_progress;0;1)"` |
| 177 | + |
| 178 | +### Sync key 规则(避免冲突) |
| 179 | + |
| 180 | +ModularUI 要求 `(key,id)` 对应唯一 SyncHandler 类型。 |
| 181 | + |
| 182 | +因此 `client/gui/builder/UIBindings.kt` 把同一个逻辑 key 按类型做了命名空间隔离: |
| 183 | + |
| 184 | +- `prototypemachinery:ui_binding:bool:<raw>` |
| 185 | +- `prototypemachinery:ui_binding:double:<raw>` |
| 186 | +- `prototypemachinery:ui_binding:string:<raw>` |
| 187 | + |
| 188 | +这允许你在不同组件里用同一个“业务 key 名”,不会因为类型不同而冲突。 |
| 189 | + |
| 190 | +## 支持的 widget 类型(runtime JSON → Kotlin 定义) |
| 191 | + |
| 192 | +以下是当前 `MachineUiRuntimeJson` 明确支持的 type(大小写不敏感,内部会 `lowercase()`): |
| 193 | + |
| 194 | +### 容器/布局(可递归) |
| 195 | + |
| 196 | +- `panel` |
| 197 | +- `row` |
| 198 | +- `column` |
| 199 | +- `grid` |
| 200 | +- `scroll_container`(以及同义:`scrollcontainer/scroll/scrollarea`) |
| 201 | + |
| 202 | +### 叶子组件 |
| 203 | + |
| 204 | +- `text` |
| 205 | +- `progress` |
| 206 | +- `slotGrid`(JSON 中常见写法;lowercase 后等价于 `slotgrid`) |
| 207 | +- `image` |
| 208 | +- `button` |
| 209 | +- `toggle`(以及同义:`togglebutton/toggle_button`) |
| 210 | +- `slider` |
| 211 | +- `textField`(以及同义:`text_field/textfield/input_box/inputbox`) |
| 212 | +- `playerInventory`(以及同义:`player_inventory`) |
| 213 | + |
| 214 | +> 注意:这里的“支持”指 runtime JSON 解析器能产出对应 `WidgetDefinition`;最终是否能正确显示,还取决于对应 WidgetFactory 是否实现。 |
| 215 | +
|
| 216 | +## 已知限制与待办(建议写在这里,避免散落) |
| 217 | + |
| 218 | +### 1) visibleIf/enabledIf 目前等价 |
| 219 | + |
| 220 | +如果你需要“不可见但仍占位/可见但不可交互”等更细的语义,需要在 `ConditionalWidgetFactory` 做更精细的区分(例如只控制渲染/只控制交互)。 |
| 221 | + |
| 222 | +### 2) 表达式功能刻意保持最小 |
| 223 | + |
| 224 | +目前没有: |
| 225 | + |
| 226 | +- `eq(...)` / 比较运算 |
| 227 | +- string 表达式(拼接、format) |
| 228 | + |
| 229 | +建议策略:先用脚本端注册基础 binding key,把复杂逻辑放在脚本端;表达式只用于 UI 内的轻量“归一化/开关组合”。 |
| 230 | + |
| 231 | +### 3) TabContainer 目前只对 LEFT 做了精细实现 |
| 232 | + |
| 233 | +TOP/BOTTOM/RIGHT 仍是 fallback(按钮 + enable/disable)。如果 WebEditor 将来支持不同 tab bar 布局,建议在 `TabWidgetFactory` 里补齐。 |
| 234 | + |
| 235 | +### 4) 资源化管线(长期) |
| 236 | + |
| 237 | +当前推荐的“落地姿势”仍是:WebEditor 导出 ZenScript(runtime-json),脚本里注册。 |
| 238 | + |
| 239 | +如果要做真正的“资源/数据包驱动”加载(不复制粘贴 JSON 字符串),需要新增: |
| 240 | + |
| 241 | +- runtime JSON loader(从资源路径读取) |
| 242 | +- schema 版本迁移 |
| 243 | +- 更友好的 parse 错误提示 |
0 commit comments