sapdon 3.5.3 → 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 +620 -74
- 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/package.json +4 -5
- package/prod/templates/js_sapdon/res/models/blocks/cube.geo.json +35 -0
- package/prod/templates/ts_sapdon/package.json +4 -5
- package/prod/templates/ts_sapdon/res/models/blocks/cube.geo.json +35 -0
package/doc/dev/oc.md
CHANGED
|
@@ -4,6 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## 0. 分层铁律:构建期 `@sapdon/core` vs 运行期 `@sapdon/runtime`
|
|
8
|
+
|
|
9
|
+
**这一节是读懂 `src/core` 与 `src/oc` 分工的前提,写运行期代码前必看。**
|
|
10
|
+
|
|
11
|
+
| | `@sapdon/core`(`src/core`) | `@sapdon/runtime`(`src/oc`) |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| 运行时机 | **构建期**(`main.ts` 跑一遍生成 JSON) | **运行期**(游戏内 `scripts/*` 执行) |
|
|
14
|
+
| 在 `rollupIgnores` 里? | ✅ 是 → 打包时被当作 **external** | ❌ 否 → **会被打包进脚本包** |
|
|
15
|
+
| 典型内容 | `ItemAPI` / `BlockAPI` / `registry.submit()` / DTO 类 | `ComponentManager` / `Level` / `Scheduler` / `persist` / `components` |
|
|
16
|
+
| 运行宿主 | Node.js(CLI 子进程) | Minecraft Bedrock Script API |
|
|
17
|
+
|
|
18
|
+
依据:`src/cli/build.js:38-44` 的 `rollupIgnores`:
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
const rollupIgnores = [
|
|
22
|
+
'rollup',
|
|
23
|
+
'typescript',
|
|
24
|
+
'@sapdon/core', // ← 构建期库,external
|
|
25
|
+
'@sapdon/cli',
|
|
26
|
+
'@minecraft', // ← 由游戏内置提供
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
⚠️ **红线**:**运行期脚本里不要 `import '@sapdon/core'`**。
|
|
31
|
+
因为 `@sapdon/core` 在 external 名单里,rollup 会**原样保留** `import ... from '@sapdon/core'` 这条裸包名语句,而 Bedrock **不是它的宿主**(游戏运行时只提供 `@minecraft/*`,没有 npm 解析)→ 产物里留下**游戏解析不了的裸包名**,导致**整包脚本挂掉**。需要"构建期和运行期共用"的逻辑,必须放在 `src/oc`(`@sapdon/runtime`)里,或各自实现一份。
|
|
32
|
+
|
|
33
|
+
**判断方法**:问「这段代码是在 `main.ts` 里生成 JSON,还是在游戏里每 tick 跑?」前者属于 `core`,后者属于 `oc`。新 API 落地前先想清楚属于哪一层。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
7
37
|
## 1. 架构总览
|
|
8
38
|
|
|
9
39
|
OC 模块实现了一个简化的 **Entity-Component 架构**:
|
|
@@ -35,10 +65,12 @@ OC 模块实现了一个简化的 **Entity-Component 架构**:
|
|
|
35
65
|
| **Component** | 附着在实体上的数据+行为单元,每个 Component 有自己的 `onTick()` |
|
|
36
66
|
| **ComponentManager** | 每个实体一个,管理其所有 Component 的生命周期 |
|
|
37
67
|
| **Level** | 实体集合,以 string ID 索引,每个 ID 对应一个 ComponentManager |
|
|
38
|
-
| **Scheduler** | 驱动游戏循环,每 tick
|
|
68
|
+
| **Scheduler** | 驱动游戏循环,每 tick 遍历 `Level.table` 里的每个 ID 调用 `onTick()`。⚠️ 它**只认实体**(内部写死 `world.getEntity(id)`),见 §4.2 |
|
|
39
69
|
| **Optional\<T\>** | Monadic 空值包装器 |
|
|
40
70
|
| **EventEmitter** | 自定义发布/订阅事件系统 |
|
|
41
71
|
|
|
72
|
+
> ⚠️ **调度器只遍历实体(重要架构约束)**:`Level.table` 的键会被当作**实体 ID** 去查 `world.getEntity(id)`。把**方块**的 ID 注册进这张表,`getEntity` 恒为 `undefined` → **方块永远不会 tick,而且不报任何错**。想做「机器每 tick 干活」必须**自建调度**(自己 `system.runInterval`)。详见 §4.2。
|
|
73
|
+
|
|
42
74
|
### 目录结构
|
|
43
75
|
|
|
44
76
|
```
|
|
@@ -64,6 +96,14 @@ src/oc/
|
|
|
64
96
|
│ ├── triggers.ts # ActionTriggers (pressed/released/hold 等)
|
|
65
97
|
│ └── modifiers.ts # ActionModifiers (negate/scale)
|
|
66
98
|
├── ui/hud.ts # HudComponent (基类)
|
|
99
|
+
├── components/ # 【新增】运行期自定义组件注册(路线 B)
|
|
100
|
+
│ └── registry.ts # registerBlockComponent / registerItemComponent / pendingComponentCount / registeredComponents
|
|
101
|
+
├── persist/ # 【新增】分块持久化(绕开动态属性约 32KB 上限)
|
|
102
|
+
│ └── chunked.ts # saveChunked / loadChunked / clearChunked / CHUNK_SIZE
|
|
103
|
+
├── builtin/ # 框架内建自定义组件(在 startup 时机注册)
|
|
104
|
+
│ ├── index.ts # registerBuiltinComponents()
|
|
105
|
+
│ ├── blocks/ # crop / fallingBlock / headRotation / intercardinalOrientation
|
|
106
|
+
│ └── items/guiBook.ts # sapdon:guibook
|
|
67
107
|
└── minecraft/ # Minecraft 集成层
|
|
68
108
|
├── index.ts # 聚合导出
|
|
69
109
|
├── core.ts # MinecraftTickingScheduler, MinecraftLevel, MinecraftGameInstance
|
|
@@ -79,6 +119,8 @@ src/oc/
|
|
|
79
119
|
└── actionbar.ts # PlayerHudComponent (actionbar HUD)
|
|
80
120
|
```
|
|
81
121
|
|
|
122
|
+
> `components/` 与 `persist/` 的 **API 细节(签名、参数、示例)见[运行期 API 文档](../user/api/runtime.md)**;本节只说明它们的架构定位。
|
|
123
|
+
|
|
82
124
|
---
|
|
83
125
|
|
|
84
126
|
## 2. Component 系统 (`core.ts`)
|
|
@@ -259,39 +301,116 @@ MinecraftGameInstance.onStart()
|
|
|
259
301
|
|
|
260
302
|
### 4.2 MinecraftTickingScheduler
|
|
261
303
|
|
|
304
|
+
源码:`src/oc/minecraft/core.ts:7-57`。
|
|
305
|
+
|
|
262
306
|
```typescript
|
|
263
307
|
@Minecraft
|
|
264
308
|
class MinecraftTickingScheduler implements Scheduler<string> {
|
|
265
|
-
|
|
309
|
+
private _currentTick = 0
|
|
310
|
+
private _timeStamp = 0
|
|
266
311
|
timeDilation = 1
|
|
267
312
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
313
|
+
get currentTick() { return this._currentTick }
|
|
314
|
+
|
|
315
|
+
start(table: Map<string, ComponentManager<any>>) {
|
|
316
|
+
this._timeStamp = Date.now()
|
|
317
|
+
this._run = system.runInterval(this.executeTick.bind(this, table))
|
|
272
318
|
}
|
|
319
|
+
stop() { system.clearRun(this._run) }
|
|
273
320
|
|
|
274
321
|
executeTick(table) {
|
|
275
|
-
this.
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
322
|
+
if (!this.timeDilation) return
|
|
323
|
+
const prev = this._currentTick
|
|
324
|
+
const cur = this._currentTick += this.timeDilation
|
|
325
|
+
if (cur - prev < 1) return // 时间膨胀不足 1 tick → 本帧跳过
|
|
326
|
+
const lastTickTime = this._timeStamp
|
|
327
|
+
const currentTime = (this._timeStamp = Date.now())
|
|
328
|
+
const dt = (currentTime - lastTickTime) * this.timeDilation
|
|
329
|
+
this._handleTick(table, dt)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
_handleTick(table, dt) {
|
|
333
|
+
for (const [ id, manager ] of table.entries()) {
|
|
334
|
+
const entity = world.getEntity(id) // ★ 键被当作实体 ID
|
|
335
|
+
if (entity) manager.handleTicks(entity, dt) // ★ 查不到就静默跳过
|
|
280
336
|
}
|
|
281
337
|
}
|
|
282
338
|
}
|
|
283
339
|
```
|
|
284
340
|
|
|
341
|
+
> `currentTick` 是**累积的时间膨胀值**(不是帧数):只有累积跨过 1 才真正执行一次 tick,`dt` 是「距上次执行的真实毫秒差 × timeDilation」。
|
|
342
|
+
|
|
343
|
+
#### ★ 固有开销:调度器的 `runInterval` **不传 `tickInterval`** ⇒ 每 tick 无条件回调(项目侧无法消除)
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
// src/oc/minecraft/core.ts:46-49
|
|
347
|
+
start(table) {
|
|
348
|
+
this._timeStamp = Date.now()
|
|
349
|
+
this._run = system.runInterval(this.executeTick.bind(this, table)) // ← 只传了 callback
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
- `system.runInterval(callback, tickInterval?)` 的第二个参数是**可选**的,框架**没传** ⇒ 间隔由引擎缺省决定,
|
|
354
|
+
框架**自己无法控制**它 ⇒ **每个 game tick 都会回调一次**(`executeTick` 里只可能在
|
|
355
|
+
`timeDilation === 0` 时提前 `return`,但**回调本身照样发生**)。
|
|
356
|
+
(⚠️ 官方文档页把 `tickInterval` 只写成 "An interval of every N ticks"、**没有显式写出缺省数字** ——
|
|
357
|
+
缺省值是 **1** 这一点标**「未验证」**;但"不传 ⇒ 框架控制不了间隔、每 tick 被回调"这个结论
|
|
358
|
+
与 FZ S3a 的真机观察一致。)
|
|
359
|
+
- **启动时机与项目无关**:`MinecraftGameInstance.onStart(ev)` → `setLevel(new MinecraftLevel())` →
|
|
360
|
+
`Level.start()`(`src/oc/level.ts:32-34`)→ `this.getScheduler().start(this.table)` ⇒
|
|
361
|
+
**世界一开始就常驻**,哪怕项目里一个机器/实体组件都没注册。
|
|
362
|
+
- 框架里**第二处** `system.runInterval` 是 `src/oc/builtin/blocks/fallingBlock.ts:18`(间隔 **10** tick)——
|
|
363
|
+
但它是**条件触发**的:只有下落方块 `onTick` 里 spawn 出实体后才起,实体落地/失效时 `clearRun` 自清,
|
|
364
|
+
**不是常驻开销**。(全仓 grep 判据:`Select-String -Path src\oc -Recurse -Pattern 'runInterval'` → 恰好 2 处命中。)
|
|
365
|
+
|
|
366
|
+
⇒ **结论**:项目侧能做到「**零机器 tick**」(活跃数 0 时 `clearRun` 掉自己那个 interval),
|
|
367
|
+
但**做不到「零脚本回调」** —— 框架这处每 tick 的固定开销无法被项目消除。
|
|
368
|
+
若将来要真正的「空闲零回调」,需要给调度器加「**无订阅者就停**」的能力;
|
|
369
|
+
⚠️ 但它还兼管**实体**组件的 tick(`_handleTick` 遍历 `Level.table` 里的实体),
|
|
370
|
+
停掉会连带停掉实体组件 —— 有副作用,**不能简单停**。本轮**未改代码**(改调度语义影响面大,仅记档)。
|
|
371
|
+
|
|
372
|
+
#### ⚠️ 调度器只遍历实体 —— 「方块每 tick 干活」必须自建调度
|
|
373
|
+
|
|
374
|
+
`_handleTick` 里**硬编码**了 `world.getEntity(id)`(`src/oc/minecraft/core.ts:20`)。而 `Level.table` 的键就是任意 string ID(`src/oc/level.ts:11-17`),**框架不校验它是不是实体**。于是:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
Level.addEntity("my_addon:my_machine") // 键是方块标识符
|
|
378
|
+
→ 进 table
|
|
379
|
+
→ 每 tick: world.getEntity("my_addon:my_machine") → undefined
|
|
380
|
+
→ if (entity) 不成立 → 什么都不做
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**症状是本项目排查最久的一类坑**:
|
|
384
|
+
|
|
385
|
+
- 方块**永远不会 tick**;
|
|
386
|
+
- **不报任何错、不打印任何警告**(`if (entity)` 把 `undefined` 静默吞掉了);
|
|
387
|
+
- 你在 Component 里写的 `onTick(dt)` 代码看起来完全正常,只是**从来没被调用过**。
|
|
388
|
+
|
|
389
|
+
⇒ 结论:**`Component.onTick()` 这条路只对实体成立**。
|
|
390
|
+
想做「机器 / 方块每 tick 干活」,必须**自建调度**,例如:
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
// 运行期脚本:自己驱动,不依赖 Level/Scheduler
|
|
394
|
+
import { system, world } from '@minecraft/server'
|
|
395
|
+
system.runInterval(() => {
|
|
396
|
+
// 自己遍历方块(例如按 dimension.getBlock 查询已知坐标,
|
|
397
|
+
// 或维护一份「已放置机器」的坐标表)
|
|
398
|
+
}, 1)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
或走方块**自定义组件**的 `onTick` —— 那条路属于**游戏自身的调度**(Bedrock Wiki 的方块组件列表里包含 [Tick 组件](https://wiki.bedrock.dev/blocks/block-components#tick)),**不经过本框架的 `Scheduler`**。⚠️ 其具体的驱动关系(`onTick` 与 `minecraft:tick` 如何配合)**未验证** —— 本项目无法启动 Minecraft。
|
|
402
|
+
|
|
285
403
|
### 4.3 MinecraftGameInstance
|
|
286
404
|
|
|
287
405
|
```typescript
|
|
406
|
+
// src/oc/minecraft/core.ts:68-100
|
|
288
407
|
abstract class MinecraftGameInstance implements GameInstance {
|
|
289
|
-
onStart() {
|
|
290
|
-
|
|
291
|
-
this.
|
|
292
|
-
level.start()
|
|
293
|
-
system.run(() => this.afterStart?.()) // 下一 tick 执行 afterStart
|
|
408
|
+
onStart(ev: StartupEvent) {
|
|
409
|
+
this.setLevel(new MinecraftLevel()) // setLevel 内部会调 lvl.start()
|
|
410
|
+
system.runTimeout(() => this.afterStart(), 1) // 下一 tick 执行 afterStart
|
|
294
411
|
}
|
|
412
|
+
shutdown() { this.level?.stop?.() }
|
|
413
|
+
afterStart() {} // 子类覆写
|
|
295
414
|
}
|
|
296
415
|
```
|
|
297
416
|
|
|
@@ -571,12 +690,26 @@ KeyState (独立)
|
|
|
571
690
|
## 13. 包入口 (`index.ts`)
|
|
572
691
|
|
|
573
692
|
```typescript
|
|
574
|
-
// src/oc/index.ts
|
|
575
|
-
export * from './core.js'
|
|
576
|
-
export * from './optional.js'
|
|
577
|
-
export * from './
|
|
578
|
-
export * from './
|
|
579
|
-
export * from './
|
|
693
|
+
// src/oc/index.ts (共 10 条导出,与源码逐行对应)
|
|
694
|
+
export * from './core.js' // Component 体系 + ComponentManager + lazyGet + RequireComponents
|
|
695
|
+
export * from './optional.js' // Optional<T>
|
|
696
|
+
export * from './components/index.js'// 【新增】运行期自定义组件注册(路线 B)
|
|
697
|
+
export * from './input/base.js' // PlayerInputComponent
|
|
698
|
+
export * from './arch.js' // GameInstance, initialize
|
|
699
|
+
export * from './math/index.js' // Vec3, Vec4, Matrix, Spline, MathExt
|
|
580
700
|
export * from './minecraft/index.js' // Minecraft 集成全部
|
|
581
|
-
export * from './
|
|
701
|
+
export * from './persist/index.js' // 【新增】分块持久化
|
|
702
|
+
export * from './ui/hud.js' // HudComponent
|
|
703
|
+
export * from './builtin/index.js' // 【新增】框架内建自定义组件(registerBuiltinComponents)
|
|
582
704
|
```
|
|
705
|
+
|
|
706
|
+
### 本轮新增的两个运行期模块(架构定位)
|
|
707
|
+
|
|
708
|
+
| 模块 | 目录 | 定位 | 一行用途 |
|
|
709
|
+
|------|------|------|---------|
|
|
710
|
+
| **分块持久化** | `src/oc/persist/` | 运行期(`@sapdon/runtime`),**会被打包进脚本包** | 把超过动态属性上限(约 32KB)的大 JSON **分块存取**,避免 `setDynamicProperty` 抛错被吞掉导致**静默丢存档** |
|
|
711
|
+
| **自定义组件注册** | `src/oc/components/` | 运行期(`@sapdon/runtime`),**会被打包进脚本包** | 在**运行期脚本**里声明自定义组件 id + handler,由框架保证在 `system.beforeEvents.startup` 时机注册(路线 B;handler 是普通闭包,可以 `import` 共享模块) |
|
|
712
|
+
|
|
713
|
+
> 📖 **这两个模块的 API 签名、参数与示例见[运行期 API 文档](../user/api/runtime.md)** —— 本节只说明「模块存在 + 属于哪一层 + 干什么」,API 细节不在此重复。
|
|
714
|
+
|
|
715
|
+
⚠️ 注意与**路线 A** 的分工:`BlockCustomComponentBuilder`(构建期声明,CLI 生成 `scripts/custom_components/*.js`,handler 靠 `toString()` 序列化)仍然兼容,但**同一个组件 id 不要同时用两条路线注册**(`src/oc/components/registry.ts:14-25`)。运行期注册必须在**脚本模块加载期**调用(顶层语句),启动完成后再注册会**抛错**(宁可炸也不静默)。
|
|
@@ -23,6 +23,7 @@ src/core/ui/
|
|
|
23
23
|
└── systems/ # 系统层:UISystem + 各类落地系统
|
|
24
24
|
├── system.ts # UISystem = 一个 UI 文件(.json),序列化入口
|
|
25
25
|
├── chest.ts # ChestUISystem(接管 vanilla 箱子 screen)
|
|
26
|
+
├── containerLayout.ts # 纯函数:槽号 ↔ grid_position、像素 pos ↔ offset、槽位校验(零 import)
|
|
26
27
|
├── containerUISystem.ts # ContainerUISystem(自定义容器 UI)
|
|
27
28
|
├── hud/ # HudUISystem + HudStatePanel
|
|
28
29
|
├── neoGuibook/ # (旧)NeoGuidebook + NeoGuidebookPage(兼容保留,新用 SapdonGuideBook)
|
|
@@ -226,6 +227,88 @@ long_form ─(modification: bindings)→ title 含 'sapdon_ui:' 时隐藏原生
|
|
|
226
227
|
|
|
227
228
|
`HudStatePanel` 的核心是"状态字符串"驱动:根面板定义 `$update_string`,监听 `#hud_title_text_string` 变化;每个 `addStateControl(state, control)` 给子控件挂两条 view 绑定——一条回填文本、一条 `(#text = 'ui.hud.<name>.<state>') → #visible`。组件作者只需在 tick 里往 `#hud_title_text_string` 写 `<name>.<state>`,对应状态层即显隐。
|
|
228
229
|
|
|
230
|
+
### 4.3 自定义容器面板(`ContainerUISystem`)
|
|
231
|
+
|
|
232
|
+
`ContainerUISystem(identifier, path)` 生成一份**绝对像素版面**的容器根面板,并通过
|
|
233
|
+
`ChestUISystem.registerContainerUI(name, '<ns>.container_root_panel')` 接管原版小箱子界面。
|
|
234
|
+
门控键 = identifier 冒号后半段(同时也是 `ui/<name>.json` 的文件名,只允许 `A-Z a-z 0-9 _ -`)。
|
|
235
|
+
|
|
236
|
+
**生成的根面板骨架**(`container_root_panel`,尺寸 = `setPanel({ size })`):
|
|
237
|
+
|
|
238
|
+
| 子控件 | 定位 | layer | 说明 |
|
|
239
|
+
|---|---|---|---|
|
|
240
|
+
| `common_panel@common.common_panel` | 原版 | — | 原版灰底 |
|
|
241
|
+
| `inventory_selected_icon_button@…` | 原版 | — | 飞行动画图标按钮 |
|
|
242
|
+
| `panel_background`(可选) | `top_left` @ `[0,0]`,size = 面板尺寸 | 1 | `setPanel({ background })` |
|
|
243
|
+
| `title` | `top_left` @ `[0,0]`,`100%` 宽 | 12 | `setTitle()`;`text_alignment: center` |
|
|
244
|
+
| `grids`(`type: grid`) | `top_left` @ `setGridOrigin([x,y])` | 3 | `collection_name: container_items`,尺寸 = 列×格位尺寸 |
|
|
245
|
+
| `main_panel` | `top_left` @ `[0,0]`,size = 面板尺寸 | 4 | `addControl(el, pos)` 的落点 |
|
|
246
|
+
| `inventory_panel` | `bottom_left` @ `[0,0]`,`100%×50%` | 2 | 原版背包 + 快捷栏 + 取物进度按钮 |
|
|
247
|
+
|
|
248
|
+
**槽位 API**(推荐路径):
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
ui.setPanel({ size: [256, 128], background: 'textures/ui/machine_panel' })
|
|
252
|
+
.setGridOrigin([8, 40])
|
|
253
|
+
.setSlotDefaults({ cellSize: 20, background: 'textures/ui/slot_bg' })
|
|
254
|
+
.addSlot({ slot: 0, pos: [8, 40], kind: 'input' })
|
|
255
|
+
.addSlot({ slot: 1, pos: [30, 40], kind: 'input' })
|
|
256
|
+
.addSlot({ slot: 2, pos: [60, 40], kind: 'output' })
|
|
257
|
+
.addSlot({ slot: 3, pos: [82, 40], kind: 'display' }) // 伪进度条:脚本每 tick 换物品
|
|
258
|
+
.addControl(new Label('hint').setText(new Text().setText('…')), [8, 8])
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
- **`slot` = 容器槽位号**(与版面解耦):框架按 `[slot % columns, slot / columns]`(默认单列)换成
|
|
262
|
+
`grid_position`;`pos` = 面板内像素坐标,框架内部换算成格位 `offset`,调用方不手算偏移。
|
|
263
|
+
- **`kind`**:`input` 不写标志位;`output` / `display` **缺省**写 `"enabled": false`
|
|
264
|
+
(`display` 语义 = 不进不出、纯显示,供项目脚本每 tick 换物品做伪进度条)。
|
|
265
|
+
★ 显式传 `enabled` 时**一律以它为准**(`resolveSlot`)—— 真机已确认 `enabled: false` 是
|
|
266
|
+
**整体禁用这一格**(连"把产物取出来"都会被拦)⇒ **产物格必须写
|
|
267
|
+
`addSlot({ kind: 'output', enabled: true })`**,见 `known-pitfalls.md` §4.14。
|
|
268
|
+
- **每格可覆盖的原版变量**(`SlotSpec`):`cellSize`(**视觉**尺寸 → `size` + `$cell_image_size|default`,
|
|
269
|
+
可溢出格位)、`background`(→ 生成背景 image 控件 + `$background_images|default`)、
|
|
270
|
+
`itemRenderer.{ref,size,offset,panelSize}`(→ `$item_renderer*|default`),
|
|
271
|
+
以及 `vars` 原样透传(键写进 `$<key>|default`)。
|
|
272
|
+
- **网格几何只认一处**:`setSlotDefaults({ cellSize })`(缺省 = 标定表 `cellSize`)——
|
|
273
|
+
网格尺寸与格位基座换算都用它;**逐槽 `cellSize` 不参与几何**(引擎的网格格位是均匀的)。
|
|
274
|
+
混用尺寸把基座算歪的经过见 `known-pitfalls.md` §4.11。
|
|
275
|
+
- **坐标标定**:`containerLayout.ts` 的 `SLOT_CALIBRATION` 是**唯一的待真机校准点**
|
|
276
|
+
(`anchor` / `originPadding` / `cellSize` / `columns` / `defaultGridOrigin`);改 `anchor` 会同时翻转换算
|
|
277
|
+
与产物里的 `anchor_from` / `anchor_to`。`defaultGridOrigin`(`[0, 24]`)是未调 `setGridOrigin` 时的网格原点,
|
|
278
|
+
已给顶部标题让开一行。详见 `known-pitfalls.md` §4.9。
|
|
279
|
+
- **旧接口**:`addInputGrid` / `addOutputGrid` / `addGridItem` / `setGridDimension` / `setSize` /
|
|
280
|
+
`setTitle` / `addElementToMain` 全部保留为薄封装(显式 `grid_position` + 显式 `offset`,不参与换算);
|
|
281
|
+
`setInputGrid` 是 `setOutputSlots` 的 `@deprecated` 别名。**`setItemMatrix` 已删除**
|
|
282
|
+
(三个独立缺陷,见 `known-pitfalls.md` §4.7)。
|
|
283
|
+
|
|
284
|
+
**进度指示槽 API**:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
ui.addProgressSlot({
|
|
288
|
+
slot: 3, pos: [77, 42], size: [22, 15], // 位置/尺寸 = 原版熔炉箭头
|
|
289
|
+
base: 'textures/ui/arrow_inactive', // 静止底图(可选;垫在下面当兜底)
|
|
290
|
+
fill: 'textures/ui/arrow_active', // 按比例裁开的填充图
|
|
291
|
+
clipDirection: 'left', // 'left' = 从左往右填;火焰那类从下往上用 'down'
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
- **比例取「本格物品的剩余耐久」**:`fill` 上挂了两条 collection 绑定
|
|
296
|
+
(`#item_durability_current_amount` / `#item_durability_total_amount`)与一条 view 绑定
|
|
297
|
+
(Molang 除法 → `#clip_ratio`)。脚本往该槽写一个可损耗物品、让「剩余耐久 = 进度」即可
|
|
298
|
+
(见 `examples/mob_chest/scripts/progress_bar.js`),**不需要任何进度条贴图**。
|
|
299
|
+
⚠️ `current_amount` 是**已损耗量**,所以框架内部做了取反 —— 别照直觉写 `current / total`,
|
|
300
|
+
依据见 `known-pitfalls.md` §4.12。
|
|
301
|
+
- **它比 `addSlot` 多做三件事**:① 生成一个自定控件、经 `$cell_overlay_ref` 注入到
|
|
302
|
+
`common.container_item` 的 `item_cell` 内(因此保留**每格**的 collection 上下文,每格各显示各的进度);
|
|
303
|
+
② 关掉引擎自带的耐久条(`$durability_bar_required: false`);③ 把格子的浅灰底换成零尺寸面板
|
|
304
|
+
(`$background_images`),否则会看到「灰方块 + 图」而不是原版那样只有图。`vars` 可覆盖后两条。
|
|
305
|
+
- `kind` 固定为 `display`、物品图标尺寸归零(只要图不要图标)。
|
|
306
|
+
- **为什么要自己算比例**:原版熔炉的 `#furnace_arrow_ratio` / `#furnace_flame_ratio` 是引擎按界面
|
|
307
|
+
硬编码下发的,挂在 `chest_screen` 上的自定义面板拿不到;同理 `progress_bar_renderer` 只能画色块、
|
|
308
|
+
没有 texture 属性,画不出箭头形状。完整考证与真机实测见 `known-pitfalls.md` §4.12。
|
|
309
|
+
- **判据**:`node tests/container-ui-output.test.mjs` 的进度槽三条
|
|
310
|
+
(产物里有 `<ns>.progress_<slot>` 控制、`$cell_overlay_ref` 指过去、比例是取反表达式)。
|
|
311
|
+
|
|
229
312
|
---
|
|
230
313
|
|
|
231
314
|
## 5. 对称门控模型(`examples/guidebook_demo`)
|
package/doc/dev/workflow.md
CHANGED
|
@@ -15,21 +15,54 @@
|
|
|
15
15
|
|
|
16
16
|
## 2. 框架自身的构建流程
|
|
17
17
|
|
|
18
|
+
`npm run build` → `node scripts/build.cjs`,串起**四步**:
|
|
19
|
+
|
|
18
20
|
```
|
|
19
21
|
npm run build
|
|
20
22
|
│
|
|
21
|
-
├─ tsc
|
|
22
|
-
├─ tsc-alias
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
├─ 1. tsc → src/ → dist/ (TypeScript 编译,保留路径结构)
|
|
24
|
+
├─ 2. npx tsc-alias → 解析路径别名(@sapdon/* → 相对路径)
|
|
25
|
+
├─ 3. node scripts/buildTask.cjs → dist/ → prod/ (rollup 打包为 ESM bundle)
|
|
26
|
+
│ ├─ prod/cli/start.js CLI 入口
|
|
27
|
+
│ ├─ prod/cli/index.js CLI 库
|
|
28
|
+
│ ├─ prod/core/index.js @sapdon/core
|
|
29
|
+
│ ├─ prod/oc/index.js @sapdon/runtime
|
|
30
|
+
│ ├─ prod/utils/index.js @sapdon/utils
|
|
31
|
+
│ └─ prod/*/index.d.ts TypeScript 声明文件
|
|
32
|
+
└─ 4. 收尾:拷贝 src/templates → prod/templates,并删除 dist/
|
|
33
|
+
(`node scripts/build.cjs keep` 可保留 dist/)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> 打包脚本的**实际文件名是 `scripts/buildTask.cjs`**(由 `scripts/build.cjs:92` 以 `node ./scripts/buildTask.cjs` 调用),任务的配置在 `scripts/buildConfig.cjs`,共 9 个任务(`buildTask.cjs:9-19`)。
|
|
37
|
+
|
|
38
|
+
### 2.1 受限环境下的「4 步直跑」
|
|
39
|
+
|
|
40
|
+
本环境(受限沙箱)下 `npm run build` 可能因 spawn/管道限制失败,此时**直接拆开跑这 4 步**:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx tsc
|
|
44
|
+
npx tsc-alias
|
|
45
|
+
node scripts/buildTask.cjs
|
|
46
|
+
# 再手工:拷贝 src/templates → prod/templates,然后删除 dist/
|
|
30
47
|
```
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
### 2.2 两条实测坑(务必照做)
|
|
50
|
+
|
|
51
|
+
1. ★ **不能漏掉 `tsc-alias`**。
|
|
52
|
+
漏掉它,`prod/` 里的 TS 产物会**残留无法解析的裸包名**(例如 `prod/cli/start.js` 里的 `@sapdon/utils`)。游戏/Node 侧都解析不了这个别名 —— 症状是运行期报模块找不到,而不是构建报错。
|
|
53
|
+
2. ★ **「rollup 9/9 Successful」≠ `prod/` 是新的**。
|
|
54
|
+
`buildTask.cjs` 只在 `failCount > 0` 时 `exit 1`;即使 `Failed: 0`,也可能是产物没更新(缓存/输入未变/拷贝环节被跳过)。**必须断言 `prod/` 的内容**,例如确认新加的导出真的在 `prod/core/index.d.ts` 或 `prod/oc/index.js` 里,而不是只看 `Failed: 0`。
|
|
55
|
+
|
|
56
|
+
### 2.3 单元测试
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
node tests/persist.test.mjs # ✅ 直接跑
|
|
60
|
+
node --test "tests/*.test.mjs" # ⚠️ 受限环境下会 fork 子进程(EPERM)而失败
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `node --test` 在受限环境里会**fork 子进程**(命名管道 `EPERM`)→ 直接 `node tests/<name>.test.mjs`。
|
|
64
|
+
- 测试 `import` 的是 `../dist/...`,所以**必须先 `tsc` 生成 `dist/`**,否则测试 import 不到模块。
|
|
65
|
+
- 运行期代码(`src/oc`)的单测要靠**纯逻辑 + 内存 target**,不要依赖 `@minecraft/server`(该包只发 `index.d.ts`,Node 里导入不了)。
|
|
33
66
|
|
|
34
67
|
---
|
|
35
68
|
|
|
@@ -117,7 +150,14 @@ npm run build # 构建 + 启动开发服务器(HMR)
|
|
|
117
150
|
|
|
118
151
|
### 6.3 编译后服务器不退出
|
|
119
152
|
|
|
120
|
-
`sapdon compile`
|
|
153
|
+
`sapdon compile` / `sapdon build` **默认构建完成后自动退出**(`keepServer` 默认 false,`src/cli/build.js:324-328`):
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
[sapdon] 构建完成,开发服务器已自动退出。
|
|
157
|
+
[sapdon] 如需保持服务器常开(HMR / 热更新),请在 build.config 中设置 "buildOptions.keepServer": true
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
如果服务器一直占端口,检查 `build.config` 中是否设置了 `buildOptions.keepServer: true`。多进程/多 agent 并发构建时还应各自设置 `SAPDON_DEV_SERVER_PORT`,端口被占时 CLI 会直接 `exit 1`。
|
|
121
161
|
|
|
122
162
|
### 6.4 新建项目后忘记同步库
|
|
123
163
|
|
|
@@ -127,7 +167,7 @@ npm run build # 构建 + 启动开发服务器(HMR)
|
|
|
127
167
|
|
|
128
168
|
`src/cli/build.js` 中 manifest 只在**不存在时**才生成(`if (pathNotExist(manifestPath))`),目的是保留 uuid。因此修改 `build.config` 里的 `@minecraft/server` 版本后,旧 manifest 会被保留、新版本不生效。
|
|
129
169
|
|
|
130
|
-
**解决**:手动删除 `dev/<name>_BP/manifest.json` 后重新 `sapdon compile`。删除 manifest 不影响 uuid
|
|
170
|
+
**解决**:手动删除 `dev/<name>_BP/manifest.json` 后重新 `sapdon compile`。删除 manifest 不影响 uuid —— **uuid 就存在项目的 `mod.info` 里**(`bp` / `rp` 两个字段,由 `loadOrCreateUuids()` 读写,`src/cli/build.js:49-62`),删 manifest 后重建会从 `mod.info` 复用同一组 uuid,BP/RP 交叉绑定不会断。
|
|
131
171
|
|
|
132
172
|
### 6.6 方块脚本事件不存在(blockTick / blockUpdate)与自定义组件注册时机
|
|
133
173
|
|
|
@@ -162,7 +202,7 @@ npm run build # 构建 + 启动开发服务器(HMR)
|
|
|
162
202
|
|
|
163
203
|
**问题**:Minecraft 从 format version 1.26.20 起,`minecraft:material_instances` 的 `ambient_occlusion` 不接受布尔值,必须是 0.0–10.0 浮点数。
|
|
164
204
|
|
|
165
|
-
**修复位置**:`src/
|
|
205
|
+
**修复位置**:`src/core/block/block.js`(材质实例 `ambient_occlusion` 归一化,见 `block.js:70-75`)。
|
|
166
206
|
|
|
167
207
|
**修复前**:
|
|
168
208
|
```ts
|
|
@@ -176,14 +216,16 @@ ambient_occlusion: "number" == typeof i ? i : !1 === i ? 0 : 1
|
|
|
176
216
|
// 生成: "ambient_occlusion": 0 ← 合法浮点数
|
|
177
217
|
```
|
|
178
218
|
|
|
179
|
-
### 案例 2
|
|
219
|
+
### 案例 2:`keepServer` 配置项(**已实现**)
|
|
180
220
|
|
|
181
|
-
|
|
221
|
+
**背景**:`sapdon compile` 构建完成后 dev server 不退出,一直占端口。需要一个开关控制是否常驻。
|
|
182
222
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
- `src/
|
|
223
|
+
**状态:已实现**(不再是待做项)。默认 `keepServer` 为 false,构建完成后打印提示并 `process.exit(0)`;设为 true 则保持服务器常开(配合 `useHMR`)。
|
|
224
|
+
|
|
225
|
+
**实现位置**(均已落地):
|
|
226
|
+
- `src/cli/meta/buildConfig.ts:16`:`BuildOptions` 中的 `keepServer?: boolean` 字段。
|
|
227
|
+
- `src/cli/build.js:323-328`:`buildProject()` 末尾的自动退出逻辑 —— `if (!getBuildConfig().buildOptions.keepServer) { ... process.exit(0) }`。
|
|
228
|
+
- `src/templates/js_sapdon/build.config` 和 `src/templates/ts_sapdon/build.config`:模板中带 `keepServer` 字段及注释(ts 模板见第 5 行)。
|
|
187
229
|
|
|
188
230
|
**使用方式**:
|
|
189
231
|
```json
|