sapdon 3.5.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -3
- package/doc/dev/architecture.md +69 -23
- package/doc/dev/cli.md +179 -43
- package/doc/dev/core.md +2 -1
- package/doc/dev/known-pitfalls.md +869 -0
- package/doc/dev/oc.md +156 -23
- package/doc/dev/ui-architecture.md +83 -0
- package/doc/dev/workflow.md +61 -19
- package/doc/guidebook.md +196 -6
- package/doc/user/api/block.md +342 -0
- package/doc/user/api/runtime.md +332 -0
- package/doc/user/faq.md +96 -14
- package/package.json +1 -1
- package/prod/cli/index.js +1 -1
- package/prod/cli/start.js +1 -1
- package/prod/core/index.d.ts +619 -73
- package/prod/core/index.js +1 -1
- package/prod/oc/index.d.ts +134 -3
- package/prod/oc/index.js +1 -1
- package/prod/templates/js_sapdon/mod.info +1 -1
- package/prod/templates/js_sapdon/package.json +4 -5
- package/prod/templates/js_sapdon/res/models/blocks/cube.geo.json +35 -0
- package/prod/templates/js_sapdon/scripts/custom_components/index.js +4 -0
- package/prod/templates/js_sapdon/scripts/custom_components/registry.js +0 -5
- package/prod/templates/ts_sapdon/package.json +5 -5
- package/prod/templates/ts_sapdon/res/models/blocks/cube.geo.json +35 -0
- package/prod/templates/ts_sapdon/scripts/custom_components/index.js +3 -0
package/doc/guidebook.md
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
- **三层结构**:`INDEX`(分类索引)→ `CAT`(词条列表)→ `ENT`(词条内容页)。
|
|
6
6
|
- **浏览器式导航**:每屏固定 `prev / home / next`,`home` 随时回首页。
|
|
7
7
|
- **多种页类型**:`text` / `crafting` / `spotlight` / `image`。
|
|
8
|
-
-
|
|
8
|
+
- **自动分页**:分类卡每页 16 张(超出翻页);分类词条每页最多 16 行;正文每页 10 行。
|
|
9
9
|
- **路由驱动**:运行时通过 Server Form 的 `body` 路径 + 按钮槽位显隐,无需每个页面单独写路由。
|
|
10
10
|
|
|
11
11
|
> 示例见 `examples/guidebook_demo`(打开游戏手持木棍右键即可看到成品)。
|
|
12
|
+
> 索引分页的规则、槽位硬约束与可调常量见 [§5 路由协议](#5-路由协议运行时)。
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
@@ -55,8 +56,18 @@ registry.submit()
|
|
|
55
56
|
new SapdonGuideBook(
|
|
56
57
|
identifier: string, // "namespace:name",如 "mymod:book"
|
|
57
58
|
size: [number, number] = [320, 207], // 手册画布尺寸
|
|
58
|
-
background: string = 'textures/ui/book_back'
|
|
59
|
+
background: string = 'textures/ui/book_back',// 背景贴图
|
|
60
|
+
options: GuideBookOptions = {} // 可选,见下
|
|
59
61
|
)
|
|
62
|
+
|
|
63
|
+
interface GuideBookOptions {
|
|
64
|
+
labels?: Partial<GuideBookLabels> // 覆盖框架渲染的固定标签(只传要改的键)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface GuideBookLabels {
|
|
68
|
+
chapter: string // CAT 页章节列表标题,默认 '章节'
|
|
69
|
+
category: string // INDEX 页索引卡列标题,默认 '类别'
|
|
70
|
+
}
|
|
60
71
|
```
|
|
61
72
|
|
|
62
73
|
### 常用方法
|
|
@@ -65,11 +76,40 @@ new SapdonGuideBook(
|
|
|
65
76
|
|---|---|
|
|
66
77
|
| `.build(categories: GuideBookCategory[])` | 传入分类数据,生成全部页面;返回 `this` |
|
|
67
78
|
| `.setCover(title, lines)` | 自定义封面标题(可含 `\n`)与简介行,如 `.setCover(' 我的手册 \\n by Me', ['第一行简介', '第二行简介'])` |
|
|
79
|
+
| `.setLabels(labels: Partial<GuideBookLabels>)` | 覆盖框架渲染的固定标签(`章节` / `类别`);**增量合并、可反复调用、必须在 `build()` 之前**。详见下文「手册的多语言」 |
|
|
68
80
|
| `.enableDebug()` | 开启调试(显示 `#form_text` 当前值 / 格子描边) |
|
|
69
81
|
| `.getSystem()` | 返回内部 `UISystem` |
|
|
70
82
|
|
|
71
83
|
调用链结束前记得 `registry.submit()`,把注册的 UI 数据提交给构建工具生成 `book.json`。
|
|
72
84
|
|
|
85
|
+
### 手册的多语言(`labels`)
|
|
86
|
+
|
|
87
|
+
框架自己渲染的**固定标签只有两个**:`CAT` 页章节列表的标题(默认 `章节`)和 `INDEX` 页索引卡列的标题(默认 `类别`)。它们可以被覆盖成任意字符串——**包括 lang 键**:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { SapdonGuideBook } from '@sapdon/core'
|
|
91
|
+
|
|
92
|
+
// 方式一:构造第 4 参
|
|
93
|
+
const book = new SapdonGuideBook('mymod:book', [320, 207], 'textures/ui/book_back', {
|
|
94
|
+
labels: { chapter: 'fz.gb.ui.chapter', category: 'fz.gb.ui.category' },
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// 方式二:链式 setLabels(增量合并,可反复调用)
|
|
98
|
+
book.setLabels({ chapter: 'fz.gb.ui.chapter' })
|
|
99
|
+
book.setLabels({ category: 'fz.gb.ui.category' }) // 上一次设的 chapter 不会被冲掉
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**五条必须知道的规则**
|
|
103
|
+
|
|
104
|
+
1. **框架不解析 lang 键**:字符串**原样**交给 `Text.setText`,由 **JSON UI 自己**解析。所以传 `fz.gb.ui.chapter` 这类键的项目**必须**在 `RP/texts/*.lang` 里定义该键,否则界面显示的是**裸键名**。
|
|
105
|
+
> ⚠️「游戏里真的解析成对应语言」这一步**未验证**(本环境无法启动 Minecraft)。
|
|
106
|
+
2. **`DEFAULT_GUIDE_BOOK_LABELS = { chapter: '章节', category: '类别' }` 是历史中文字面量,不要改**。改它会让**所有既有项目的产物发生变化**——不传 `labels` 的项目产物必须逐字节不变(回归基线见 §5「向后兼容」的 `examples/guidebook_demo`)。
|
|
107
|
+
3. **`setLabels` 是增量合并**(`chapter: labels.chapter ?? this.labels.chapter`):只传要改的键,其余保持**当前值**,可以反复调用。**不要**指望"未传的键回落默认值"——那样第二次链式调用会把第一次的设置冲掉。
|
|
108
|
+
4. **必须在 `build()` 之前调用**:labels 是**构建期**写进元素树的(CAT 页标题、INDEX 栏目标题都在 `build()` 里渲染)。`build()` 之后再调 `setLabels` 对产物没有任何影响。
|
|
109
|
+
5. **等价判据**:把中文字面量换成 lang 键时,产物 `ui/*.json` 里应当**只有那两处文本变化**,其余结构(元素数、绑定、`grid_position`、`offset`)逐字节不变。改完先 diff 产物、再进游戏。
|
|
110
|
+
|
|
111
|
+
**真实案例**:FZ 项目用这条接口把两处栏目标题换成了 `fz.gb.ui.chapter` / `fz.gb.ui.category`,并**去掉了旧的"构建后遍历元素树改文本"临时绕过**。它采用的判据就是上面第 5 条:产物结构逐字节不变。
|
|
112
|
+
|
|
73
113
|
---
|
|
74
114
|
|
|
75
115
|
## 3. 数据结构
|
|
@@ -136,25 +176,95 @@ interface GuideBookChapter {
|
|
|
136
176
|
|
|
137
177
|
- **`title`**:固定为 `sapdon_ui:<name>`(如 `sapdon_ui:book`)。
|
|
138
178
|
- **`body`(路径)**:
|
|
139
|
-
- `"INDEX"` →
|
|
179
|
+
- `"INDEX"` → 分类索引页第 1 页
|
|
180
|
+
- `"IDX|p<N>"` → 分类索引页第 `N` 页(`N`≥1;只有分类超过 16 个才需要)
|
|
140
181
|
- `"CAT:<id>|p<N>"` → 分类页(`N` 为分类页码,`p0` 左简介右列表)
|
|
141
182
|
- `"ENT:<id>:<gi>|p<N>"` → 词条内容页(`gi` 为词条序号,`N` 为内容页码)
|
|
142
183
|
|
|
184
|
+
> ⚠️ 索引第 2 页起必须写 `IDX|p<N>`,**不能**写 `INDEX|p<N>`:容器的门控是「包含」
|
|
185
|
+
> 匹配(`(not((#form_text - $gtag) = #form_text))`),`"INDEX|p1"` 同样命中索引第 1 页的
|
|
186
|
+
> `INDEX`,会让封面与首页卡格一起亮起来。新体 `IDX|p1` 不含 `INDEX` 子串,两页互斥。
|
|
187
|
+
|
|
143
188
|
### 按钮槽位(顺序固定)
|
|
144
189
|
|
|
145
190
|
| 页面 | 槽位 |
|
|
146
191
|
|---|---|
|
|
147
|
-
| INDEX | `[no_prev, no_home, no_next, idx0..
|
|
192
|
+
| INDEX `p0` | `[no_prev, no_home, next\|no_next, idx0..idx15]`(≤16 分类时后三槽全隐藏) |
|
|
193
|
+
| INDEX `p1+` | `[prev_button, home_button, next\|no_next, idx…]`(卡片仍从槽 3 起) |
|
|
148
194
|
| CAT | `[prev\|no_prev, home, next\|no_next, <id>_e<num>...]` |
|
|
149
195
|
| ENT | `[prev, home, next\|no_next]` |
|
|
150
196
|
|
|
151
|
-
占位键 `no_prev / no_home / no_next`
|
|
197
|
+
占位键 `no_prev / no_home / no_next` 不代表任何注册按钮,从而让对应导航按钮**隐藏**(仍占槽位,保证后面按钮的槽序不变)。
|
|
198
|
+
|
|
199
|
+
> ⚠️ **槽位序号是硬约束,不能改动顺序**:框架把每张卡注册进格盘时,用的是它在 form 里的
|
|
200
|
+
> **槽位序号**(`addButton(index, …, pos)` 的 `index`,会被编码成 `grid_position`),
|
|
201
|
+
> Bedrock 的集合格盘正是靠 `grid_position`(行优先序号)**把格子绑到对应的 form 按钮**上;
|
|
202
|
+
> 卡片画在哪一格另由 `pos` 决定(`offset = -基准格 + pos`)。
|
|
203
|
+
> 所以 INDEX 页的卡片槽位**必须**从槽 3 开始(prev/home/next 占 0-2,CAT 页的 `<id>_e<gi>` 同理)。
|
|
204
|
+
> 若运行期增删了前导按钮、或框架侧误把视觉序号当槽位序号传,卡片会绑到错误(占位)槽,
|
|
205
|
+
> 门控 `($binding_button_text = #form_button_text)` 不成立 → **整片卡片不显示**(详见 §8)。
|
|
152
206
|
|
|
153
207
|
### 分页规则
|
|
154
208
|
|
|
209
|
+
- **INDEX 分类索引**:**每页最多 16 张卡**,超出自动分页。
|
|
210
|
+
- `p0`(body `"INDEX"`):左半页是封面,右半页 4 列 × ≤4 行 = ≤16 张。
|
|
211
|
+
- `p1+`(body `"IDX|p<N>"`):左半页 4 列 × ≤2 行 + 右半页 4 列 × ≤2 行 = ≤16 张/页,**先填左列再填右列**。
|
|
212
|
+
- 卡片绑定名 = 分类在 `build()` 入参里的**全局序号**(`idx0..idxN`,跨页唯一,与页码无关)。
|
|
155
213
|
- **CAT 列表**:每列最多 8 行。`p0` 右列 8 行;`p1+` 左 8 + 右 8(=16 行/页)。
|
|
156
214
|
- **ENT 正文(text)**:左右半页各最多 5 行,先填左半页、超出再填右半页;**整体超过 10 行才分页**。
|
|
157
215
|
|
|
216
|
+
#### 索引分页的计算公式
|
|
217
|
+
|
|
218
|
+
框架实现:`src/core/ui/systems/sapdon/sapdonGuideBook.ts` 的
|
|
219
|
+
`IDX_*` 常量(`:73-90`)、`catCard()`(`:457`)、`addIndexColumn()`(`:486`)、`indexPageCount()`(`:515`)、
|
|
220
|
+
`p0` 组装(`:574-585`)、`p1+` 组装(`:590-627`)。
|
|
221
|
+
|
|
222
|
+
| 量 | 公式 | 备注 |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| 页数 | `total ≤ 16 ? 1 : 1 + ceil((total − 16) / 16)` | `total` = `build()` 入参的分类数 |
|
|
225
|
+
| `p0` 收录 | 前 `min(total, 16)` 张 | 右半页 4 列 |
|
|
226
|
+
| `p0` 行数 | `rows = max(1, ceil(收录数 / 4))` | 卡格栈高 `min(79%, 20% × rows)`,余量给底部 spacer |
|
|
227
|
+
| `p<k>` 收录 | `[16 + (k−1)·16, +16)` | 左列前 8 张、右列后 8 张 |
|
|
228
|
+
| `p<k>` 行数 | `rows = max(1, ceil(max(左列张数, 右列张数) / 4))` | 同页两列共用 `rows`,保证标题/分割线左右对齐 |
|
|
229
|
+
| 末页右列为空 | 用 `idx_col_p<k>_r_empty` 占位(不画标题与分割线) | 只在末尾出现,不会出现在中间页 |
|
|
230
|
+
|
|
231
|
+
#### 卡片 → 槽位 → 格位 对照
|
|
232
|
+
|
|
233
|
+
第 `j` 张卡(`j` = 该卡在**本页(p0)/ 本列(p1+)内**的序号,从 0 起):
|
|
234
|
+
|
|
235
|
+
| 量 | 值 | 由谁决定 |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| 绑定名 | `idx<全局序号>` | `FormButton.setBinding()` |
|
|
238
|
+
| form 槽位 | `3 + j`(`p1+` 右列继续 `3 + 8 + j`) | 运行期 `ActionFormData.button()` 的顺序 |
|
|
239
|
+
| `grid_position` | `[(3+j) % 4, ⌊(3+j) / 4⌋]` | `FormButtonGrid.addButton(index, …)` 的 `index` |
|
|
240
|
+
| 视觉格 | `[j % 4, ⌊j / 4⌋]` | `FormButtonGrid.addButton(…, pos)` 的 `pos` |
|
|
241
|
+
|
|
242
|
+
> 例(4 分类的 `p0`):`idx0 → 槽3 → grid_position[3,0] → 视觉格[0,0]`、`idx1 → 槽4 → [0,1] → [1,0]`、
|
|
243
|
+
> `idx2 → 槽5 → [1,1] → [2,0]`、`idx3 → 槽6 → [2,1] → [3,0]`。
|
|
244
|
+
> 这就是生成产物里 `grid_item_003..006` 与 `offset: -300% / (100%,-100%)` 的由来。
|
|
245
|
+
|
|
246
|
+
#### 可调常量
|
|
247
|
+
|
|
248
|
+
| 常量 | 默认 | 含义 / 影响 |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `IDX_COLS` | `4` | 每行几张卡(改大→卡更小) |
|
|
251
|
+
| `IDX_ROWS_P0` | `4` | `p0` 右半页行数上限(`× IDX_COLS` = 每页容量) |
|
|
252
|
+
| `IDX_ROWS_COL` | `2` | `p1+` 每半页行数上限(`× IDX_COLS` = 每半页容量) |
|
|
253
|
+
| `IDX_PER_PAGE` | `16` | 每页容量 = `IDX_COLS × IDX_ROWS_P0` |
|
|
254
|
+
| `IDX_PER_COL` | `8` | `p1+` 每半页容量 = `IDX_COLS × IDX_ROWS_COL` |
|
|
255
|
+
| `IDX_SLOT_BASE` | `3` | 首张卡的 form 槽位序号(导航占 0-2) |
|
|
256
|
+
|
|
257
|
+
> 目前**没有**公开构造参数:改容量 = 改这几个模块级常量后重建框架。
|
|
258
|
+
> 运行期必须同步**同一个容量规则**(见下方 `idxPageCount` / `idxRange` 示例)。
|
|
259
|
+
|
|
260
|
+
#### 向后兼容
|
|
261
|
+
|
|
262
|
+
- 分类 **≤16**:只有 `p0`,body 仍是历史上的 `"INDEX"` —— **旧脚本一行都不用改**。
|
|
263
|
+
- 分类 **≤4**:索引页产物与引入分页前**逐字节一致**(`examples/guidebook_demo` 的
|
|
264
|
+
`dev/guidebook_demo_RP/ui/book.json` 可作回归基线:587870 字节,
|
|
265
|
+
sha256 `97859A7B3B1B254233AE83EBE86452F4A3F21108FDB49F3C63017F3E1225DD97`);
|
|
266
|
+
一旦这个文件不再逐字节相同,就说明索引布局(尤其是槽位编码)被动了。
|
|
267
|
+
|
|
158
268
|
运行时脚本里需要维护两个与 `main.ts` 数据对齐的量:
|
|
159
269
|
|
|
160
270
|
```ts
|
|
@@ -162,6 +272,15 @@ interface GuideBookChapter {
|
|
|
162
272
|
const CATS = ["intro", "pages", "routing", "controls"]; // 与 main.ts 分类 id 对齐(含顺序)
|
|
163
273
|
const CAT_CHAPTERS: Record<string, number> = { intro: 4, pages: 6, routing: 6, controls: 6 }; // 每分类词条数
|
|
164
274
|
const ENT_PAGES: Record<string, number> = { pages_e4: 2 }; // 需要多页的 text 词条 → 页数(ceil(lines/10))
|
|
275
|
+
|
|
276
|
+
// 索引分页(分类 >16 才用得上):p0 容量 16,p1+ 每页 16
|
|
277
|
+
const IDX_PER_PAGE = 16;
|
|
278
|
+
const idxPageCount = (total: number) => (total <= IDX_PER_PAGE ? 1 : 1 + Math.ceil((total - IDX_PER_PAGE) / IDX_PER_PAGE));
|
|
279
|
+
const idxRange = (page: number, total: number): [number, number] => {
|
|
280
|
+
const start = page === 0 ? 0 : IDX_PER_PAGE + (page - 1) * IDX_PER_PAGE;
|
|
281
|
+
return [start, Math.min(start + IDX_PER_PAGE, total)];
|
|
282
|
+
};
|
|
283
|
+
const idxBody = (page: number) => (page === 0 ? "INDEX" : `IDX|p${page}`);
|
|
165
284
|
```
|
|
166
285
|
|
|
167
286
|
> 若某 text 词条行数超过 10,`main.ts` 会用 `ENT_PAGES` 里的页数来让 next/prev 生效。忘加会导致分页无法翻动。
|
|
@@ -185,7 +304,7 @@ const NO_PREV = "no_prev", NO_HOME = "no_home", NO_NEXT = "no_next";
|
|
|
185
304
|
function openIndex(p: Player): void {
|
|
186
305
|
const f = new ActionFormData().title(TITLE).body("INDEX");
|
|
187
306
|
f.button(NO_PREV); f.button(NO_HOME); f.button(NO_NEXT);
|
|
188
|
-
CATS.forEach((_, i) => f.button(`idx${i}`));
|
|
307
|
+
CATS.forEach((_, i) => f.button(`idx${i}`)); // 分类 ≤16 时一页足够
|
|
189
308
|
f.show(p).then((r) => {
|
|
190
309
|
if (r.canceled) return;
|
|
191
310
|
const s = r.selection!;
|
|
@@ -235,6 +354,54 @@ world.afterEvents.itemUse.subscribe((e) => {
|
|
|
235
354
|
|
|
236
355
|
> **打开触发**默认是手持**木棍右键**(`minecraft:stick`)。若手册由某个具体物品打开(如 more-golem 的指南书),把 `e.itemStack.typeId` 换成该物品的 id,或改为在物品的 `onUse` 自定义组件里直接调 `openIndex(player)`。
|
|
237
356
|
|
|
357
|
+
### 分类超过 16 个:把 `openIndex` 换成可翻页版本
|
|
358
|
+
|
|
359
|
+
上面那版 `openIndex` 只在分类 ≤16 时够用(`p1+` 的 body 收不到)。改成下面这版即可支持任意数量:
|
|
360
|
+
`p0` 保持 `body = "INDEX"`(**旧行为不变**),`p1+` 用 `body = "IDX|p<k>"`,并在槽 0/2 放 `prev`/`next`。
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
const IDX_PER_PAGE = 16; // 与框架常量一致(见 §5「可调常量」)
|
|
364
|
+
const idxBody = (page: number) => (page === 0 ? "INDEX" : `IDX|p${page}`);
|
|
365
|
+
const idxRange = (page: number, total: number): [number, number] => {
|
|
366
|
+
const start = page === 0 ? 0 : IDX_PER_PAGE + (page - 1) * IDX_PER_PAGE;
|
|
367
|
+
return [start, Math.min(start + IDX_PER_PAGE, total)];
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/** 索引页(可翻页)。p0:三枚导航隐藏(历史行为);p1+:prev/home 回索引首页、next 翻页 */
|
|
371
|
+
function openIndex(p: Player, page = 0): void {
|
|
372
|
+
const total = CATS.length;
|
|
373
|
+
const [start, end] = idxRange(page, total);
|
|
374
|
+
const hasPrev = page > 0;
|
|
375
|
+
const hasNext = end < total;
|
|
376
|
+
|
|
377
|
+
// 槽位固定:[prev?, home?, next?, idx<start>..idx<end-1>] —— 卡片必须从槽 3 起(见 §5 槽位硬约束)
|
|
378
|
+
const f = new ActionFormData().title(TITLE).body(idxBody(page));
|
|
379
|
+
f.button(hasPrev ? "prev_button" : NO_PREV);
|
|
380
|
+
f.button(hasPrev ? "home_button" : NO_HOME);
|
|
381
|
+
f.button(hasNext ? "next_button" : NO_NEXT);
|
|
382
|
+
for (let i = start; i < end; i++) f.button(`idx${i}`);
|
|
383
|
+
|
|
384
|
+
f.show(p).then((r) => {
|
|
385
|
+
if (r.canceled) return;
|
|
386
|
+
const s = r.selection!;
|
|
387
|
+
if (s === 0 && hasPrev) openIndex(p, page - 1);
|
|
388
|
+
else if (s === 1 && hasPrev) openIndex(p, 0);
|
|
389
|
+
else if (s === 2 && hasNext) openIndex(p, page + 1);
|
|
390
|
+
else if (s >= 3) {
|
|
391
|
+
const ci = start + (s - 3);
|
|
392
|
+
if (ci < end) openCat(p, CATS[ci], 0);
|
|
393
|
+
else openIndex(p, page);
|
|
394
|
+
} else openIndex(p, page);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
> 两处容易踩的点:
|
|
400
|
+
> 1. **`p0` 的三个槽位必须留着**(`no_*` 占位即可)—— 卡片就是靠「槽 3 起」绑定的,把卡片挪到槽 0 会整片不显示(§8)。
|
|
401
|
+
> 2. **`p1+` 的 body 只能是 `IDX|p<k>`**,写成 `INDEX|p<k>` 会连 `p0` 的封面一起点亮(§5 的门控说明)。
|
|
402
|
+
>
|
|
403
|
+
> 可参考的实现:`examples/guidebook_demo`(≤16 的简版)、`fz-sapdon/scripts/index.ts` 的 `openIndex(player, page)`(完整版)。
|
|
404
|
+
|
|
238
405
|
---
|
|
239
406
|
|
|
240
407
|
## 7. 完整教程(从零做一个手册)
|
|
@@ -300,3 +467,26 @@ sapdon build ./
|
|
|
300
467
|
- **合成页输出是品红/黑格**:`craft.output` 引用了一个不存在的贴图。换成有效的(如 `textures/items/iron_leggings`)。
|
|
301
468
|
- **文本词条点 next 翻不动**:`ENT_PAGES` 里没给它配页数。`ENT_PAGES[`${catId}_e${gi}`] = Math.ceil(lines.length / 10)`。
|
|
302
469
|
- **想显示字面 `#`**:不要放在字符串开头,如 `form_text 门控`。
|
|
470
|
+
|
|
471
|
+
### 索引页不显示?(四个高频故障,按症状区分)
|
|
472
|
+
|
|
473
|
+
1. **右半页「类别」标题和分割线在,但一张卡都没有**(最容易被误判为"框架坏了")
|
|
474
|
+
- 原因:卡片绑到了错误的 form 槽位 —— `addButton(index, …)` 的 `index` 必须是**槽位序号**
|
|
475
|
+
(`3 + 页内序号`),写成视觉序号(`0/1/2…`)就会让卡片去绑 `no_prev/no_home/no_next/idx0` 这些槽,
|
|
476
|
+
`$binding_button_text ≠ #form_button_text` → 4 张卡全部隐藏。**生成的 JSON 看不出异常**(只差 `grid_position`/`offset` 数值)。
|
|
477
|
+
- 自检:打开产物 `dev/<proj>_RP/ui/guide.json`,找 `cat_row`:
|
|
478
|
+
4 分类时应是 `grid_item_003/004/005/006` + `grid_position [3,0]/[0,1]/[1,1]/[2,1]` +
|
|
479
|
+
`offset ["-300%","0%"] / ["100%","-100%"] …`。若看到 `grid_item_000..003` +
|
|
480
|
+
`grid_position [0,0]/[1,0]…` + `offset ["0%","0%"]`,就是槽位编码被改坏了(见 `formButtonGrid.ts` 的 `addButton` 注释)。
|
|
481
|
+
- 也可以直接跑回归对比:`examples/guidebook_demo`(4 分类)重新构建后,
|
|
482
|
+
`dev/guidebook_demo_RP/ui/book.json` 应与基线**逐字节一致**(见 §5「向后兼容」)。
|
|
483
|
+
2. **整个索引页(含封面)都不显示**
|
|
484
|
+
- 原因:运行期 `body` 与门控不匹配。`p0` 必须是 `"INDEX"`;若脚本发的是 `"INDEX|p0"` 之类,
|
|
485
|
+
将命中不了任何容器。
|
|
486
|
+
3. **索引第 2 页起「封面和卡片一起亮」/ 两页重叠**
|
|
487
|
+
- 原因:`p1+` 的 `body` 写成了 `"INDEX|p<N>"`。门控是**包含**匹配,它会同时命中 `p0` 的 `"INDEX"`。
|
|
488
|
+
改用 `"IDX|p<N>"`(不含 `INDEX` 子串,两页互斥)。
|
|
489
|
+
4. **索引能翻页,但点 `next` 没反应 / 翻到空页**
|
|
490
|
+
- 原因:运行期的容量规则与框架不一致(`p0` 16 张,`p<k>` 16 张 = 左 8 + 右 8),
|
|
491
|
+
或索引页的按钮顺序不是「3 个导航槽 + 卡片」。见 §5「索引分页的计算公式」与 §6 的完整版 `openIndex`。
|
|
492
|
+
|
package/doc/user/api/block.md
CHANGED
|
@@ -177,6 +177,161 @@ const block = BlockAPI.createGeometryBlock('demo:chair', 'construction',
|
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
180
|
+
### createTileBlock
|
|
181
|
+
|
|
182
|
+
创建**带实体的方块**(方块 + 承载它的实体),用于**可动模型 / 容器类方块**:外观是一个方块,行为由一个配套实体承担(箱子、机器外壳这类"看起来是方块、实际靠实体驱动"的东西)。
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
BlockAPI.createTileBlock(
|
|
186
|
+
identifier: string,
|
|
187
|
+
category: string,
|
|
188
|
+
textures_arr: string[],
|
|
189
|
+
options?: Object
|
|
190
|
+
): TileBlock
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**参数**
|
|
194
|
+
|
|
195
|
+
| 参数 | 类型 | 说明 |
|
|
196
|
+
|------|------|------|
|
|
197
|
+
| `identifier` | `string` | 方块唯一标识符,格式 `命名空间:名称` |
|
|
198
|
+
| `category` | `string` | 创造栏分类 |
|
|
199
|
+
| `textures_arr` | `string[]` | 6 纹理数组,顺序 `[up, down, east, west, south, north]`(= `[上, 下, 东, 西, 南, 北]`,见下方「贴图:方块侧是地形图集**键**」) |
|
|
200
|
+
| `options` | `object` | 透传给内部 `BasicBlock`(`group` / `hide_in_command` / `format_version`),另加 `entity_texture` 与三个**实体容器**参数 |
|
|
201
|
+
| `options.group` | `string` | 创造菜单分组(→ `menu_category.group`)。**分组名是本地化键**,须在 `RP/texts/*.lang` 定义,否则显示裸键名。不传 = 不写该字段 |
|
|
202
|
+
| `options.hide_in_command` | `boolean` | 是否在命令中隐藏,默认 `false` |
|
|
203
|
+
| `options.format_version` | `string` | 方块 JSON 的 `format_version`,默认 `"1.26.30"` |
|
|
204
|
+
| `options.entity_texture` | `string` | **承载实体**(客户端实体)的贴图**资源路径**,如 `"textures/blocks/entity/normal"`(省略扩展名)。默认 = `textures_arr[0]`(历史行为) |
|
|
205
|
+
| `options.inventory_size` | `number` | **实体容器**槽位数(正整数),默认 `27` |
|
|
206
|
+
| `options.container_type` | `string` | 容器音效/行为类型,默认 `"minecart_chest"`。官方文档列出的取值:`horse` / `minecart_chest` / `chest_boat` / `minecart_hopper` / `inventory` / `container` / `hopper` |
|
|
207
|
+
| `options.can_be_siphoned_from` | `boolean` | 能否用漏斗抽取,默认 `true` |
|
|
208
|
+
|
|
209
|
+
缺 `identifier` / `category` / `textures_arr` 任一项时抛错;`inventory_size` 非正整数、`container_type` 非空字符串、
|
|
210
|
+
`can_be_siphoned_from` 非布尔、`entity_texture` 非**非空字符串**时抛错。
|
|
211
|
+
|
|
212
|
+
> ★ `group` / `hide_in_command` / `format_version` / `entity_texture` 这四个键是 **2026-09 才补进 `.d.ts` 的**。
|
|
213
|
+
> 在此之前 `createTileBlock` 的声明里只有三个容器参数 ⇒ 传 `{ group }` 这种**对象字面量**会踩 TS2353
|
|
214
|
+
> (运行期其实会读 `options.group`)。现在传对象字面量不再报错;只传 `{ inventory_size }` 也照旧合法
|
|
215
|
+
> (四个新键都是**可选**)。
|
|
216
|
+
|
|
217
|
+
> ★ **贴图:方块侧是地形图集「键」,实体侧是「资源路径」** —— 同一个 `textures_arr` 被两个系统消费:
|
|
218
|
+
> - 方块侧(`minecraft:material_instances`)要 `terrain_texture.json` 的**键**(地形短名,如 `machineblock_0`);
|
|
219
|
+
> - 实体侧(`client_entity.textures.default`)要**资源路径**(如 `textures/blocks/entity/normal`),
|
|
220
|
+
> 官方示例 `"default": "textures/entity/pig/pig"`:
|
|
221
|
+
> [Microsoft Learn · Client Entity JSON and Introduction](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/entityreference/examples/cliententitydocumentation/cliententitydocumentationintroduction)。
|
|
222
|
+
>
|
|
223
|
+
> 所以**只给地形短名**的项目必须传 `options.entity_texture`,否则客户端实体会报 `Missing referenced asset`。
|
|
224
|
+
|
|
225
|
+
> ★ **项目还必须自带两份资源**(框架只生成 JSON,生不出 RP 里的资源文件):
|
|
226
|
+
> 1. **几何 `geometry.cube`** —— `TileBlock` 的状态 1 变体与承载实体都用它。它是**自定义**几何名
|
|
227
|
+
> (不是原版几何),要放在 `res/models/blocks/*.geo.json`,`identifier` = `geometry.cube`
|
|
228
|
+
> (16³ 立方体、pivot 在底面;参考 `examples/mob_chest/res/models/blocks/cube.geo.json`)。
|
|
229
|
+
> 模板 `src/templates/{js,ts}_sapdon/res/models/blocks/cube.geo.json` 已随框架提供一份默认实现
|
|
230
|
+
> (`sapdon create` 出的新项目直接可用;**既有项目请自己拷一份**)。
|
|
231
|
+
> 2. **地形键 `none`** —— 状态 1(透明)的变体用 `material_instances: { "*": { texture: "none" } }`,
|
|
232
|
+
> 需要 `res/textures/blocks/` 下有 `none.png`(模板已带)。
|
|
233
|
+
|
|
234
|
+
> ★ **这是当前引擎版本下唯一可用的方块容器路线**:容器挂在**实体**上(实体组件 `minecraft:inventory`)。
|
|
235
|
+
> 不传那三个参数时产物与历史版本**逐字节一致**(默认 27 槽 / `minecart_chest` / 可抽取),
|
|
236
|
+
> 且每次都按**实例**拷贝,改一个方块不会污染另一个。
|
|
237
|
+
> `inventory_size` 的**上限**官方文档**没给**(实体组件那页只写 "Number of slots the container has")——
|
|
238
|
+
> **不要**套用方块路线的 `[1,54]`;本环境只校验正整数,**上限「未验证」**。
|
|
239
|
+
> 依据:[Microsoft Learn · Entity Documentation - minecraft:inventory](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_inventory)。
|
|
240
|
+
|
|
241
|
+
**它会注册三份数据**
|
|
242
|
+
|
|
243
|
+
| # | 内容 | 包(root) | 路径 |
|
|
244
|
+
|---|---|---|---|
|
|
245
|
+
| 1 | 方块本体 | `behavior` | `blocks/` |
|
|
246
|
+
| 2 | 方块实体**行为** | `behavior` | `entities/`(容器就写在这里的实体组件里) |
|
|
247
|
+
| 3 | 方块实体**资源** | `resource` | `entity/` |
|
|
248
|
+
|
|
249
|
+
> ⚠️ `blocks.json`(RP 根目录)**自 2026-09 起不再被写入任何方块条目**(只留 `format_version`)。
|
|
250
|
+
> 曾经写进去的 `textures` 会让引擎对**每个自定义方块**报
|
|
251
|
+
> `trying to override the Geometry component with blocks.json settings for a custom block`。
|
|
252
|
+
> 官方文档把 `blocks.json` 定位成 "just a sound configuration system":
|
|
253
|
+
> [Microsoft Learn · blocks.json File Reference](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/blockreference/examples/blocksjsonfilestructure)。
|
|
254
|
+
> 自定义方块的贴图由 `minecraft:material_instances` + `terrain_texture.json` 提供。
|
|
255
|
+
|
|
256
|
+
**实体 identifier 自动为 `${identifier}_entity`**
|
|
257
|
+
|
|
258
|
+
例如 `demo:machine` → 实体 `demo:machine_entity`,不需要自己起名。
|
|
259
|
+
|
|
260
|
+
**返回值 `TileBlock`**
|
|
261
|
+
|
|
262
|
+
| 属性 | 类型 | 说明 |
|
|
263
|
+
|------|------|------|
|
|
264
|
+
| `.block` | `BasicBlock` | 方块本体(已带 `sapdon:block_or_entity` 状态与 `sapdon:block_with_entity` 自定义组件) |
|
|
265
|
+
| `.entity` | `Entity` | 实体(含容器、`minecraft:block_sensor`、无敌 / 不可推动等组件) |
|
|
266
|
+
|
|
267
|
+
> ★ **`sapdon:block_with_entity` 由框架注册,你不用管**(2026-09 起)。
|
|
268
|
+
> `TileBlock` 会给**每个**带实体方块挂上这个自定义组件,而自定义组件**必须在
|
|
269
|
+
> `system.beforeEvents.startup` 注册**,否则引擎会**把整份方块丢掉**:
|
|
270
|
+
> `-> components -> sapdon:block_with_entity: this component was found in the input, but is not present in the Schema`。
|
|
271
|
+
> 框架内置实现的职责只有一个:`onPlace` 时在方块中心 spawn `${identifier}_entity`
|
|
272
|
+
> (该坐标已有同种实体则跳过),并且**不**替你切换 `sapdon:block_or_entity` 状态
|
|
273
|
+
> (刻意不决定「方块是否变透明、外观交给实体」)。
|
|
274
|
+
> **历史项目手工注册过这个 id 的**:你的实现会**取代**内置实现(启动时一条 warn 说明),行为不变。
|
|
275
|
+
> 需要「方块透明 + 实体接管外观 / 动画」(`examples/mob_chest` 那种)时,自己注册即可:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// scripts/index.ts —— 运行期(仅在你需要自定义交互或透明渲染时)
|
|
279
|
+
import { registerBlockComponent } from '@sapdon/runtime'
|
|
280
|
+
|
|
281
|
+
registerBlockComponent('sapdon:block_with_entity', {
|
|
282
|
+
onPlace({ block, dimension }) {
|
|
283
|
+
// 你的 spawn / 状态切换逻辑(一注册,框架内置实现就不再生效)
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
> ⚠️ **`TileBlock` 本身只是包装器**:它只有 `{ block, entity }`,**没有** `identifier` / `textures`,
|
|
289
|
+
> 也没有继承 `BasicBlock`。所以**不能**把它拿去调 `createBasicBlock` 那一套(`addComponent` / `addPermutation` /
|
|
290
|
+
> `registerState` / `registerTrait` 都不在它身上)——需要这些能力时改调 `.block`(行为侧改 `.entity.behavior`,
|
|
291
|
+
> 外观侧改 `.entity.resource`)。
|
|
292
|
+
> 这也正是 `createTileBlock` 在 `blockFactory.js` 里必须被特殊处理的原因:它注册的是内部的 `block` 与 `entity`
|
|
293
|
+
> 这两个对象,而不是 `TileBlock` 自己。
|
|
294
|
+
|
|
295
|
+
**示例**
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { BlockAPI, BlockComponent, registry } from '@sapdon/core'
|
|
299
|
+
|
|
300
|
+
const machine = BlockAPI.createTileBlock('demo:machine', 'construction', [
|
|
301
|
+
'machine_down', 'machine_up', 'machine_north', 'machine_south', 'machine_west', 'machine_east'
|
|
302
|
+
])
|
|
303
|
+
|
|
304
|
+
// 同时设置方块模型与实体模型(TileBlock 自带的便捷方法)
|
|
305
|
+
machine.setGeometry('geometry.machine')
|
|
306
|
+
|
|
307
|
+
// 继续配方块本体时必须走 .block
|
|
308
|
+
machine.block.addComponent(BlockComponent.setLightEmission(5))
|
|
309
|
+
|
|
310
|
+
registry.submit()
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**容器类方块用哪条路?**
|
|
314
|
+
|
|
315
|
+
| 路线 | 写法 | 引擎现状 |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| ★ **实体(唯一可用)** | `BlockAPI.createTileBlock(id, cat, textures, { inventory_size, container_type, can_be_siphoned_from })`;等价地:`tile.entity.behavior.addComponent(EntityComponent.setInventoryProperties({...}))` | **可用**(实体组件 `minecraft:inventory`) |
|
|
318
|
+
| 方块·规范 | `BlockComponent.setBlockEntity(true, { container: { slot_count } })` → `minecraft:block_entity.container` | **当前引擎版本拒绝**:`-> minecraft:block_entity -> container: … is not present in the Schema`(1.26.30 / 1.26.40 实测同样报错) |
|
|
319
|
+
| 方块·历史 | `BlockComponent.setInventory({...})` → 方块 `components` 里的 `minecraft:inventory` | **必然被拒**(那是**实体**组件):`-> components -> minecraft:inventory: … not present in the Schema`。该 API **已废弃**(产物不变 + 构建期 warn) |
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// 推荐:容器挂实体,槽位数按需给(官方文档对实体容器**没有**上限约束)
|
|
323
|
+
const machine = BlockAPI.createTileBlock('demo:machine', 'construction', textures, {
|
|
324
|
+
inventory_size: 56,
|
|
325
|
+
container_type: 'minecart_chest',
|
|
326
|
+
can_be_siphoned_from: true,
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
> ⚠️ **游戏内行为「未验证」**(本环境无法启动 Minecraft)。真机请确认:右键能打开容器、能存取、
|
|
331
|
+
> 漏斗抽取行为符合预期,以及大槽位(如 56)是否被引擎接受。
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
180
335
|
### createCropBlock
|
|
181
336
|
|
|
182
337
|
创建作物方块。
|
|
@@ -855,6 +1010,17 @@ new TileBlock(
|
|
|
855
1010
|
)
|
|
856
1011
|
```
|
|
857
1012
|
|
|
1013
|
+
**`options` 里的容器参数**(★ 实体容器的唯一可用路线):
|
|
1014
|
+
|
|
1015
|
+
| 参数 | 类型 | 默认 | 说明 |
|
|
1016
|
+
|------|------|------|------|
|
|
1017
|
+
| `options.inventory_size` | `number` | `27` | 实体容器槽位数(正整数,非正整数抛错)。官方文档**未给上限** |
|
|
1018
|
+
| `options.container_type` | `string` | `"minecart_chest"` | 容器音效/行为类型(非空字符串,否则抛错) |
|
|
1019
|
+
| `options.can_be_siphoned_from` | `boolean` | `true` | 能否用漏斗抽取(非布尔抛错) |
|
|
1020
|
+
|
|
1021
|
+
> 不传这三个参数时,`<id>_entity` 的行为产物与历史版本**逐字节一致**;
|
|
1022
|
+
> 每个实例都会**拷贝**一份容器数据,改一个方块不会污染另一个(已用 `tests/block-api.test.mjs` 断言)。
|
|
1023
|
+
|
|
858
1024
|
### 属性
|
|
859
1025
|
|
|
860
1026
|
| 属性 | 类型 | 说明 |
|
|
@@ -1113,6 +1279,146 @@ static setCraftingTable(craftingTags: string[], tableName?: string): Map
|
|
|
1113
1279
|
|
|
1114
1280
|
设置合成台属性(最多 64 个标签)。
|
|
1115
1281
|
|
|
1282
|
+
### setBlockEntity
|
|
1283
|
+
|
|
1284
|
+
```typescript
|
|
1285
|
+
static setBlockEntity(dynamic_properties?: boolean | Object, options?: {
|
|
1286
|
+
dynamic_properties?: boolean
|
|
1287
|
+
container?: { slot_count: number } | { inventory_size: number } | number
|
|
1288
|
+
}): Map
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
创建**方块实体**(Block Entity)。`dynamic_properties` 控制是否启用方块实体的动态属性存储,默认 `false`。
|
|
1292
|
+
|
|
1293
|
+
三种写法(第一、二种产物与历史**逐字节一致**):
|
|
1294
|
+
|
|
1295
|
+
```typescript
|
|
1296
|
+
BlockComponent.setBlockEntity() // → { dynamic_properties: false }
|
|
1297
|
+
BlockComponent.setBlockEntity(true) // → { dynamic_properties: true }
|
|
1298
|
+
BlockComponent.setBlockEntity(true, { container: { slot_count: 27 } })
|
|
1299
|
+
BlockComponent.setBlockEntity({ container: 54 }) // 只给容器时可省掉第一个参数
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
**`container` 参数**(★ 方块容器的**规范**写法)
|
|
1303
|
+
|
|
1304
|
+
| 参数 | 类型 | 说明 |
|
|
1305
|
+
|------|------|------|
|
|
1306
|
+
| `container.slot_count` | `number` | 槽位数。官方文档:`Value must be >= 1. Value must be <= 54.` —— **超限抛错** |
|
|
1307
|
+
| `container.inventory_size` | `number` | `slot_count` 的别名(框架其它容器 API 用这个名字);两者同时给出且不一致时抛错 |
|
|
1308
|
+
| `container` | `number` | 直接给槽位数(等价于 `{ slot_count: n }`) |
|
|
1309
|
+
|
|
1310
|
+
依据:[Microsoft Learn · Block Components - minecraft:block_entity](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/blockreference/examples/blockcomponents/minecraftblock_block_entity)
|
|
1311
|
+
(`container.slot_count` 与 `dynamic_properties` 两个成员都在该页)。
|
|
1312
|
+
|
|
1313
|
+
> ⚠️ **当前引擎版本会拒绝 `container` 成员**:实测报
|
|
1314
|
+
> `-> minecraft:block_entity -> container: this component was found in the input, but is not present in the Schema`
|
|
1315
|
+
> (`format_version` 1.26.30 / 1.26.40 报同样的错)。
|
|
1316
|
+
> ⇒ 这个 API 产出的是**规范正确、但引擎暂时不认**的 JSON;**要现在就能用的容器,请走实体路线**
|
|
1317
|
+
> ([`createTileBlock` 的 `inventory_size`](#createtileblock))。
|
|
1318
|
+
> 真机行为**「未验证」**:等引擎支持后应可直接使用。
|
|
1319
|
+
|
|
1320
|
+
> ⚠️ `combineComponents` 是「后者覆盖前者」:要同时给 `dynamic_properties` 与 `container`,
|
|
1321
|
+
> 请**一次调用写完**(上面第 3、4 种写法);把两个 `setBlockEntity(...)` 合并会让先出现的那个被整份丢掉。
|
|
1322
|
+
|
|
1323
|
+
> ⚠️ 源码 JSDoc 标注该组件为**实验性**,需开启 `Upcoming Creator Features` 实验开关。
|
|
1324
|
+
> Bedrock Wiki 的 [Block Components](https://wiki.bedrock.dev/blocks/block-components) 页**没有**该条目 ——
|
|
1325
|
+
> 引用请用上面的 Microsoft Learn 链接。
|
|
1326
|
+
|
|
1327
|
+
### setInventory
|
|
1328
|
+
|
|
1329
|
+
> ⚠️ **已废弃(deprecated)**,且**当前引擎版本必然拒绝**它产出的组件。新项目**不要**用它。
|
|
1330
|
+
> 保留签名只为不破坏既有项目:**产物一个字节都不变**,但每次调用会打一条构建期 `console.warn`。
|
|
1331
|
+
|
|
1332
|
+
设置方块容器 —— 它产出的是方块 `components` 里的 `minecraft:inventory`。
|
|
1333
|
+
但 `minecraft:inventory` 是**实体**组件(官方文档列在 Entity Components 下),
|
|
1334
|
+
**不是**方块组件 —— 写在方块里**在任何版本上都不成立**(不是"版本太旧"),真机报:
|
|
1335
|
+
|
|
1336
|
+
```
|
|
1337
|
+
-> components -> minecraft:inventory: this component was found in the input, but is not present in the Schema
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
**现在应该怎么做**
|
|
1341
|
+
|
|
1342
|
+
| 目标 | 用什么 |
|
|
1343
|
+
|---|---|
|
|
1344
|
+
| **要一个能用的容器**(唯一可用路线) | `BlockAPI.createTileBlock(id, cat, textures, { inventory_size, container_type, can_be_siphoned_from })` —— 容器挂在承载**实体**上。见 [`createTileBlock`](#createtileblock) |
|
|
1345
|
+
| 已在用 `TileBlock`,想改槽位 | `tile.entity.behavior.addComponent(EntityComponent.setInventoryProperties({ inventorySize: 56, ... }))`(注意是**实体**组件 API,参数是 camelCase) |
|
|
1346
|
+
| **要规范的方块容器 JSON** | `BlockComponent.setBlockEntity(true, { container: { slot_count } })`(`[1,54]`);当前引擎版本同样会拒该成员,但 JSON 与官方文档一致 |
|
|
1347
|
+
|
|
1348
|
+
```typescript
|
|
1349
|
+
static setInventory(options?: {
|
|
1350
|
+
inventory_size: number
|
|
1351
|
+
private?: boolean
|
|
1352
|
+
container_type?: string
|
|
1353
|
+
can_be_siphoned_from?: boolean
|
|
1354
|
+
additional_slots_per_strength?: number
|
|
1355
|
+
restrict_to_owner?: boolean
|
|
1356
|
+
}): Map
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
**参数**
|
|
1360
|
+
|
|
1361
|
+
| 参数 | 类型 | 可选性 | 校验 / 说明 |
|
|
1362
|
+
|------|------|--------|-------------|
|
|
1363
|
+
| `inventory_size` | `number` | **必填** | 槽位数。必须满足 `Number.isInteger(x) && x >= 1`,否则抛错。**上限未验证**(实体组件文档没给上限,见下) |
|
|
1364
|
+
| `private` | `boolean` | 可选 | 是否仅所有者可访问。非布尔抛错 |
|
|
1365
|
+
| `container_type` | `string` | 可选 | 容器音效 / 行为类型。必须是非空字符串。官方文档(实体 `minecraft:inventory`)列出的取值:`horse` / `minecart_chest` / `chest_boat` / `minecart_hopper` / `inventory` / `container` / `hopper`。框架**不做白名单**(避免写死过窄),所以拼错不会在构建期报错 |
|
|
1366
|
+
| `can_be_siphoned_from` | `boolean` | 可选 | 能否用漏斗抽取 |
|
|
1367
|
+
| `additional_slots_per_strength` | `number` | 可选 | 每级强度的额外槽位。必须是非负整数,否则抛错 |
|
|
1368
|
+
| `restrict_to_owner` | `boolean` | 可选 | 是否限制为所有者可打开 |
|
|
1369
|
+
|
|
1370
|
+
**没有隐式默认值**:只写入你**显式赋值**的字段,未赋值的字段不会出现在产物 JSON 里(`inventory_size` 除外,它必填)。框架**不会**替你补 `container_type` / `can_be_siphoned_from` 之类的"常见默认值"。
|
|
1371
|
+
|
|
1372
|
+
> ⚠️ **`inventory_size` 的上限「未验证」**:官方文档(实体 `minecraft:inventory`)只写
|
|
1373
|
+
> "Number of slots the container has"、**未给上限**;方块路线的 `[1,54]` **不适用**于实体容器。
|
|
1374
|
+
> 框架只校验正整数。需要几十槽的大容器时请自行在真机确认。
|
|
1375
|
+
|
|
1376
|
+
#### 自检(`BasicBlock.validate()`)
|
|
1377
|
+
|
|
1378
|
+
提交前(`registry.submit()`)框架会检查两件事,**只 warn、不改产物**:
|
|
1379
|
+
|
|
1380
|
+
1. 方块 components 里出现 `minecraft:inventory` → 提示"它是实体组件、必然被拒",并给出上面两条替代路线。
|
|
1381
|
+
```
|
|
1382
|
+
[sapdon] 方块 "demo:crate" 的 components 里写了 minecraft:inventory —— 它是**实体**组件(官方文档列在 Entity Components 下),
|
|
1383
|
+
方块组件表里没有它,引擎会报 "-> components -> minecraft:inventory: this component was found in the input,
|
|
1384
|
+
but is not present in the Schema"。可用的容器请走实体路线:BlockAPI.createTileBlock(...);
|
|
1385
|
+
要方块侧的规范写法用 BlockComponent.setBlockEntity(true, { container: { slot_count } })(当前引擎版本同样会拒该成员)。
|
|
1386
|
+
```
|
|
1387
|
+
这条能兜住**手写裸 Map** 的写法(项目侧曾经就是那样绕过框架的)。
|
|
1388
|
+
2. `minecraft:block_entity.container.slot_count` 不在 `[1,54]` → warn(兜住手写组件对象 / 裸 Map 的写法)。
|
|
1389
|
+
|
|
1390
|
+
> 检查点在 `src/core/block/basicBlock.js` 的 `BasicBlock.validate()`,由 `src/core/registry.ts` 的 `runValidators()` 调用。
|
|
1391
|
+
> ⚠️ 它**不在** `blockFactory.registerBlock`(旧 JSDoc 曾那样写,是错的:`blockFactory.js` 里 `block_entity` 命中数为 0)。
|
|
1392
|
+
|
|
1393
|
+
#### 历史写法(不再推荐,且产出会被引擎拒绝)
|
|
1394
|
+
|
|
1395
|
+
```typescript
|
|
1396
|
+
// ❌ 不要再用:minecraft:inventory 是实体组件,写在方块里必然被拒
|
|
1397
|
+
crate.addComponent(BlockComponent.combineComponents(
|
|
1398
|
+
BlockComponent.setBlockEntity(false),
|
|
1399
|
+
BlockComponent.setInventory({ inventory_size: 27, container_type: 'minecart_chest' })
|
|
1400
|
+
))
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
#### 可用写法
|
|
1404
|
+
|
|
1405
|
+
```typescript
|
|
1406
|
+
import { BlockAPI, EntityComponent, registry } from '@sapdon/core'
|
|
1407
|
+
|
|
1408
|
+
// ★ 唯一可用:方块 + 承载实体,容器挂实体
|
|
1409
|
+
const crate = BlockAPI.createTileBlock('demo:crate', 'construction', [
|
|
1410
|
+
'crate_down', 'crate_up', 'crate_north', 'crate_south', 'crate_west', 'crate_east'
|
|
1411
|
+
], { inventory_size: 27, container_type: 'minecart_chest', can_be_siphoned_from: true })
|
|
1412
|
+
|
|
1413
|
+
// 或者构造后再改(实体组件 API,camelCase)
|
|
1414
|
+
crate.entity.behavior.addComponent(EntityComponent.setInventoryProperties({
|
|
1415
|
+
inventorySize: 56, containerType: 'minecart_chest', canBeSiphonedFrom: true
|
|
1416
|
+
}))
|
|
1417
|
+
|
|
1418
|
+
registry.submit()
|
|
1419
|
+
```
|
|
1420
|
+
> ⚠️ 上例的**游戏内表现(右键能否打开、能否存取)未验证**——本环境无法启动 Minecraft。
|
|
1421
|
+
|
|
1116
1422
|
### setTick
|
|
1117
1423
|
|
|
1118
1424
|
```typescript
|
|
@@ -1355,6 +1661,39 @@ import { registerBuiltinComponents } from '@sapdon/runtime'
|
|
|
1355
1661
|
registerBuiltinComponents()
|
|
1356
1662
|
```
|
|
1357
1663
|
|
|
1664
|
+
### 两条路线的分工(★ 本轮结论)
|
|
1665
|
+
|
|
1666
|
+
自定义组件有**两条**合法路线,**都要保留**:
|
|
1667
|
+
|
|
1668
|
+
| | **路线 A**:`BlockCustomComponentBuilder`(本节) | **路线 B**:运行期注册(**推荐**) |
|
|
1669
|
+
|---|---|---|
|
|
1670
|
+
| 声明位置 | **构建期**(`main.ts`) | **运行期**(`scripts/*`,随脚本打包) |
|
|
1671
|
+
| handler 形态 | 被 `handler.toString()` **序列化成源码**,写进 `scripts/custom_components/<name>.js` | **普通闭包**(你自己模块里的函数) |
|
|
1672
|
+
| 能否 import 共享模块 | ❌ **不能**(见下) | ✅ 能 |
|
|
1673
|
+
| 注册时机 | CLI 生成的 `scripts/custom_components/index.js` 在 `system.beforeEvents.startup` 里注册 | 框架内建 `system.beforeEvents.startup` |
|
|
1674
|
+
| 兼容性 | `build()` / `generateRuntimeCode()` 签名**不变**,`examples/block_demo` 在用 | 本轮新增,见 [`@sapdon/runtime`](./runtime.md) |
|
|
1675
|
+
|
|
1676
|
+
**★ 为什么必须保留路线 B:A 做不了「共享基类」的系统**
|
|
1677
|
+
|
|
1678
|
+
A 的 handler 会被 `toString()` 序列化进生成的脚本文件,而生成的源码里**只有调用、没有定义也没有 import** ⇒ handler 内**不能引用跨模块的变量**,否则运行期报 `ReferenceError: x is not defined`。
|
|
1679
|
+
|
|
1680
|
+
所以 A 路线**做不了需要共享模块的系统** —— 典型是「20 台机器共用一个 `MachineBase`」:20 个 handler 都得 import 同一份基类 / 工具函数,A 路线写不出来。B 路线的 handler 是**普通闭包**,与 `MachineBase` 处在同一个 bundle 里,直接 `import` 即可。**这是保留 B 的唯一决定性理由。**
|
|
1681
|
+
|
|
1682
|
+
**时机红线:只有上面这两条路**
|
|
1683
|
+
|
|
1684
|
+
- ✅ 路线 A:`BlockCustomComponentBuilder` + CLI 生成的 `scripts/custom_components/index.js`
|
|
1685
|
+
- ✅ 路线 B:运行期 `registerBlockComponent` / `registerItemComponent`
|
|
1686
|
+
- ❌ `world.beforeEvents.worldInitialize`:**太晚**。症状是启动时方块报
|
|
1687
|
+
`this component was found in the input, but is not present in the Schema`
|
|
1688
|
+
(方块 JSON 里写了 `"ns:xxx": {}`,但脚本没在正确时机注册)
|
|
1689
|
+
|
|
1690
|
+
**路线 A 的其它现状**(详述见 [doc/dev/known-pitfalls.md](../../dev/known-pitfalls.md) §2):
|
|
1691
|
+
|
|
1692
|
+
- **源码已支持(依据 `prod/cli/start.js` 的索引合并分支;未端到端实跑路线 A)**:
|
|
1693
|
+
`scripts/custom_components/index.js` 已改为**标记块合并** —— 文件存在但没有标记块时在**末尾追加**自动注册块(原内容保留),有标记块时每次构建**替换标记块区间**(幂等)。历史上"只在文件不存在时才生成 / 增量新增组件时索引不更新"的问题已由这次改动覆盖。
|
|
1694
|
+
- **设计如此(不是缺陷)**:`scripts/custom_components/<name>.js` 是「**生成一次、之后归用户**」(存在即跳过、不覆盖),所以改完 handler 后该文件不会自动跟着更新 —— 覆盖会毁掉用户手写的实现。
|
|
1695
|
+
- **未修(结构性限制)**:`handler.toString()` 丢失跨模块引用,即上面那条"必须保留 B"的理由。
|
|
1696
|
+
|
|
1358
1697
|
---
|
|
1359
1698
|
|
|
1360
1699
|
## Permutation
|
|
@@ -1496,6 +1835,9 @@ BlockAPI.createBasicBlock(identifier, category, textures_arr, options?)
|
|
|
1496
1835
|
BlockAPI.createBlock(identifier, category, variantDatas, options?)
|
|
1497
1836
|
BlockAPI.createRotatableBlock(identifier, category, textures_arr, options?)
|
|
1498
1837
|
BlockAPI.createGeometryBlock(identifier, category, geometry, material_instances, options?)
|
|
1838
|
+
BlockAPI.createTileBlock(identifier, category, textures_arr, options?)
|
|
1839
|
+
// options?: { group?, hide_in_command?, format_version?, entity_texture?,
|
|
1840
|
+
// inventory_size?, container_type?, can_be_siphoned_from? } ← 全部**可选**
|
|
1499
1841
|
BlockAPI.createCropBlock(identifier, category, variantDatas, options?)
|
|
1500
1842
|
BlockAPI.createOreBlock(identifier, category, textures_arr, options?)
|
|
1501
1843
|
BlockAPI.createGlassBlock(identifier, category, texture, options?)
|