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,640 @@
1
+ # NeoGuidebook 指南书教程
2
+
3
+ 本教程将带你一步步创建一个带自定义 UI 的技术指南手册。
4
+
5
+ ---
6
+
7
+ ## 目录
8
+
9
+ 1. [准备工作](#1-准备工作)
10
+ 2. [创建构建入口](#2-创建构建入口)
11
+ 3. [定义书页内容](#3-定义书页内容)
12
+ 4. [编写运行时脚本](#4-编写运行时脚本)
13
+ 5. [注册物品组件](#5-注册物品组件)
14
+ 6. [构建与部署](#6-构建与部署)
15
+ 7. [完整示例](#7-完整示例)
16
+
17
+ ---
18
+
19
+ ## 1. 准备工作
20
+
21
+ 确保你的项目已经初始化:
22
+
23
+ ```bash
24
+ npm init
25
+ npm install @sapdon/core @sapdon/cli
26
+ npm install @minecraft/server@2.8.0 @minecraft/server-ui@2.1.0
27
+ ```
28
+
29
+ 项目结构:
30
+
31
+ ```
32
+ my_guidebook/
33
+ ├── main.ts # 构建入口(定义 UI 和物品)
34
+ ├── tsconfig.json # TypeScript 配置
35
+ ├── build.config # 构建配置
36
+ ├── mod.info # 模块信息
37
+ ├── package.json
38
+ ├── scripts/
39
+ │ ├── index.ts # 运行时入口
40
+ │ ├── lib/
41
+ │ │ └── page_bridge.ts # Bridge 导航库
42
+ │ ├── custom_components/
43
+ │ │ ├── registry.ts # 组件注册
44
+ │ │ └── items/
45
+ │ │ └── gui_book.ts # 物品交互
46
+ │ └── page_ids.json # 由构建自动生成
47
+ ├── dev/ # 构建输出目录
48
+ │ ├── guidebook_BP/
49
+ │ └── guidebook_RP/
50
+ └── res/ # 资源文件
51
+ ```
52
+
53
+ ### build.config
54
+
55
+ ```json
56
+ {
57
+ "formatVersion": 2,
58
+ "buildOptions": {
59
+ "useHMR": true,
60
+ "buildMode": "dev",
61
+ "buildEntry": "main.ts",
62
+ "scriptEntry": "scripts/index.ts",
63
+ "scriptOutput": "scripts/index.js",
64
+ "useJs": false,
65
+ "buildDir": "dev/",
66
+ "dependencies": [
67
+ { "module_name": "@minecraft/server-ui", "version": "2.1.0" },
68
+ { "module_name": "@minecraft/server", "version": "2.8.0" }
69
+ ],
70
+ "resource": { "path": "res/", "resourceHints": true }
71
+ },
72
+ "versionType": "release"
73
+ }
74
+ ```
75
+
76
+ ### mod.info
77
+
78
+ ```json
79
+ {
80
+ "name": "guidebook",
81
+ "description": "我的技术手册",
82
+ "author": "you",
83
+ "version": "1.0.0",
84
+ "min_engine_version": "1.26.30"
85
+ }
86
+ ```
87
+
88
+ ### tsconfig.json
89
+
90
+ ```json
91
+ {
92
+ "compilerOptions": {
93
+ "module": "ESNext",
94
+ "moduleResolution": "bundler",
95
+ "target": "ESNext",
96
+ "strict": true,
97
+ "outDir": "./dev"
98
+ },
99
+ "include": ["scripts/**/*.ts", "main.ts"]
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 2. 创建构建入口
106
+
107
+ `main.ts` 在构建时运行,负责定义物品和 UI 布局。
108
+
109
+ ```typescript
110
+ import fs from "fs"
111
+ import path from "path"
112
+ import {
113
+ ItemAPI, ItemComponent, ItemCategory, registry,
114
+ NeoGuidebook, NeoGuidebookPage
115
+ } from '@sapdon/core'
116
+
117
+ // 2.1 创建物品
118
+ const item = ItemAPI.createItem("my_mod:guidebook", ItemCategory.Items, "book_icon")
119
+ item.format_version = "1.21.90"
120
+ item.addComponent(ItemComponent.setCustomComponentV2("my_mod:book_component", {}))
121
+ item.addComponent(ItemComponent.setMaxStackSize(1))
122
+ item.addComponent(ItemComponent.setDisplayName("技术手册"))
123
+ item.addComponent(ItemComponent.setInteractButton("打开手册"))
124
+
125
+ // 2.2 创建指南书 UI
126
+ const book = new NeoGuidebook("my_mod:guidebook", "ui/", [320, 207], {
127
+ debug: true,
128
+ buttons: { close: { visible: false } },
129
+ textures: {
130
+ homeDefault: "textures/ui/book_shiftleft_default",
131
+ homeHover: "textures/ui/book_shiftleft_hover",
132
+ homePressed: "textures/ui/book_shiftleft_pressed",
133
+ }
134
+ })
135
+
136
+ // 2.3 注册页面(见下一节)
137
+ // ...
138
+
139
+ // 2.4 生成 page_ids.json
140
+ const pageIds: string[] = book.getPageIds()
141
+ fs.writeFileSync(
142
+ path.join(process.cwd(), "scripts", "page_ids.json"),
143
+ JSON.stringify(pageIds, null, 2)
144
+ )
145
+
146
+ // 2.5 提交注册
147
+ registry.submit()
148
+ ```
149
+
150
+ ---
151
+
152
+ ## 3. 定义书页内容
153
+
154
+ ### 3.1 创建单页
155
+
156
+ ```typescript
157
+ const cover = new NeoGuidebookPage("cover")
158
+ .addEmptySpace(["100%", "5%"])
159
+ .addBookTitleBar("技术手册\n by You", ["100%", "15%"])
160
+ .addEmptySpace(["100%", "3%"])
161
+ .addBookText(
162
+ "这里是手册的介绍内容。\n可以跨越多行。",
163
+ ["100%", "60%"]
164
+ )
165
+ ```
166
+
167
+ ### 3.2 双页布局
168
+
169
+ ```typescript
170
+ // 左页
171
+ const ch1_left = new NeoGuidebookPage("ch1_left")
172
+ .addEmptySpace(["100%", "5%"])
173
+ .addCategoryTitle("第一章", ["100%", "15%"])
174
+ .addDivider(["100%", "3%"])
175
+ .addBookText("第一章的内容...", ["100%", "60%"])
176
+ .addEmptySpace(["100%", "12%"])
177
+
178
+ // 右页
179
+ const ch1_right = new NeoGuidebookPage("ch1_right")
180
+ .addEmptySpace(["100%", "5%"])
181
+ .addCategoryTitle("补充说明", ["100%", "15%"])
182
+ .addDivider(["100%", "3%"])
183
+ .addBookText("补充内容...", ["100%", "65%"])
184
+ .addEmptySpace(["100%", "7%"])
185
+
186
+ // 注册为双页
187
+ book.addDoublePageStack("page_index1", ch1_left.getPanel(), ch1_right.getPanel())
188
+ ```
189
+
190
+ ### 3.3 章节目录
191
+
192
+ ```typescript
193
+ const chapter_list = new NeoGuidebookPage("chapter_list")
194
+ .addChapters([
195
+ { chapter_name: "第一章", chapter_texture: "textures/items/map" },
196
+ { chapter_name: "第二章", chapter_texture: "textures/items/iron_ingot" },
197
+ { chapter_name: "第三章", chapter_texture: "textures/items/book_writable" },
198
+ ])
199
+ .buildChapterList()
200
+
201
+ // 封面 + 目录作为首页
202
+ book.addDoublePageStack("page_index0", cover.getPanel(), chapter_list.getPanel())
203
+ ```
204
+
205
+ ### 3.4 合成表示例
206
+
207
+ ```typescript
208
+ const recipe_page = new NeoGuidebookPage("recipe")
209
+ .addEmptySpace(["100%", "5%"])
210
+ .addCategoryTitle("合成配方", ["100%", "15%"])
211
+ .addDivider(["100%", "3%"])
212
+ .addRecipeGrid(2, 3, [
213
+ "textures/items/iron_ingot",
214
+ "textures/items/iron_ingot",
215
+ "textures/items/iron_ingot",
216
+ "textures/items/stick",
217
+ "textures/items/stick",
218
+ "textures/items/stick",
219
+ ], ["100%", "40%"])
220
+ ```
221
+
222
+ ### 3.5 注册所有页面
223
+
224
+ ```typescript
225
+ // 按 page_index0 ~ page_indexN 顺序注册
226
+ book.addDoublePageStack("page_index0", cover.getPanel(), chapter_list.getPanel())
227
+ book.addDoublePageStack("page_index1", ch1_left.getPanel(), ch1_right.getPanel())
228
+ book.addSinglePageStack("page_index2", recipe_page.getPanel())
229
+ ```
230
+
231
+ ---
232
+
233
+ ## 4. 编写运行时脚本
234
+
235
+ ### 4.1 page_bridge.ts(导航管理器)
236
+
237
+ ```typescript
238
+ import { ActionFormData, ActionFormResponse } from "@minecraft/server-ui"
239
+ import { world, Player } from "@minecraft/server"
240
+
241
+ interface PageCallbacks {
242
+ onEnter?: (pageId: string, index: number) => void
243
+ onLeave?: (pageId: string, index: number) => void
244
+ }
245
+
246
+ interface BridgeOptions {
247
+ debug?: boolean
248
+ }
249
+
250
+ export class NeoGuidebookBridge {
251
+ private uiName: string
252
+ private pageIds: string[]
253
+ private totalPages: number
254
+ private hooks: Map<string, PageCallbacks>
255
+ private currentIndex: number
256
+ private debug: boolean
257
+
258
+ constructor(uiName: string, pageIds: string[], options: BridgeOptions = {}) {
259
+ this.uiName = uiName
260
+ this.pageIds = pageIds
261
+ this.totalPages = pageIds.length
262
+ this.hooks = new Map()
263
+ this.currentIndex = 0
264
+ this.debug = options.debug === true
265
+ }
266
+
267
+ private log(...args: string[]): void {
268
+ if (this.debug) world.sendMessage("[Guidebook] " + args.join(" "))
269
+ }
270
+
271
+ onPage(pageId: string, callbacks: PageCallbacks): this {
272
+ this.hooks.set(pageId, callbacks)
273
+ return this
274
+ }
275
+
276
+ show(player: Player, startIndex: number = 0): void {
277
+ this.currentIndex = Math.max(0, Math.min(startIndex, this.totalPages - 1))
278
+ this.open(player)
279
+ }
280
+
281
+ open(player: Player): void {
282
+ const pageId = this.pageIds[this.currentIndex]
283
+ const hook = this.hooks.get(pageId)
284
+ if (hook?.onEnter) hook.onEnter(pageId, this.currentIndex)
285
+
286
+ const form = new ActionFormData()
287
+ .title(this.uiName)
288
+ .body(pageId)
289
+
290
+ const actions: string[] = []
291
+ const hasPrev = this.currentIndex > 0
292
+ const hasNext = this.currentIndex < this.totalPages - 1
293
+ const hasHome = this.currentIndex !== 0
294
+ const isChapterList = this.currentIndex === 0
295
+
296
+ // 按钮文字必须匹配 JSON UI 的 $binding_button_text
297
+ if (hasPrev) { form.button("prev_button"); actions.push("prev") }
298
+ if (hasNext) { form.button("next_button"); actions.push("next") }
299
+ if (hasHome) { form.button("home_button"); actions.push("home") }
300
+
301
+ // 首页显示章节选择按钮
302
+ if (isChapterList) {
303
+ for (let i = 1; i < this.totalPages; i++) {
304
+ form.button(`item_${i - 1}_button`)
305
+ actions.push(`goto:${i}`)
306
+ }
307
+ }
308
+
309
+ form.show(player).then((response: ActionFormResponse) => {
310
+ if (response.canceled) {
311
+ if (hook?.onLeave) hook.onLeave(pageId, this.currentIndex)
312
+ return
313
+ }
314
+
315
+ const action = actions[response.selection!]
316
+ if (!action) return
317
+
318
+ const prev = this.currentIndex
319
+ if (action === "prev") this.currentIndex--
320
+ else if (action === "next") this.currentIndex++
321
+ else if (action === "home") this.currentIndex = 0
322
+ else if (action.startsWith("goto:"))
323
+ this.currentIndex = parseInt(action.split(":")[1], 10)
324
+
325
+ if (hook?.onLeave) hook.onLeave(pageId, prev)
326
+ if (this.currentIndex !== prev) this.open(player)
327
+ })
328
+ }
329
+ }
330
+ ```
331
+
332
+ > **重要**:`form.button()` 的参数**不是显示文字**,而是 JSON UI 绑定的键名。必须与 `neo_guidebook.json` 中对应按钮的 `$binding_button_text` 一致。
333
+
334
+ ### 4.2 gui_book.ts(物品交互)
335
+
336
+ ```typescript
337
+ import { world, ItemCustomComponent, ItemComponentUseEvent } from "@minecraft/server"
338
+ import { NeoGuidebookBridge } from "../lib/page_bridge"
339
+ import pageIds from "../page_ids.json"
340
+
341
+ const DEBUG: boolean = true
342
+
343
+ const bridge = new NeoGuidebookBridge("sapdon_ui:guidebook", pageIds, { debug: DEBUG })
344
+
345
+ export const GuiBookItemComponent: ItemCustomComponent = {
346
+ onUse(event: ItemComponentUseEvent, params: any): void {
347
+ const player = event.source
348
+ if (player.typeId != "minecraft:player") return
349
+ bridge.show(player, 0)
350
+ }
351
+ }
352
+ ```
353
+
354
+ ---
355
+
356
+ ## 5. 注册物品组件
357
+
358
+ ### 5.1 registry.ts
359
+
360
+ ```typescript
361
+ import { GuiBookItemComponent } from "./items/gui_book"
362
+ import { system, world, StartupEvent } from "@minecraft/server"
363
+
364
+ const DEBUG: boolean = false
365
+ let registered: boolean = false
366
+
367
+ export const registerCustomItemComponent = (): void => {
368
+ system.beforeEvents.startup.subscribe((initEvent: StartupEvent) => {
369
+ if (registered) return
370
+ registered = true
371
+ if (DEBUG) world.sendMessage("[Registry] registering my_mod:book_component")
372
+ initEvent.itemComponentRegistry.registerCustomComponent(
373
+ "my_mod:book_component",
374
+ GuiBookItemComponent
375
+ )
376
+ })
377
+ }
378
+ ```
379
+
380
+ > `registerCustomComponent` 的第一个参数**必须**与 `main.ts` 中 `setCustomComponentV2()` 的第一个参数一致。
381
+
382
+ ### 5.2 scripts/index.ts(运行时入口)
383
+
384
+ ```typescript
385
+ import { registerCustomItemComponent } from "./custom_components/registry"
386
+ registerCustomItemComponent()
387
+ ```
388
+
389
+ ---
390
+
391
+ ## 6. 构建与部署
392
+
393
+ ### 6.1 构建
394
+
395
+ ```bash
396
+ npm run build
397
+ ```
398
+
399
+ 构建完成后:
400
+ - `dev/guidebook_BP/` — 行为包(manifest.json + scripts/ + items/)
401
+ - `dev/guidebook_RP/` — 资源包(manifest.json + ui/ + textures/)
402
+
403
+ ### 6.2 部署
404
+
405
+ 将 `dev/guidebook_BP` 和 `dev/guidebook_RP` 复制到 Minecraft 的 `development_behavior_packs` / `development_resource_packs` 目录:
406
+
407
+ ```
408
+ C:/Users/你的用户名/AppData/Local/Packages/
409
+ Microsoft.MinecraftUWP_8wekyb3d8bbwe/LocalState/games/
410
+ com.mojang/development_behavior_packs/guidebook_BP/
411
+ com.mojang/development_resource_packs/guidebook_RP/
412
+ ```
413
+
414
+ ### 6.3 在游戏中启用
415
+
416
+ 1. 创建一个新世界或打开现有世界
417
+ 2. 设置 → 行为包 → 添加 `guidebook_BP`
418
+ 3. 设置 → 资源包 → 添加 `guidebook_RP`
419
+ 4. 进入游戏,获取物品:`/give @s sapdon:neo_guidebook`
420
+
421
+ ---
422
+
423
+ ## 7. 完整示例
424
+
425
+ 以下是一个完整的最小化指南书示例:
426
+
427
+ ### main.ts
428
+
429
+ ```typescript
430
+ import fs from "fs"
431
+ import path from "path"
432
+ import {
433
+ ItemAPI, ItemComponent, ItemCategory, registry,
434
+ NeoGuidebook, NeoGuidebookPage
435
+ } from '@sapdon/core'
436
+
437
+ // 物品
438
+ const item = ItemAPI.createItem("my_mod:guidebook", ItemCategory.Items, "book_icon")
439
+ item.format_version = "1.21.90"
440
+ item.addComponent(ItemComponent.setCustomComponentV2("my_mod:book_component", {}))
441
+ item.addComponent(ItemComponent.setMaxStackSize(1))
442
+ item.addComponent(ItemComponent.setDisplayName("快速入门手册"))
443
+ item.addComponent(ItemComponent.setInteractButton("打开"))
444
+
445
+ // 指南书
446
+ const book = new NeoGuidebook("my_mod:guidebook", "ui/", [320, 207])
447
+
448
+ // 封面
449
+ const cover = new NeoGuidebookPage("cover")
450
+ .addEmptySpace(["100%", "5%"])
451
+ .addBookTitleBar("快速入门手册", ["100%", "15%"])
452
+ .addEmptySpace(["100%", "3%"])
453
+ .addBookText("欢迎使用快速入门手册!", ["100%", "60%"])
454
+
455
+ // 第一章
456
+ const ch1 = new NeoGuidebookPage("ch1")
457
+ .addEmptySpace(["100%", "5%"])
458
+ .addCategoryTitle("第一章:基础", ["100%", "15%"])
459
+ .addDivider(["100%", "3%"])
460
+ .addBookText("这是第一章的内容。", ["100%", "72%"])
461
+
462
+ // 第二章
463
+ const ch2 = new NeoGuidebookPage("ch2")
464
+ .addEmptySpace(["100%", "5%"])
465
+ .addCategoryTitle("第二章:进阶", ["100%", "15%"])
466
+ .addDivider(["100%", "3%"])
467
+ .addBookText("这是第二章的内容。", ["100%", "72%"])
468
+
469
+ // 注册页面
470
+ book.addDoublePageStack("page_index0", cover.getPanel(), ch1.getPanel())
471
+ book.addDoublePageStack("page_index1", ch2.getPanel(), ch2.getPanel())
472
+
473
+ // 输出 page_ids
474
+ fs.writeFileSync(
475
+ path.join(process.cwd(), "scripts", "page_ids.json"),
476
+ JSON.stringify(book.getPageIds(), null, 2)
477
+ )
478
+
479
+ registry.submit()
480
+ ```
481
+
482
+ ### 运行时文件
483
+
484
+ `scripts/index.ts`:
485
+ ```typescript
486
+ import { registerCustomItemComponent } from "./custom_components/registry"
487
+ registerCustomItemComponent()
488
+ ```
489
+
490
+ `scripts/custom_components/registry.ts`:
491
+ ```typescript
492
+ import { GuiBookItemComponent } from "./items/gui_book"
493
+ import { system, StartupEvent } from "@minecraft/server"
494
+
495
+ let registered: boolean = false
496
+
497
+ export const registerCustomItemComponent = (): void => {
498
+ system.beforeEvents.startup.subscribe((initEvent: StartupEvent) => {
499
+ if (registered) return
500
+ registered = true
501
+ initEvent.itemComponentRegistry.registerCustomComponent(
502
+ "my_mod:book_component",
503
+ GuiBookItemComponent
504
+ )
505
+ })
506
+ }
507
+ ```
508
+
509
+ `scripts/custom_components/items/gui_book.ts`:
510
+ ```typescript
511
+ import { world, ItemCustomComponent, ItemComponentUseEvent } from "@minecraft/server"
512
+ import { NeoGuidebookBridge } from "../lib/page_bridge"
513
+ import pageIds from "../../page_ids.json"
514
+
515
+ const bridge = new NeoGuidebookBridge("sapdon_ui:guidebook", pageIds)
516
+
517
+ export const GuiBookItemComponent: ItemCustomComponent = {
518
+ onUse(event: ItemComponentUseEvent, params: any): void {
519
+ const player = event.source
520
+ if (player.typeId != "minecraft:player") return
521
+ bridge.show(player, 0)
522
+ }
523
+ }
524
+ ```
525
+
526
+ `scripts/lib/page_bridge.ts`:
527
+ ```typescript
528
+ import { ActionFormData, ActionFormResponse } from "@minecraft/server-ui"
529
+ import { world, Player } from "@minecraft/server"
530
+
531
+ interface PageCallbacks {
532
+ onEnter?: (pageId: string, index: number) => void
533
+ onLeave?: (pageId: string, index: number) => void
534
+ }
535
+
536
+ interface BridgeOptions {
537
+ debug?: boolean
538
+ }
539
+
540
+ export class NeoGuidebookBridge {
541
+ private uiName: string
542
+ private pageIds: string[]
543
+ private totalPages: number
544
+ private hooks: Map<string, PageCallbacks>
545
+ private currentIndex: number
546
+ private debug: boolean
547
+
548
+ constructor(uiName: string, pageIds: string[], options: BridgeOptions = {}) {
549
+ this.uiName = uiName
550
+ this.pageIds = pageIds
551
+ this.totalPages = pageIds.length
552
+ this.hooks = new Map()
553
+ this.currentIndex = 0
554
+ this.debug = options.debug === true
555
+ }
556
+
557
+ private log(...args: string[]): void {
558
+ if (this.debug) world.sendMessage("[Guidebook] " + args.join(" "))
559
+ }
560
+
561
+ onPage(pageId: string, callbacks: PageCallbacks): this {
562
+ this.hooks.set(pageId, callbacks)
563
+ return this
564
+ }
565
+
566
+ show(player: Player, startIndex: number = 0): void {
567
+ this.currentIndex = Math.max(0, Math.min(startIndex, this.totalPages - 1))
568
+ this.open(player)
569
+ }
570
+
571
+ open(player: Player): void {
572
+ const pageId = this.pageIds[this.currentIndex]
573
+ const hook = this.hooks.get(pageId)
574
+ if (hook?.onEnter) hook.onEnter(pageId, this.currentIndex)
575
+
576
+ const form = new ActionFormData()
577
+ .title(this.uiName)
578
+ .body(pageId)
579
+
580
+ const actions: string[] = []
581
+ const hasPrev = this.currentIndex > 0
582
+ const hasNext = this.currentIndex < this.totalPages - 1
583
+ const hasHome = this.currentIndex !== 0
584
+ const isChapterList = this.currentIndex === 0
585
+
586
+ if (hasPrev) { form.button("prev_button"); actions.push("prev") }
587
+ if (hasNext) { form.button("next_button"); actions.push("next") }
588
+ if (hasHome) { form.button("home_button"); actions.push("home") }
589
+
590
+ if (isChapterList) {
591
+ for (let i = 1; i < this.totalPages; i++) {
592
+ form.button(`item_${i - 1}_button`)
593
+ actions.push(`goto:${i}`)
594
+ }
595
+ }
596
+
597
+ form.show(player).then((response: ActionFormResponse) => {
598
+ if (response.canceled) {
599
+ if (hook?.onLeave) hook.onLeave(pageId, this.currentIndex)
600
+ return
601
+ }
602
+
603
+ const action = actions[response.selection!]
604
+ if (!action) return
605
+
606
+ const prev = this.currentIndex
607
+ if (action === "prev") this.currentIndex--
608
+ else if (action === "next") this.currentIndex++
609
+ else if (action === "home") this.currentIndex = 0
610
+ else if (action.startsWith("goto:"))
611
+ this.currentIndex = parseInt(action.split(":")[1], 10)
612
+
613
+ if (hook?.onLeave) hook.onLeave(pageId, prev)
614
+ if (this.currentIndex !== prev) this.open(player)
615
+ })
616
+ }
617
+ }
618
+ ```
619
+
620
+ ---
621
+
622
+ ## 常见问题
623
+
624
+ ### Q: 为什么按钮不显示?
625
+
626
+ 检查 `form.button()` 的参数是否与 JSON UI 中 `$binding_button_text` 一致。按钮文字**不是**显示文本,而是绑定键名。
627
+
628
+ ### Q: 为什么显示原版表单而不是自定义 UI?
629
+
630
+ 检查 `title()` 的参数与 `new NeoGuidebook()` 的 identifier 是否一致。
631
+
632
+ ### Q: 翻页后页面空白?
633
+
634
+ 检查 `body()` 的参数是否与注册时的 `page_id` 完全一致。
635
+
636
+ ### Q: 物品拿在手上右键没反应?
637
+
638
+ 1. 检查 manifest.json 的依赖版本是否正确(`2.8.0` / `2.1.0`)
639
+ 2. 检查注册的组件名是否与物品 JSON 中的一致
640
+ 3. 检查控制台是否有版本冲突错误