sapdon 3.2.2 → 3.4.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.
Files changed (76) hide show
  1. package/README.md +255 -121
  2. package/doc/dev/architecture.md +418 -0
  3. package/doc/dev/cli.md +467 -0
  4. package/doc/dev/core.md +751 -0
  5. package/doc/dev/lr-paradigm.md +85 -0
  6. package/doc/dev/oc.md +582 -0
  7. package/doc/dev/workflow.md +257 -0
  8. package/doc/hello_sapdon/hello_sapdon.md +3 -3
  9. package/doc/user/api/biome.md +558 -0
  10. package/doc/user/api/block.md +1530 -0
  11. package/doc/user/api/entity.md +685 -0
  12. package/doc/user/api/extra.md +231 -0
  13. package/doc/user/api/item.md +1125 -0
  14. package/doc/user/api/neo-guidebook.md +409 -0
  15. package/doc/user/api/recipe.md +427 -0
  16. package/doc/user/api/sapdon-ui.md +185 -0
  17. package/doc/user/api/texture.md +181 -0
  18. package/doc/user/config/build-config.md +83 -0
  19. package/doc/user/config/mod-info.md +33 -0
  20. package/doc/user/faq.md +160 -0
  21. package/doc/user/quick-start.md +122 -0
  22. package/doc/user/tutorials/block.md +474 -0
  23. package/doc/user/tutorials/entity.md +356 -0
  24. package/doc/user/tutorials/item.md +449 -0
  25. package/doc/user/tutorials/neo-guidebook-experience.md +381 -0
  26. package/doc/user/tutorials/neo-guidebook.md +640 -0
  27. package/doc/user/tutorials/recipe.md +278 -0
  28. package/doc/user/tutorials/sapdon-ui.md +207 -0
  29. package/package.json +8 -3
  30. package/prod/cli/index.js +1 -1
  31. package/prod/cli/start.js +1 -1458
  32. package/prod/core/index.d.ts +4564 -2166
  33. package/prod/core/index.js +1 -59015
  34. package/prod/core/package.json +7 -0
  35. package/prod/oc/index.d.ts +350 -108
  36. package/prod/oc/index.js +1 -1
  37. package/prod/oc/package.json +7 -0
  38. package/prod/utils/index.d.ts +20 -9
  39. package/prod/utils/index.js +1 -1
  40. package/prod/utils/package.json +7 -0
  41. package/doc/BlockAPI.md +0 -145
  42. package/doc/api.md +0 -128
  43. package/doc/oc/index.md +0 -0
  44. package/doc/sapdon-ts.md +0 -64
  45. package/src/templates/js_sapdon/build.config +0 -23
  46. package/src/templates/js_sapdon/main.mjs +0 -4
  47. package/src/templates/js_sapdon/mod.info +0 -7
  48. package/src/templates/js_sapdon/pack_icon.png +0 -0
  49. package/src/templates/js_sapdon/package.json +0 -20
  50. package/src/templates/js_sapdon/res/animations/animation_item.animation.json +0 -34
  51. package/src/templates/js_sapdon/res/animations/large_item.animation.json +0 -27
  52. package/src/templates/js_sapdon/res/models/blocks/crop.geo.json +0 -48
  53. package/src/templates/js_sapdon/res/models/entity/animation/animation_item.geo.json +0 -26
  54. package/src/templates/js_sapdon/res/models/entity/animation/large_item.geo.json +0 -28
  55. package/src/templates/js_sapdon/res/textures/blocks/none.png +0 -0
  56. package/src/templates/js_sapdon/res/textures/blocks/test_log_oak.png +0 -0
  57. package/src/templates/js_sapdon/res/textures/blocks/test_log_top.png +0 -0
  58. package/src/templates/js_sapdon/res/textures/items/masterball.png +0 -0
  59. package/src/templates/js_sapdon/scripts/custom_components/cropComponent.js +0 -50
  60. package/src/templates/js_sapdon/scripts/custom_components/items/gui_book.js +0 -37
  61. package/src/templates/js_sapdon/scripts/custom_components/registry.js +0 -25
  62. package/src/templates/js_sapdon/scripts/index.js +0 -0
  63. package/src/templates/ts_sapdon/build.config +0 -23
  64. package/src/templates/ts_sapdon/main.ts +0 -11
  65. package/src/templates/ts_sapdon/mod.info +0 -7
  66. package/src/templates/ts_sapdon/pack_icon.png +0 -0
  67. package/src/templates/ts_sapdon/package.json +0 -20
  68. package/src/templates/ts_sapdon/res/models/blocks/crop.geo.json +0 -48
  69. package/src/templates/ts_sapdon/res/textures/blocks/test_log_oak.png +0 -0
  70. package/src/templates/ts_sapdon/res/textures/blocks/test_log_top.png +0 -0
  71. package/src/templates/ts_sapdon/res/textures/items/masterball.png +0 -0
  72. package/src/templates/ts_sapdon/scripts/components/cropComponent.ts +0 -44
  73. package/src/templates/ts_sapdon/scripts/components/items/guiBook.ts +0 -36
  74. package/src/templates/ts_sapdon/scripts/components/registry.ts +0 -24
  75. package/src/templates/ts_sapdon/scripts/index.ts +0 -7
  76. package/src/templates/ts_sapdon/tsconfig.json +0 -117
