sapdon 3.3.3 → 3.5.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 +17 -7
- package/doc/dev/architecture.md +418 -415
- package/doc/dev/cli.md +467 -467
- package/doc/dev/core.md +750 -717
- package/doc/dev/lr-paradigm.md +85 -0
- package/doc/dev/ui-architecture.md +274 -0
- package/doc/dev/ui-lessons.md +147 -0
- package/doc/dev/workflow.md +257 -0
- package/doc/guidebook.md +294 -0
- package/doc/hello_sapdon/hello_sapdon.md +3 -3
- package/doc/user/api/block.md +599 -14
- package/doc/user/api/item.md +284 -55
- package/doc/user/api/neo-guidebook.md +409 -0
- package/doc/user/api/sapdon-ui.md +189 -0
- package/doc/user/config/build-config.md +4 -5
- package/doc/user/quick-start.md +20 -6
- package/doc/user/tutorials/block.md +12 -1
- package/doc/user/tutorials/item.md +3 -3
- package/doc/user/tutorials/neo-guidebook-experience.md +381 -0
- package/doc/user/tutorials/neo-guidebook.md +640 -0
- package/doc/user/tutorials/sapdon-ui.md +204 -0
- package/package.json +5 -1
- package/prod/cli/index.js +1 -1
- package/prod/cli/start.js +1 -1
- package/prod/core/index.d.ts +2339 -937
- package/prod/core/index.js +1 -1
- package/prod/oc/index.d.ts +3 -1
- package/prod/oc/index.js +1 -1
- package/prod/utils/index.d.ts +0 -7
- package/prod/utils/index.js +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Sapdon 框架开发工作流
|
|
2
|
+
|
|
3
|
+
本文档记录 Sapdon 框架自身的开发流程与经验,帮助贡献者快速上手。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 核心原则
|
|
8
|
+
|
|
9
|
+
**永远只修改 `src/` 下的源码,不要直接修改 `prod/` 或 `node_modules/`。**
|
|
10
|
+
|
|
11
|
+
- `prod/` 是构建产物,由 `npm run build` 自动生成,手动编辑会被覆盖。
|
|
12
|
+
- 项目中的 `node_modules/@sapdon/*` 是从 `prod/` 同步的副本,手动编辑会被 `sapdon lib` 覆盖。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 2. 框架自身的构建流程
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npm run build
|
|
20
|
+
│
|
|
21
|
+
├─ tsc → src/ → dist/ (TypeScript 编译)
|
|
22
|
+
├─ tsc-alias → 解析路径别名
|
|
23
|
+
└─ rollup → dist/ → prod/ (打包为 ESM bundle)
|
|
24
|
+
├─ prod/cli/start.js CLI 入口
|
|
25
|
+
├─ prod/cli/index.js CLI 库
|
|
26
|
+
├─ prod/core/index.js @sapdon/core
|
|
27
|
+
├─ prod/oc/index.js @sapdon/runtime
|
|
28
|
+
├─ prod/utils/index.js @sapdon/utils
|
|
29
|
+
└─ prod/*/index.d.ts TypeScript 声明文件
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
构建命令在 `scripts/build.cjs` 中定义。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 3. 全局 CLI 与本地开发
|
|
37
|
+
|
|
38
|
+
全局安装的 `sapdon` 命令通常通过 **junction 软链接** 指向仓库目录:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
C:\nodejs\node_modules\sapdon → D:\Projects\sapdon
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
这意味着:
|
|
45
|
+
- 修改 `src/` 后只需 `npm run build` 重建 `prod/`,全局 CLI 立即生效,无需重新 `npm i -g`。
|
|
46
|
+
- 如果不是 junction 链接(例如直接 npm publish 后安装),需要把新 `prod/` 覆盖到全局安装目录,或重新 `npm i -g sapdon`。
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 4. 项目侧同步新库
|
|
51
|
+
|
|
52
|
+
在示例项目或用户项目中,使用框架新功能前,需要将 `prod/` 同步到项目的 `node_modules/@sapdon/`。有两种方式:
|
|
53
|
+
|
|
54
|
+
### 方式一:`npm i`(推荐)
|
|
55
|
+
|
|
56
|
+
项目 `package.json` 的 `postinstall` 脚本会自动执行 `sapdon lib`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"scripts": {
|
|
61
|
+
"postinstall": "sapdon lib"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`npm i` 会触发 `postinstall`,将 `prod/` 下的库复制到 `node_modules/@sapdon/`:
|
|
67
|
+
|
|
68
|
+
| 源路径 | 目标路径 |
|
|
69
|
+
|--------|----------|
|
|
70
|
+
| `prod/core/` | `node_modules/@sapdon/core/` |
|
|
71
|
+
| `prod/cli/` | `node_modules/@sapdon/cli/` |
|
|
72
|
+
| `prod/oc/` | `node_modules/@sapdon/runtime/` |
|
|
73
|
+
|
|
74
|
+
### 方式二:手动执行 `sapdon lib`
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sapdon lib
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
效果相同,适合在 `npm i` 之后快速刷新库。
|
|
81
|
+
|
|
82
|
+
### 然后编译项目
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
sapdon compile # 仅构建,不启动 HMR
|
|
86
|
+
# 或
|
|
87
|
+
npm run build # 构建 + 启动开发服务器(HMR)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 5. 完整开发循环
|
|
93
|
+
|
|
94
|
+
当修改框架源码时,完整的开发和验证流程如下:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
1. 修改 src/ 下的源码
|
|
98
|
+
2. npm run build # 重建 prod/
|
|
99
|
+
3. 进入示例项目目录
|
|
100
|
+
4. npm i # 触发 postinstall → sapdon lib,同步新库
|
|
101
|
+
# 或:sapdon lib # 手动同步
|
|
102
|
+
5. sapdon compile # 验证构建结果
|
|
103
|
+
6. 确认无误后提交 PR
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 6. 常见坑点
|
|
109
|
+
|
|
110
|
+
### 6.1 修改了 prod/ 但项目没生效
|
|
111
|
+
|
|
112
|
+
`prod/` 是构建产物,手动修改会被下次 `npm run build` 覆盖。务必回到 `src/` 修改,然后重建。
|
|
113
|
+
|
|
114
|
+
### 6.2 手动修改了 node_modules/@sapdon/*
|
|
115
|
+
|
|
116
|
+
`node_modules/@sapdon/*` 是 `sapdon lib` 的输出,运行 `npm i` 或 `sapdon lib` 会覆盖所有手动修改。
|
|
117
|
+
|
|
118
|
+
### 6.3 编译后服务器不退出
|
|
119
|
+
|
|
120
|
+
`sapdon compile` 默认构建完成后自动退出。如果服务器一直占端口,检查 `build.config` 中是否设置了 `buildOptions.keepServer: true`。
|
|
121
|
+
|
|
122
|
+
### 6.4 新建项目后忘记同步库
|
|
123
|
+
|
|
124
|
+
使用 `sapdon create` 创建新项目后,`postinstall` 会自动执行 `sapdon lib`。如果手动创建项目或删除了 `node_modules`,需要重新 `npm i` 或手动运行 `sapdon lib`。
|
|
125
|
+
|
|
126
|
+
### 6.5 manifest.json 不会随 dependencies 变化重新生成
|
|
127
|
+
|
|
128
|
+
`src/cli/build.js` 中 manifest 只在**不存在时**才生成(`if (pathNotExist(manifestPath))`),目的是保留 uuid。因此修改 `build.config` 里的 `@minecraft/server` 版本后,旧 manifest 会被保留、新版本不生效。
|
|
129
|
+
|
|
130
|
+
**解决**:手动删除 `dev/<name>_BP/manifest.json` 后重新 `sapdon compile`。删除 manifest 不影响 uuid(uuid 缓存在 `dev/.sapdon` 等目录,由 `loadOrCreateUuids` 复用)。
|
|
131
|
+
|
|
132
|
+
### 6.6 方块脚本事件不存在(blockTick / blockUpdate)与自定义组件注册时机
|
|
133
|
+
|
|
134
|
+
`world.afterEvents.blockTick` 与 `world.afterEvents.blockUpdate` **在任何版本都不存在**(包括 @minecraft/server 1.19.0)。调用会报 `cannot read property 'subscribe' of undefined`。
|
|
135
|
+
|
|
136
|
+
**正确做法**:使用**自定义组件**(custom components):
|
|
137
|
+
- 方块 JSON 加扁平化自定义组件(V2,format_version ≥ 1.21.90):`"sapdon:xxx_tick": {}`
|
|
138
|
+
- 脚本在 `system.beforeEvents.startup` 注册(需 **@minecraft/server ≥ 2.0.0**):
|
|
139
|
+
```js
|
|
140
|
+
import { system } from "@minecraft/server";
|
|
141
|
+
system.beforeEvents.startup.subscribe((init) => {
|
|
142
|
+
init.blockComponentRegistry.registerCustomComponent('sapdon:xxx_tick', { onTick, onPlayerInteract, ... });
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
- `onTick` 由 `minecraft:tick` 组件驱动(两者共存),`onPlayerInteract` 替代 `world.afterEvents.itemUseOn`
|
|
146
|
+
|
|
147
|
+
**注册时机是关键**:`startup`(2.0.0+)在方块 JSON 被加载/校验**之前**触发,此时注册自定义组件才能让 Schema 识别。用 `world.beforeEvents.worldInitialize`(1.x 时代的入口)注册会在方块 JSON 校验之后,报 `this component was found in the input, but is not present in the Schema`。
|
|
148
|
+
|
|
149
|
+
**版本对应关系**(@minecraft/server → Minecraft 稳定版):`2.0.0`→1.21.90/1.21.100,`2.6.0`→1.26.20,`2.8.0`→1.26.30,`2.9.0`→1.26.40。声明版本≤游戏支持的版本即可。
|
|
150
|
+
|
|
151
|
+
### 6.7 build.config 带 UTF-8 BOM 导致 JSON 解析失败
|
|
152
|
+
|
|
153
|
+
如果 `build.config` 被以带 BOM 的 UTF-8 保存(如某些编辑器/命令输出),`JSON.parse` 会报 `Unexpected token ''`。
|
|
154
|
+
|
|
155
|
+
**解决**:字节级剥离 BOM(重新保存为无 BOM 的 UTF-8)。注意 `ConvertTo-Json | Out-File` 或部分重定向写法会引入 BOM;编辑此类配置文件时用无 BOM 编码保存。
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 7. 实战案例
|
|
160
|
+
|
|
161
|
+
### 案例 1:修复方块 JSON 报错(Minecraft 1.26.20+)
|
|
162
|
+
|
|
163
|
+
**问题**:Minecraft 从 format version 1.26.20 起,`minecraft:material_instances` 的 `ambient_occlusion` 不接受布尔值,必须是 0.0–10.0 浮点数。
|
|
164
|
+
|
|
165
|
+
**修复位置**:`src/cli/load.ts`(方块 JSON 生成逻辑)。
|
|
166
|
+
|
|
167
|
+
**修复前**:
|
|
168
|
+
```ts
|
|
169
|
+
ambient_occlusion: this.options.ambient_occlusion ?? true
|
|
170
|
+
// 生成: "ambient_occlusion": true ← Minecraft 1.26.20+ 报错
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**修复后**:
|
|
174
|
+
```ts
|
|
175
|
+
ambient_occlusion: "number" == typeof i ? i : !1 === i ? 0 : 1
|
|
176
|
+
// 生成: "ambient_occlusion": 0 ← 合法浮点数
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 案例 2:新增 keepServer 配置项
|
|
180
|
+
|
|
181
|
+
**需求**:`sapdon compile` 构建完成后 dev server 不退出,一直占端口 49037。需要加开关控制是否常驻。
|
|
182
|
+
|
|
183
|
+
**修改文件**:
|
|
184
|
+
- `src/cli/meta/buildConfig.ts`:在 `BuildOptions` 中新增 `keepServer?: boolean` 字段。
|
|
185
|
+
- `src/cli/build.js`:在 `buildProject()` 末尾添加自动退出逻辑,默认 `keepServer` 为 false 时 `process.exit(0)`。
|
|
186
|
+
- `src/templates/js_sapdon/build.config` 和 `src/templates/ts_sapdon/build.config`:在模板中新增 `keepServer` 字段及注释。
|
|
187
|
+
|
|
188
|
+
**使用方式**:
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"buildOptions": {
|
|
192
|
+
"keepServer": false, // 默认 false,构建完成后自动退出;true 保持服务器常开(配合 useHMR)
|
|
193
|
+
"useHMR": true
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 案例 3:修复 keepServer 自动退出导致脚本未打包
|
|
199
|
+
|
|
200
|
+
**问题**:`sapdon compile` 加入 `keepServer: false` 自动退出后,`dev/<name>_BP/scripts/index.js` 经常缺失或内容为旧版本。
|
|
201
|
+
|
|
202
|
+
**根因**:`src/cli/build.js` 的 `bundleScripts()` 调用异步的 `scriptBundler[elementType](...)` 时**没有 `await`**。之前服务器一直常驻(不退出),异步打包有足够时间完成,问题被掩盖;开启自动退出后,`process.exit(0)` 在异步 rollup 写入完成前就终止了进程,导致打包文件未写入。
|
|
203
|
+
|
|
204
|
+
**修复**:在 `bundleScripts()` 中 `await` 异步调用,并提前 `fs.mkdirSync` 创建输出目录(rollup 不会自动创建父目录):
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
async function bundleScripts(useJs=false) {
|
|
208
|
+
const elementType = useJs ? 'js' : 'ts'
|
|
209
|
+
const projectPath = getProjectPath()
|
|
210
|
+
const buildConfig = getBuildConfig()
|
|
211
|
+
const { scriptEntry, scriptOutput, buildMode } = buildConfig.buildOptions
|
|
212
|
+
const targetPath = path.join(getBuildDirBp(), scriptOutput)
|
|
213
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
214
|
+
await scriptBundler[elementType]( // ← 必须 await,否则进程退出导致打包中断
|
|
215
|
+
path.join(projectPath, scriptEntry),
|
|
216
|
+
targetPath,
|
|
217
|
+
buildMode === 'dev' ? true : false
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**排查经验**:当框架新增 `process.exit(0)` 类逻辑时,要检查所有异步操作是否都被 `await`。现象是"同步创建的目录存在,但异步写入的文件缺失"。
|
|
223
|
+
|
|
224
|
+
### 案例 4:数字电路方块报 `subscribe of undefined` / `not present in the Schema` → 自定义组件 + @minecraft/server 2.x
|
|
225
|
+
|
|
226
|
+
**问题**:`examples/digitCircuit` 的 `scripts/index.js` 中 `world.afterEvents.blockTick.subscribe(...)` 报 `TypeError: cannot read property 'subscribe' of undefined`;改用 `world.beforeEvents.worldInitialize` 注册自定义组件后又报 `sapdon:wire_tick: this component was found in the input, but is not present in the Schema`。
|
|
227
|
+
|
|
228
|
+
**根因(三层)**:
|
|
229
|
+
1. **API 版本被 manifest 门控**:`build.config` 里声明 `@minecraft/server: 1.8.0`,Bedrock 运行时只暴露该版本的 API 表面。`blockTick`/`blockUpdate` 即使在新版本也不存在(1.19.0 的 `.d.ts` 中确认无此事件),但 `registerCustomComponent` 需要 ≥1.9.0。
|
|
230
|
+
2. **事件选错**:正确的 tick 机制是自定义组件的 `onTick`,而非 afterEvents。
|
|
231
|
+
3. **注册入口太晚**:`world.beforeEvents.worldInitialize` 在方块 JSON 校验之后触发,Schema 不认自定义组件。必须用 **`system.beforeEvents.startup`(@minecraft/server ≥ 2.0.0)**,它在方块 JSON 加载前触发。
|
|
232
|
+
|
|
233
|
+
**修改文件**:
|
|
234
|
+
- `examples/digitCircuit/build.config`:`@minecraft/server` `1.8.0` → `2.6.0`(对应游戏 1.26.20),并**删除旧 manifest** 强制重新生成(见 6.5)。同时把 `package.json` devDependency 同步为 `2.6.0`(精确版本,保证类型与运行时一致)。
|
|
235
|
+
- `lib/wire.js`、`main.mjs`:给方块加 `BlockComponent.setCustomComponents(["sapdon:wire_tick"])` / `["sapdon:gate_tick"]`,生成扁平化自定义组件 JSON:
|
|
236
|
+
```json
|
|
237
|
+
"components": { "sapdon:wire_tick": {}, "minecraft:tick": { "interval_range": [5, 10], "looping": true } }
|
|
238
|
+
```
|
|
239
|
+
- `scripts/index.js`:在 `system.beforeEvents.startup` 注册:
|
|
240
|
+
```js
|
|
241
|
+
import { Direction, system } from "@minecraft/server";
|
|
242
|
+
system.beforeEvents.startup.subscribe((init) => {
|
|
243
|
+
init.blockComponentRegistry.registerCustomComponent("sapdon:wire_tick", {
|
|
244
|
+
onPlayerInteract(event) { /* 右键设信号 15 */ },
|
|
245
|
+
onTick(event) { /* 传播信号 */ }
|
|
246
|
+
});
|
|
247
|
+
init.blockComponentRegistry.registerCustomComponent("sapdon:gate_tick", {
|
|
248
|
+
onTick(event) { /* 重算 AND/OR/NOT */ }
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**要点**:
|
|
254
|
+
- 扁平化自定义组件(`"id": {}` 直接写在 `components`)是 format_version 1.21.90+ 的 V2 规范,与框架 `setCustomComponents` 的输出一致,`minecraft:custom_components` 数组写法已废弃。
|
|
255
|
+
- 注册入口用 `system.beforeEvents.startup`(`StartupEvent.blockComponentRegistry`),与 `src/oc/builtin/index.ts` 的 `registerBuiltinComponents()` 一致;`src/cli/load.js` 自动生成的注册模板也用的是 startup,版本升级后该模板可直接使用。
|
|
256
|
+
- `onPlayerInteract` 替代 `world.afterEvents.itemUseOn`(右键交互即触发,无需手持特定物品)。
|
|
257
|
+
- 验证手段:检查生成的 `dev/<name>_BP/manifest.json` 版本号、方块 JSON 的 `components` 里是否含自定义组件、打包后的 `scripts/index.js` 是否含 `system.beforeEvents.startup` 与 `registerCustomComponent`。
|
package/doc/guidebook.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# SapdonGuideBook —— 帕秋莉式手册框架类
|
|
2
|
+
|
|
3
|
+
`SapdonGuideBook` 是 Sapdon 提供的**帕秋莉式手册(Guidebook)**框架类。它让你用**纯数据声明**的方式,快速做出一本"分类索引 → 词条列表 → 内容页"的三层手册,并内置多种页类型、分页与导航。
|
|
4
|
+
|
|
5
|
+
- **三层结构**:`INDEX`(分类索引)→ `CAT`(词条列表)→ `ENT`(词条内容页)。
|
|
6
|
+
- **浏览器式导航**:每屏固定 `prev / home / next`,`home` 随时回首页。
|
|
7
|
+
- **多种页类型**:`text` / `crafting` / `spotlight` / `image`。
|
|
8
|
+
- **自动分页**:正文超过一屏自动翻页;分类词条超过 8 条自动分页。
|
|
9
|
+
- **路由驱动**:运行时通过 Server Form 的 `body` 路径 + 按钮槽位显隐,无需每个页面单独写路由。
|
|
10
|
+
|
|
11
|
+
> 示例见 `examples/guidebook_demo`(打开游戏手持木棍右键即可看到成品)。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. 引入
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { SapdonGuideBook } from '@sapdon/core'
|
|
19
|
+
import { registry } from '@sapdon/core'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
配套类型:
|
|
23
|
+
```ts
|
|
24
|
+
import type { GuideBookCategory, GuideBookChapter, GuideBookPageType } from '@sapdon/core'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. 快速开始
|
|
30
|
+
|
|
31
|
+
在项目的 `main.ts`(框架构建入口)里:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { SapdonGuideBook, registry } from '@sapdon/core'
|
|
35
|
+
|
|
36
|
+
const book = new SapdonGuideBook('mymod:book', [320, 207], 'textures/ui/book_back')
|
|
37
|
+
|
|
38
|
+
book.build([
|
|
39
|
+
{
|
|
40
|
+
id: 'intro', title: '介绍', icon: 'textures/items/book_writable',
|
|
41
|
+
introLines: ['欢迎使用这本手册。', '它由 SapdonGuideBook 构建。'],
|
|
42
|
+
chapters: [
|
|
43
|
+
{ name: '这是什么', icon: 'textures/items/book_writable', lines: ['这是一本帕秋莉式手册。', '分三层:索引→列表→内容。'] },
|
|
44
|
+
{ name: '如何打开', icon: 'textures/items/paper', lines: ['手持木棍右键打开本手册。'] },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
registry.submit()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 构造函数签名
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
new SapdonGuideBook(
|
|
56
|
+
identifier: string, // "namespace:name",如 "mymod:book"
|
|
57
|
+
size: [number, number] = [320, 207], // 手册画布尺寸
|
|
58
|
+
background: string = 'textures/ui/book_back' // 背景贴图
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 常用方法
|
|
63
|
+
|
|
64
|
+
| 方法 | 说明 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `.build(categories: GuideBookCategory[])` | 传入分类数据,生成全部页面;返回 `this` |
|
|
67
|
+
| `.enableDebug()` | 开启调试(显示 `#form_text` 当前值 / 格子描边) |
|
|
68
|
+
| `.getSystem()` | 返回内部 `UISystem` |
|
|
69
|
+
|
|
70
|
+
调用链结束前记得 `registry.submit()`,把注册的 UI 数据提交给构建工具生成 `book.json`。
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 3. 数据结构
|
|
75
|
+
|
|
76
|
+
### `GuideBookCategory`(分类)
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
interface GuideBookCategory {
|
|
80
|
+
id: string // 路由 id(英文,如 "intro"),唯一
|
|
81
|
+
title: string // 中文标题(索引卡名称 / 左页标题)
|
|
82
|
+
icon: string // 索引卡图标贴图路径
|
|
83
|
+
introLines: string[] // 分类简介(左半页逐行渲染)
|
|
84
|
+
chapters: GuideBookChapter[] // 词条列表
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `GuideBookChapter`(词条)
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
interface GuideBookChapter {
|
|
92
|
+
name: string // 词条名(列表行 / 内容页标题)
|
|
93
|
+
icon: string // 列表行图标
|
|
94
|
+
lines: string[] // 正文(text 页逐行渲染)
|
|
95
|
+
pageType?: 'text' | 'crafting' | 'spotlight' | 'image' // 默认 text
|
|
96
|
+
craft?: { grid: string[]; output: string } // crafting 页
|
|
97
|
+
spotlight?: { icon: string; desc: string } // spotlight 页
|
|
98
|
+
image?: { texture: string; caption: string } // image 页
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
> ⚠️ 词条名 **不要以 `#` 开头**(如 `#foo 门控`)。Bedrock 会把以 `#` 开头的文本当作绑定,渲染成空。需要表现 `#` 时放在句子中间或写成 `foo 门控`。
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 4. 页类型(`pageType`)
|
|
107
|
+
|
|
108
|
+
| `pageType` | 说明 | 相关字段 |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `text`(默认) | 逐行渲染正文,支持分页 | `lines` |
|
|
111
|
+
| `crafting` | 3×3 合成台 + 箭头 + 单个产物格 | `craft.grid`(9 项,空位 `''`)+ `craft.output` |
|
|
112
|
+
| `spotlight` | 大图标 + 描述 | `spotlight.icon` + `spotlight.desc`(含 `\n` 会多行) |
|
|
113
|
+
| `image` | 整页图 + 说明 | `image.texture` + `image.caption` |
|
|
114
|
+
|
|
115
|
+
`crafting` 示例:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
{
|
|
119
|
+
name: '合成示例', icon: 'textures/items/iron_ingot', pageType: 'crafting',
|
|
120
|
+
craft: {
|
|
121
|
+
grid: ['textures/items/iron_ingot','textures/items/iron_ingot','textures/items/iron_ingot',
|
|
122
|
+
'textures/items/iron_ingot','','textures/items/iron_ingot',
|
|
123
|
+
'textures/items/iron_ingot','','textures/items/iron_ingot'],
|
|
124
|
+
output: 'textures/items/iron_leggings',
|
|
125
|
+
},
|
|
126
|
+
lines: ['铁锭 → 铁护腿'],
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 5. 路由协议(运行时)
|
|
133
|
+
|
|
134
|
+
手册内容由**固定布局 + 门控**驱动:布局容器按 `body` 路径显隐,按钮按 `form_button_text` 精确显隐。具体由项目的 `scripts/index.ts` 用 `ActionFormData` 发射。
|
|
135
|
+
|
|
136
|
+
- **`title`**:固定为 `sapdon_ui:<name>`(如 `sapdon_ui:book`)。
|
|
137
|
+
- **`body`(路径)**:
|
|
138
|
+
- `"INDEX"` → 分类索引页
|
|
139
|
+
- `"CAT:<id>|p<N>"` → 分类页(`N` 为分类页码,`p0` 左简介右列表)
|
|
140
|
+
- `"ENT:<id>:<gi>|p<N>"` → 词条内容页(`gi` 为词条序号,`N` 为内容页码)
|
|
141
|
+
|
|
142
|
+
### 按钮槽位(顺序固定)
|
|
143
|
+
|
|
144
|
+
| 页面 | 槽位 |
|
|
145
|
+
|---|---|
|
|
146
|
+
| INDEX | `[no_prev, no_home, no_next, idx0..3]`(三导航全隐藏) |
|
|
147
|
+
| CAT | `[prev\|no_prev, home, next\|no_next, <id>_e<num>...]` |
|
|
148
|
+
| ENT | `[prev, home, next\|no_next]` |
|
|
149
|
+
|
|
150
|
+
占位键 `no_prev / no_home / no_next` 不代表任何注册按钮,从而让对应导航按钮**隐藏**。
|
|
151
|
+
|
|
152
|
+
### 分页规则
|
|
153
|
+
|
|
154
|
+
- **CAT 列表**:每列最多 8 行。`p0` 右列 8 行;`p1+` 左 8 + 右 8(=16 行/页)。
|
|
155
|
+
- **ENT 正文(text)**:左右半页各最多 5 行,先填左半页、超出再填右半页;**整体超过 10 行才分页**。
|
|
156
|
+
|
|
157
|
+
运行时脚本里需要维护两个与 `main.ts` 数据对齐的量:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// scripts/index.ts
|
|
161
|
+
const CATS = ["intro", "pages", "routing", "controls"]; // 与 main.ts 分类 id 对齐(含顺序)
|
|
162
|
+
const CAT_CHAPTERS: Record<string, number> = { intro: 4, pages: 6, routing: 6, controls: 6 }; // 每分类词条数
|
|
163
|
+
const ENT_PAGES: Record<string, number> = { pages_e4: 2 }; // 需要多页的 text 词条 → 页数(ceil(lines/10))
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
> 若某 text 词条行数超过 10,`main.ts` 会用 `ENT_PAGES` 里的页数来让 next/prev 生效。忘加会导致分页无法翻动。
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 6. 手写运行时路由(`scripts/index.ts` 参考)
|
|
171
|
+
|
|
172
|
+
项目里还需一个"脚本入口"(build.config 的 `scriptEntry`),示例为 `scripts/index.ts`,用木棍右键打开手册:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { world, Player } from "@minecraft/server";
|
|
176
|
+
import { ActionFormData } from "@minecraft/server-ui";
|
|
177
|
+
|
|
178
|
+
const CATS = ["intro", "pages", "routing", "controls"];
|
|
179
|
+
const CAT_CHAPTERS = { intro: 4, pages: 6, routing: 6, controls: 6 };
|
|
180
|
+
const ENT_PAGES = { pages_e4: 2 };
|
|
181
|
+
const TITLE = "sapdon_ui:book";
|
|
182
|
+
const NO_PREV = "no_prev", NO_HOME = "no_home", NO_NEXT = "no_next";
|
|
183
|
+
|
|
184
|
+
function openIndex(p: Player): void {
|
|
185
|
+
const f = new ActionFormData().title(TITLE).body("INDEX");
|
|
186
|
+
f.button(NO_PREV); f.button(NO_HOME); f.button(NO_NEXT);
|
|
187
|
+
CATS.forEach((_, i) => f.button(`idx${i}`));
|
|
188
|
+
f.show(p).then((r) => {
|
|
189
|
+
if (r.canceled) return;
|
|
190
|
+
const s = r.selection!;
|
|
191
|
+
if (s >= 3 && s - 3 < CATS.length) openCat(p, CATS[s - 3], 0);
|
|
192
|
+
else openIndex(p);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function openCat(p: Player, id: string, page: number): void {
|
|
197
|
+
const total = CAT_CHAPTERS[id] ?? 0;
|
|
198
|
+
const start = page === 0 ? 0 : 8 + (page - 1) * 16;
|
|
199
|
+
const end = Math.min(start + (page === 0 ? 8 : 16), total);
|
|
200
|
+
const f = new ActionFormData().title(TITLE).body(`CAT:${id}|p${page}`);
|
|
201
|
+
f.button(page > 0 ? "prev_button" : NO_PREV);
|
|
202
|
+
f.button("home_button");
|
|
203
|
+
f.button(end < total ? "next_button" : NO_NEXT);
|
|
204
|
+
for (let i = start; i < end; i++) f.button(`${id}_e${i}`);
|
|
205
|
+
f.show(p).then((r) => {
|
|
206
|
+
if (r.canceled) return;
|
|
207
|
+
const s = r.selection!;
|
|
208
|
+
if (s === 0 && page > 0) openCat(p, id, page - 1);
|
|
209
|
+
else if (s === 1) openIndex(p);
|
|
210
|
+
else if (s === 2 && end < total) openCat(p, id, page + 1);
|
|
211
|
+
else if (s >= 3) { const gi = start + (s - 3); if (gi < total) openEnt(p, id, gi, page, 0); }
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function openEnt(p: Player, id: string, gi: number, fromPage: number, ep: number): void {
|
|
216
|
+
const pc = ENT_PAGES[`${id}_e${gi}`] ?? 1;
|
|
217
|
+
const f = new ActionFormData().title(TITLE).body(`ENT:${id}:${gi}|p${ep}`);
|
|
218
|
+
f.button("prev_button"); f.button("home_button");
|
|
219
|
+
f.button(ep < pc - 1 ? "next_button" : NO_NEXT);
|
|
220
|
+
f.show(p).then((r) => {
|
|
221
|
+
if (r.canceled) return;
|
|
222
|
+
const s = r.selection!;
|
|
223
|
+
if (s === 0) ep > 0 ? openEnt(p, id, gi, fromPage, ep - 1) : openCat(p, id, fromPage);
|
|
224
|
+
else if (s === 1) openIndex(p);
|
|
225
|
+
else if (s === 2 && ep < pc - 1) openEnt(p, id, gi, fromPage, ep + 1);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
world.afterEvents.itemUse.subscribe((e) => {
|
|
230
|
+
if (e.itemStack.typeId === "minecraft:stick" && e.source.typeId === "minecraft:player")
|
|
231
|
+
openIndex(e.source as Player);
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 7. 完整教程(从零做一个手册)
|
|
238
|
+
|
|
239
|
+
**步骤 1:创建项目**
|
|
240
|
+
```bash
|
|
241
|
+
sapdon create my_guide
|
|
242
|
+
cd my_guide
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**步骤 2:在 `main.ts` 里声明手册**
|
|
246
|
+
```ts
|
|
247
|
+
import { SapdonGuideBook, registry } from '@sapdon/core'
|
|
248
|
+
|
|
249
|
+
const book = new SapdonGuideBook('my_guide:book', [320, 207])
|
|
250
|
+
|
|
251
|
+
book.build([
|
|
252
|
+
{
|
|
253
|
+
id: 'start', title: '开始', icon: 'textures/items/book_writable',
|
|
254
|
+
introLines: ['我的第一本手册。'],
|
|
255
|
+
chapters: [
|
|
256
|
+
{ name: '序言', icon: 'textures/items/book_writable', lines: ['欢迎使用 SapdonGuideBook。'] },
|
|
257
|
+
{ name: '合成演示', icon: 'textures/items/iron_ingot', pageType: 'crafting',
|
|
258
|
+
craft: { grid: ['textures/items/iron_ingot','','','','','','','',''], output: 'textures/items/iron_ingot' },
|
|
259
|
+
lines: ['一格铁锭 → 输出铁锭。'] },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
registry.submit()
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**步骤 3:写运行时路由**(见第 6 节 `scripts/index.ts`),并把 `CATS` / `CAT_CHAPTERS` / `ENT_PAGES` 对齐到你的分类与词条数。
|
|
268
|
+
|
|
269
|
+
**步骤 4:构建 & 进游戏**
|
|
270
|
+
```bash
|
|
271
|
+
sapdon build ./
|
|
272
|
+
```
|
|
273
|
+
进入游戏手持**木棍**右键即可打开手册。
|
|
274
|
+
|
|
275
|
+
**步骤 5(可选):`build.config` 配置**
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"buildOptions": {
|
|
279
|
+
"buildEntry": "main.ts",
|
|
280
|
+
"scriptEntry": "scripts/index.ts",
|
|
281
|
+
"scriptOutput": "scripts/index.js",
|
|
282
|
+
"buildMode": "dev"
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 8. 常见问题
|
|
290
|
+
|
|
291
|
+
- **词条文字为空 / 显示异常**:词条名或文案以 `#` 开头会被当作绑定。去掉开头的 `#`。
|
|
292
|
+
- **合成页输出是品红/黑格**:`craft.output` 引用了一个不存在的贴图。换成有效的(如 `textures/items/iron_leggings`)。
|
|
293
|
+
- **文本词条点 next 翻不动**:`ENT_PAGES` 里没给它配页数。`ENT_PAGES[`${catId}_e${gi}`] = Math.ceil(lines.length / 10)`。
|
|
294
|
+
- **想显示字面 `#`**:不要放在字符串开头,如 `form_text 门控`。
|
|
@@ -106,7 +106,7 @@ hello_sapdon/
|
|
|
106
106
|
]
|
|
107
107
|
}
|
|
108
108
|
```
|
|
109
|
-
- **buildMode
|
|
109
|
+
- **buildMode**:构建模式。`"dev"`(默认)执行 `main.mjs` 生成所有 JSON;`"prod"` 生成 JSON 并压缩脚本;`"debug"` 跳过代码执行,仅将 `dev/` 输出到游戏目录。
|
|
110
110
|
- **buildEntry**:构建入口文件的路径,即您编写模组内容的文件。
|
|
111
111
|
- **scriptEntry**:脚本入口文件的路径。
|
|
112
112
|
- **buildDir**:构建输出文件夹的路径,构建好的 Addon 包将输出到此文件夹。
|
|
@@ -118,9 +118,9 @@ hello_sapdon/
|
|
|
118
118
|
1. 打开 `main.mjs` 文件。
|
|
119
119
|
2. 写入以下内容以创建一个基础物品:
|
|
120
120
|
```javascript
|
|
121
|
-
import { ItemAPI } from "../src/core";
|
|
121
|
+
import { ItemAPI, ItemCategory } from "../src/core";
|
|
122
122
|
|
|
123
|
-
ItemAPI.createItem("hello_sapdon:my_item",
|
|
123
|
+
ItemAPI.createItem("hello_sapdon:my_item", ItemCategory.Items, "masterball");
|
|
124
124
|
```
|
|
125
125
|
这段代码将创建一个名为 `my_item` 的物品,其命名空间为 `hello_sapdon`,类型为 `items`,并使用 `masterball` 作为图标。
|
|
126
126
|
|