@@ -0,0 +1,381 @@
1
+ # NeoGuidebook 实战经验教程
2
+
3
+ > 基于 `examples/digitCircuit` 真实接入跑通的实战总结。API 细节见 `api/neo-guidebook.md`;本文聚焦**能直接照抄的流程**和**踩过的坑**。
4
+
5
+ ---
6
+
7
+ ## 目录
8
+
9
+ 1. [真实可用的 API 速览](#1-真实可用的-api-速览)
10
+ 2. [完整工作流:构建时 + 运行时](#2-完整工作流构建时--运行时)
11
+ 3. [构建时:定义书页(main.mjs)](#3-构建时定义书页mainmjs)
12
+ 4. [运行时:物品开书(scripts/index.js)](#4-运行时物品开书scriptsindexjs)
13
+ 5. [必须避免的坑](#5-必须避免的坑)
14
+ 6. [验证清单](#6-验证清单)
15
+
16
+ ---
17
+
18
+ ## 1. 真实可用的 API 速览
19
+
20
+ 框架在 `src/core/ui/systems/neoGuibook/` 提供两个高层类,构建时(main)使用:
21
+
22
+ | 类 | 用途 |
23
+ |---|---|
24
+ | `NeoGuidebook(identifier, path, size?, options?)` | 整本书:注册页面、按钮、背景 |
25
+ | `NeoGuidebookPage(id, size?)` | 单页内容:标题/正文/分割线/章节/分类 |
26
+
27
+ ```js
28
+ // main.mjs(构建时)
29
+ import { NeoGuidebook, NeoGuidebookPage } from '@sapdon/core'
30
+
31
+ const book = new NeoGuidebook("my_mod:guidebook", "ui/", [320, 207], {
32
+ debug: false,
33
+ buttons: { prev: { visible: true }, next: { visible: true }, home: { visible: true }, close: { visible: true } },
34
+ // textures: { prevDefault: "...", ... } // 可选自定义按钮贴图
35
+ })
36
+ ```
37
+
38
+ ### NeoGuidebook 方法
39
+
40
+ | 方法 | 说明 |
41
+ |---|---|
42
+ | `addDoublePageStack(page_id, leftPanel, rightPanel, ratio?)` | 双页跨页,`ratio` 默认 `["50%","100%"]` |
43
+ | `addSinglePageStack(page_id, panel)` | 单页铺满 |
44
+ | `addCustomButton(config)` | 自定义按钮(id/position/offset/size/纹理/bindingButtonName) |
45
+ | `getPageIds(): string[]` | 全部页面 id,用于生成运行时清单 |
46
+ | `getPageCount(): number` | 页数 |
47
+
48
+ ### NeoGuidebookPage 方法(链式)
49
+
50
+ | 方法 | 默认尺寸 | 说明 |
51
+ |---|---|---|
52
+ | `addCategoryTitle(text, size?)` | `["100%","10%"]` | 居中标题 |
53
+ | `addBookTitleBar(text, size?)` | `["100%","15%"]` | 顶部横幅标题 |
54
+ | `addBookText(text, size?)` | `["100%","15%"]` | 左对齐正文(支持 `\n` 换行) |
55
+ | `addEmptySpace(size?)` | `["100%","5%"]` | 空占位 |
56
+ | `addDivider(size?)` | `["100%","5%"]` | 分割线 |
57
+ | `addRecipeGrid(row, col, items, size?)` | `["100%","30%"]` | 配方网格(纹理路径数组) |
58
+ | `addBookCategory(title, row, col, buttons, size?)` | — | 物品分类网格 |
59
+ | `addChapter(name, texture)` / `addChapters([...])` | — | 章节目录项 |
60
+ | `buildChapterList(prefix?)` | — | 生成"Chapters"目录块(`prefix` 默认 `"item"`,多级目录换前缀避免按钮 id 冲突) |
61
+ | `addControl(control)` | — | 添加任意自定义 UI 控件(透传 panel) |
62
+ | `addStack(size, control, debug?)` | — | 自定义控件 + 占位尺寸(透传 StackPanel.addStack) |
63
+ | `getPanel(): Panel` | — | 交给 `addDoublePageStack` 用 |
64
+
65
+ > **注意**:`NeoGuidebookPage.addChapter/addChapters` 只是**声明数据**,必须再调 `buildChapterList()` 才会渲染成目录块。每页可调多次 `buildChapterList(prefix)`,不同 prefix 生成不同前缀的按钮键(如目录页 `item_0_button`、子目录页 `sub_0_button`),避免 JSON UI 元素 id 全局冲突。
66
+
67
+ ---
68
+
69
+ ## 3.5 添加自定义控件
70
+
71
+ 内置的 `addBookText/addCategoryTitle` 等只覆盖常用布局。要放任意自定义控件,用 `addControl`(无尺寸包裹)或 `addStack`(指定占位尺寸):
72
+
73
+ ```js
74
+ // main.mjs 构建时
75
+ import { NeoGuidebookPage, Label, Text, Control, Image, Sprite, Layout } from '@sapdon/core'
76
+
77
+ const page = new NeoGuidebookPage("customPage")
78
+ .addCategoryTitle("自定义控件", ["100%", "12%"])
79
+
80
+ // 1) addControl:直接加一个控件
81
+ page.addControl(
82
+ new Label("my_label", undefined)
83
+ .setText(new Text().setText("这是一段自定义文字").setColor([0, 0, 0]))
84
+ .setControl(new Control().setLayer(5))
85
+ )
86
+
87
+ // 2) addStack:控件 + 占位尺寸(推荐,可控制位置)
88
+ page.addStack(["100%", "20%"],
89
+ new Image("my_img", undefined).setSprite(new Sprite().setTexture("textures/items/iron_ingot"))
90
+ )
91
+
92
+ // 3) 或直接拿到底层 panel 操作
93
+ page.getPanel().addStack(["100%", "15%"], someControl)
94
+ ```
95
+
96
+ `addControl`/`addStack` 是 `NeoGuidebookPage` 直接透传给内部 StackPanel 的,所以能放任何 `UIElement`(Label/Image/Button/StackPanel…),也支持原生 JSON 控件对象:
97
+
98
+ ```js
99
+ page.addControl({
100
+ "my_raw_control@common.some_template": { "size": ["50%", "30%"] }
101
+ })
102
+ ```
103
+
104
+ > 自定义控件同样会被 `addDoublePageStack(page_id, panel, ...)` 正确渲染;`addControl` 紧跟在封装方法后面即可,顺序就是渲染顺序。
105
+
106
+ ---
107
+
108
+ ## 2. 完整工作流:构建时 + 运行时
109
+
110
+ ```
111
+ 构建时 (main.mjs) 运行时 (scripts/index.js)
112
+ ────────────────── ──────────────────────────
113
+ NeoGuidebook(...) 定义书页 物品 custom component
114
+ → 自动写 RP/ui/<name>.json onUse →
115
+ → 自动更新 RP/ui/server_form.json new ActionFormData()
116
+ → fs.writeFileSync(guide_pages.js) .title("<书名>")
117
+ getPageIds() 写 scripts/guide_pages.js .body(page_id)
118
+ .button("prev_button") ...
119
+ ```
120
+
121
+ 三个产物必须同时存在,缺一 UI 打不开:
122
+
123
+ | 产物 | 作用 | 谁生成 |
124
+ |---|---|---|
125
+ | `dev/<proj>_RP/ui/<name>.json` | 书页 UI 控件 | `registry.submit()` 自动 |
126
+ | `dev/<proj>_RP/ui/server_form.json` | title 绑定 + 按钮工厂 | `ServerUISystem.bindingTitlewithContent` 自动 |
127
+ | `dev/<proj>_RP/ui/_ui_defs.json` | 声明所有 UI 文件 | `UISystemRegistry` 自动 |
128
+
129
+ > `server_form.json` 的 title 绑定是**累加**的(`#title_text - 'guidebook'`),加第二个书会自动并列,互不冲突。
130
+
131
+ ---
132
+
133
+ ## 3. 构建时:定义书页(main.mjs)
134
+
135
+ ### 3.1 物品
136
+
137
+ ```js
138
+ ItemAPI.createItem("my_mod:guidebook", ItemCategory.Equipment, "book_writable", {
139
+ maxStackSize: 1,
140
+ group: GROUP_TOOL,
141
+ formatVersion: "1.21.90",
142
+ }).addComponent(
143
+ ItemComponent.combineComponents(
144
+ ItemComponent.setDisplayName("我的手册"),
145
+ ItemComponent.setInteractButton("打开手册"), // 必须!否则 onUse 不触发
146
+ ItemComponent.setHandEquipped(true),
147
+ ItemComponent.setCustomComponents(["my_mod:guidebook"]) // 组件名与运行时一致
148
+ )
149
+ )
150
+ ```
151
+
152
+ ### 3.2 书页
153
+
154
+ ```js
155
+ const cover = new NeoGuidebookPage("cover")
156
+ .addEmptySpace(["100%", "8%"])
157
+ .addBookTitleBar("我的手册\n 使用指导", ["100%", "18%"])
158
+ .addBookText("这是正文。\n支持多行。", ["100%", "46%"])
159
+
160
+ const toc = new NeoGuidebookPage("toc")
161
+ .addChapters([
162
+ { chapter_name: "第一章", chapter_texture: "textures/items/iron_ingot" },
163
+ { chapter_name: "第二章", chapter_texture: "textures/items/stick" },
164
+ ])
165
+ .buildChapterList()
166
+
167
+ book.addDoublePageStack("page_index0", cover.getPanel(), toc.getPanel())
168
+ book.addDoublePageStack("page_index1", leftPage.getPanel(), rightPage.getPanel())
169
+ ```
170
+
171
+ ### 3.3 生成页面清单(必须写成 .js)
172
+
173
+ ```js
174
+ const pageIds = book.getPageIds()
175
+ fs.writeFileSync(
176
+ path.join(process.cwd(), "scripts", "guide_pages.js"),
177
+ "export const PAGE_IDS = " + JSON.stringify(pageIds, null, 2) + ";\n"
178
+ )
179
+ ```
180
+
181
+ > 运行时导入路径固定为 `./guide_pages.js`(见下节),文件名别改。
182
+
183
+ ---
184
+
185
+ ## 4. 运行时:物品开书(scripts/index.js)
186
+
187
+ ```js
188
+ import { ActionFormData } from "@minecraft/server-ui"
189
+ import { PAGE_IDS, PAGE_NAV } from "./guide_pages.js"
190
+
191
+ // startup 回调内:
192
+ init.itemComponentRegistry.registerCustomComponent("my_mod:guidebook", {
193
+ onUse(event) {
194
+ const player = event.source
195
+ if (!player || player.typeId !== "minecraft:player") return
196
+ openGuidebook(player, 0)
197
+ },
198
+ })
199
+
200
+ function openGuidebook(player, index) {
201
+ const ids = PAGE_IDS.length ? PAGE_IDS : ["page_index0"]
202
+ const current = Math.max(0, Math.min(index, ids.length - 1))
203
+ const pageId = ids[current]
204
+
205
+ const form = new ActionFormData()
206
+ .title("guidebook") // ← identifier 的 name 部分,不带命名空间!
207
+ .body(pageId) // ← 必须是 addDoublePageStack 的 page_id
208
+
209
+ const actions = []
210
+ // 数据驱动的页面跳转按钮(PAGE_NAV:binding 键 -> 目标页)
211
+ const nav = PAGE_NAV && PAGE_NAV[pageId]
212
+ if (Array.isArray(nav)) {
213
+ for (const item of nav) {
214
+ form.button(item.key)
215
+ actions.push(`goto:${item.target}`)
216
+ }
217
+ }
218
+ if (current > 0) { form.button("prev_button"); actions.push("prev") }
219
+ if (current < ids.length - 1) { form.button("next_button"); actions.push("next") }
220
+ if (current !== 0) { form.button("home_button"); actions.push("home") }
221
+
222
+ form.show(player).then((response) => {
223
+ if (response.canceled) return
224
+ const action = actions[response.selection]
225
+ if (!action) return
226
+ if (action === "prev") openGuidebook(player, current - 1)
227
+ else if (action === "next") openGuidebook(player, current + 1)
228
+ else if (action === "home") openGuidebook(player, 0)
229
+ else if (action.startsWith("goto:")) openGuidebook(player, parseInt(action.split(":")[1], 10))
230
+ }).catch(() => {})
231
+ }
232
+ ```
233
+
234
+ ### 按钮文字是"键名"不是显示文本
235
+
236
+ `form.button("prev_button")` 的参数必须与 JSON UI 里按钮的 `$binding_button_text` 严格一致,否则按钮不显示/不可点。内置按钮键名:
237
+
238
+ | 键名 | 行为 |
239
+ |---|---|
240
+ | `prev_button` | 上一页 |
241
+ | `next_button` | 下一页 |
242
+ | `home_button` | 回首页 |
243
+ | `item_<N>_button` | 目录跳转(`page_index0` 章节按钮,目标由 `PAGE_NAV` 决定) |
244
+ | `sub_<N>_button` | 子目录跳转(`buildChapterList("sub")` 生成的分类按钮) |
245
+
246
+ ### 多级目录/返回上一级(以 digitCircuit 方块总览为例)
247
+
248
+ 核心是**任意页都能有跳转按钮**:UI 侧用 `buildChapterList(prefix)` 生成章节按钮(键名 `item_*`/`sub_*`),运行时用 `PAGE_NAV` 把键名映射到目标页 index。子分类页**不再放自定义 `back_button`**,直接用原生 `prev_button`,并用 `PAGE_PREV` 把 prev 的目标指向上一级(方块总览分类页),少一层按钮更简洁。
249
+
250
+ ```js
251
+ // main.mjs 构建时
252
+ import { NeoGuidebook, NeoGuidebookPage, ServerFormButton } from '@sapdon/core'
253
+
254
+ // 1) 方块总览分类页:子目录用 sub 前缀,避免与目录页 item_* id 冲突
255
+ const nav = new NeoGuidebookPage("blocksNavRight")
256
+ .addChapters([
257
+ { chapter_name: "信号源", chapter_texture: "textures/blocks/on" },
258
+ { chapter_name: "逻辑门", chapter_texture: "textures/blocks/and" },
259
+ // ...
260
+ ])
261
+ .buildChapterList("sub") // ← 生成 sub_0_button..
262
+
263
+ // 2) 子分类页(如信号源):左页静态 list 展示(图标+名字+一句话),不加返回按钮
264
+ // iconRow 用 StackPanel + Image/Sprite/Label 拼一行,addStack 铺到左页
265
+ function iconRow(tex, name, desc) {
266
+ return new StackPanel(undefined, undefined)
267
+ .setOrientation("horizontal")
268
+ .addStack(["16%", "100%"], new Image("icon", undefined)
269
+ .setSprite(new Sprite().setTexture(`textures/blocks/${tex}`)))
270
+ .addStack(["84%", "100%"], new Label("row_text", undefined)
271
+ .setText(new Text().setText(`${name}\n${desc}`).setColor([0, 0, 0])))
272
+ }
273
+ const pageSourceL = new NeoGuidebookPage("pageSourceL")
274
+ .addEmptySpace(["100%", "3%"])
275
+ .addCategoryTitle("信号源", ["100%", "10%"])
276
+ .addDivider(["100%", "2%"])
277
+ pageSourceL.addStack(["100%", "14%"], iconRow("on", "on_signal", "恒输出 1"))
278
+ pageSourceL.addStack(["100%", "14%"], iconRow("off", "off_signal", "恒输出 0"))
279
+ // ...每行一个 addStack;右页放"输入/输出面"说明文字
280
+
281
+ // 3) 导航数据:生成 guide_pages.js 时附上 PAGE_NAV + PAGE_PREV(目标存 index)
282
+ fs.writeFileSync(path.join(process.cwd(), "scripts", "guide_pages.js"),
283
+ "export const PAGE_IDS = " + JSON.stringify(pageIds, null, 2) + ";\n" +
284
+ "export const PAGE_NAV = " + JSON.stringify({
285
+ page_index0: [ // 目录页:item_0 → 方块总览分类页
286
+ { key: "item_0_button", target: pageIds.indexOf("page_index1") },
287
+ { key: "item_1_button", target: pageIds.indexOf("page_index2") },
288
+ // ...
289
+ ],
290
+ page_index1: [ // 方块总览分类页:sub_0 → 信号源
291
+ { key: "sub_0_button", target: pageIds.indexOf("page_source") },
292
+ // ...
293
+ ],
294
+ // 子分类页无需 PAGE_NAV,返回交给 prev
295
+ }, null, 2) + ";\n" +
296
+ "export const PAGE_PREV = " + JSON.stringify({
297
+ // 子分类页的 prev 一律回方块总览分类页;不在此表的页走线性 prev
298
+ page_source: pageIds.indexOf("page_index1"),
299
+ page_gate: pageIds.indexOf("page_index1"),
300
+ // ...
301
+ }, null, 2) + ";\n")
302
+ ```
303
+
304
+ `openGuidebook` 打开任意页时会查 `PAGE_NAV[page_id]`,渲染页面对应的跳转按钮;没有配置的页只显示 prev/next/home。`prev_button` 按下时:若 `PAGE_PREV[page_id]` 有值就跳转到它,否则 `current - 1`。
305
+
306
+ > 文本排版:中文一行约 16 汉字,超长应手动用 `\n` 拆行;子分类页 list 每行「图标 + 名称 + 一句话」最省空间,避免文字溢出按钮/越界。
307
+
308
+ ---
309
+
310
+ ## 5. 必须避免的坑
311
+
312
+ ### 坑 1:`title()` 参数是 identifier 的 name 部分
313
+
314
+ `new NeoGuidebook("sapdon:guidebook", ...)` → `ActionFormData.title("guidebook")`。
315
+ 带命名空间(`"sapdon:guidebook"`)会匹配不上 server_form 绑定 → 显示原版表单而不是自定义 UI。
316
+
317
+ ### 坑 2:页面清单必须写 `.js`,不能写 `.json`
318
+
319
+ sapdon 的 dev server 把 scripts 做**模块拼接打包**(不是 webpack/esbuild),`import("./page_ids.json", {with:{type:"json"}})` 这种动态导入不会被处理,运行时直接 undefined。正确做法是 main.mjs 生成 `export const PAGE_IDS = [...]` 的 `.js` 文件,静态 `import` 会被拼进产物。
320
+
321
+ ### 坑 3:新增依赖后必须删 manifest 重建
322
+
323
+ manifest.json 只在**首次构建**时生成,之后 build 不会自动追加新依赖。加了 `@minecraft/server-ui` 后不删旧 manifest,游戏报 `Module [@minecraft/server-ui] is unrecognized`(或 version conflict),**整个脚本 context 创建失败、所有脚本都不跑**。
324
+
325
+ ```bash
326
+ Remove-Item dev/<proj>_BP/manifest.json, dev/<proj>_RP/manifest.json -Force
327
+ npm run build
328
+ ```
329
+
330
+ ### 坑 4:server-ui 版本要匹配 server
331
+
332
+ `@minecraft/server` 2.6.0 必须配 `@minecraft/server-ui` **2.x**(如 2.1.0)。1.x 的 server-ui-bindings 内部要 server 1.3.0,与 2.6.0 冲突 → `version conflict for module @minecraft/server`。
333
+
334
+ ### 坑 5:物品必须有 `interact_button` 才能触发 onUse
335
+
336
+ 自定义组件 `onUse` 依赖 `minecraft:interact_button` 存在;不加则手持右键无反应。用 `ItemComponent.setInteractButton("打开手册")`。
337
+
338
+ ### 坑 6:icon 引用原版纹理名要查原版 item_texture.json
339
+
340
+ `minecraft:icon` 的值必须匹配原版 `resource_pack/textures/item_texture.json` 的 `texture_data` 键,否则报 `Missing referenced asset xxx`。**原版书纹理名是 `book_writable`,没有 `book`**。`stick`、`iron_ingot` 等原版物品名通常可用。
341
+
342
+ ### 坑 7:`addChapter/addChapters` 需配 `buildChapterList()`
343
+
344
+ 只 `addChapters([...])` 不调 `buildChapterList()`,目录块不会渲染。
345
+
346
+ ### 坑 8:调试时查看 ContentLog
347
+
348
+ 游戏内无报错弹窗,运行时错误在 `<APPDATA>\Minecraft Bedrock\logs\ContentLog*.txt`。搜索关键字:`guidebook`、`server-ui`、`Missing referenced`、`version conflict`、`failed to create context`。该路径文件不能用 Grep 工具搜,用 PowerShell `Select-String -Path`。
349
+
350
+ ### 坑 9:部署后要重进世界加载新包
351
+
352
+ 游戏 dev 包同步后需重新进入世界让 addon 重载;`manifest.json` 变更(如新增依赖)尤其如此,否则仍在跑旧上下文。
353
+
354
+ JSON UI 里元素 id 全局唯一。目录页若已用默认 `item_N_button`,再在子目录页用 `addChapter...buildChapterList()`(不带前缀)会生成重复 id → UI 报错/按钮错乱。子目录页务必传不同前缀:`buildChapterList("sub")` 生成 `sub_N_button`。子分类页返回直接用 `prev_button`(配 `PAGE_PREV`),一般不需要自定义按钮。
355
+
356
+ ### 坑 11:`goto:` 目标必须是 `PAGE_IDS` 的 index
357
+
358
+ `PAGE_NAV` 的 `target` 存的是页在 `PAGE_IDS` 数组里的下标(`pageIds.indexOf(page_id)`),运行时 `openGuidebook(player, target)` 按 index 定位。切勿直接存页 id 字符串。同理 `PAGE_PREV` 的 value 也是 index。
359
+
360
+ ---
361
+
362
+ ## 6. 验证清单
363
+
364
+ 构建后核对以下文件都存在:
365
+
366
+ ```text
367
+ dev/<proj>_BP/manifest.json # dependencies 含 @minecraft/server-ui(2.x)
368
+ dev/<proj>_BP/items/sapdon_guidebook.json
369
+ dev/<proj>_RP/ui/guidebook.json # 含 page_index0..N_page_panel
370
+ dev/<proj>_RP/ui/server_form.json # 含 source_property_name: ((#title_text - 'guidebook') = #title_text)
371
+ dev/<proj>_RP/ui/_ui_defs.json # ui_defs 含 ui/guidebook.json
372
+ scripts/guide_pages.js # export const PAGE_IDS + PAGE_NAV + PAGE_PREV
373
+ dev/<proj>_BP/scripts/index.js # 含 openGuidebook + registerCustomComponent("...guidebook")
374
+ ```
375
+
376
+ 游戏内验证:
377
+
378
+ 1. `/give @s sapdon:guidebook`
379
+ 2. 手持右键 → 出现书本 UI(翻页按钮 / 目录跳转)
380
+ 3. 若显示的是原版表单 → 检查 title 参数(坑 1)
381
+ 4. 若右键无反应 → 检查 interact_button(坑 5)与 ContentLog(坑 8)