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
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
# 已知坑清单(框架侧)
|
|
2
|
+
|
|
3
|
+
> 只记**已经踩过、且下次还会踩**的坑。每条都要有可验证的判据(断言 / 产物 / 症状),
|
|
4
|
+
> 不要写"注意事项"式的空话。新增条目请按「症状 → 根因 → 判据/做法」写。
|
|
5
|
+
>
|
|
6
|
+
> 相关:`AGENTS.md`(约定沉淀)、`doc/dev/workflow.md`(构建流程)、`doc/dev/ui-lessons.md`(UI 专项)。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. 构建与产物
|
|
11
|
+
|
|
12
|
+
### 1.1 「构建成功」不可信 —— 判据是日志里的 `处理数据:` 行
|
|
13
|
+
- **症状**:CLI 打印「构建完成」并 exit 0,但 `dev/` 里是**上一次**的旧产物;改了代码毫无反应。
|
|
14
|
+
- **根因**:历史上 `runOnChild` 只看子进程 `exit` 事件、不看退出码;`scriptBundler` 的 catch 只 `console.error`。
|
|
15
|
+
- **现在**:子进程非 0 退出即抛;`scriptBundler` 打包失败即抛;CLI 顶层 catch → `process.exit(1)`。
|
|
16
|
+
- **判据**:构建日志必须有 `处理数据: <name> <root> <path>` 行;数量要与注册数一致。**没有这行就是没生成。**
|
|
17
|
+
|
|
18
|
+
### 1.2 生成物清单 —— 陈旧产物的唯一清理依据
|
|
19
|
+
- **位置**:`dev/.sapdon_generated_<项目名>.json`(`src/cli/load.js`)。
|
|
20
|
+
- **规则**:本次构建写出的产物路径进清单;下次构建**只删「上次清单里有、这次没有」的文件**。
|
|
21
|
+
- **不要**改成"扫目录按规则删":`dev/*_RP/ui/`、`textures/` 里有用户 `res/` 拷进来的文件与手写文件,扫目录必误删。
|
|
22
|
+
- **改名/删条目**(清单管得到的)自动清理;**重命名项目**(清单记的是**当前**项目名)管不到 → 需**手工删** `dev/<旧名>_*`。实例:`examples/guidebook_demo/dev/ui_gated_demo_RP/`(旧项目名残留,已手工删除)。
|
|
23
|
+
- **没有清单的项目**(例如一直在构建失败的示例)里的陈旧文件也清理不到 —— 清单是"上次构建写过什么",从没有过成功构建就没有清单。
|
|
24
|
+
- **历史遗留**:框架现在只往 RP 写 `blocks.json`,但早期误写在 BP 的那些文件不在任何清单里,故对 `${proj}_BP/blocks.json` 这个**确切路径**做无条件点名清理(不做目录扫描)。
|
|
25
|
+
- **另外两份清单**(2026-09 新增,同样"只删自己上次记过的东西"):`dev/.sapdon_synced_<项目名>.json`
|
|
26
|
+
管**游戏开发包目录**里的陈旧副本(`syncFiles.js` 的 `syncDevFilesServer()`,HMR 也走它),
|
|
27
|
+
`dev/.sapdon_res_<项目名>.json` 管 `dev/<项目名>_RP` 里从 `res/` 拷进来的陈旧资源(`syncResourceFiles()`)。
|
|
28
|
+
症状、取舍与负面约束见 `doc/dev/cli.md` 的「按清单 prune」小节;单测 `node tests/sync-manifest.test.mjs`。
|
|
29
|
+
|
|
30
|
+
### 1.3 `blocks.json` 属**资源包**;并且**自 2026-09 起框架不再往里面写任何方块条目**
|
|
31
|
+
- **位置**:`src/core/factory/blockFactory.js` → `GRegistry.register("blocks","resource","",…)` → `dev/<proj>_RP/blocks.json`。
|
|
32
|
+
- **规范**:`blocks.json` 是 RP 根目录文件;写在 BP 里会被 Bedrock 完全忽略。
|
|
33
|
+
- **历史**:`e1199cc` 的 `src/cli/load.js` 写的就是 `${proj}_RP/blocks.json`;`05bd104` 挪进 `blockFactory.js` 时误标 `"behavior"`,从此落在 BP。**已回归修复**。
|
|
34
|
+
- **键格式的历史(保留记录,但已无实际用途)**:`3715e74` 曾把键从"文件名安全名 `ns_name`"改成**完整标识符 `ns:name`**:
|
|
35
|
+
- 权威源:<https://wiki.bedrock.dev/blocks/block-sounds> 的 `RP/blocks.json` 示例键为 `"wiki:chestnut_log"`。
|
|
36
|
+
- 历史产物(预言机):`git show e1199cc:examples/mob_chest/dev/mob_chest_RP/blocks.json` → `"mob_chest:chest"` / `"sapdon:falling_block"`。
|
|
37
|
+
- `_` 形态的来历:复用 `block_name`,而当时根目录还写进 BP(不生效)→ **从没有项目依赖过 `_` 形态**。
|
|
38
|
+
- ⚠️ `blocks/<name>.json` 的**文件名**仍必须是 `_` 形态(`:` 在 Windows 文件名里非法)—— 两者不可混用。
|
|
39
|
+
- ★ **2026-09 起:只写 `{"format_version": "1.20.20"}`,不写任何方块条目**(`blockFactory.js:34-59` 有完整依据):
|
|
40
|
+
- **症状**:键修成完整标识符后引擎**真的匹配上了**这些方块,于是**每个自定义方块**报一条
|
|
41
|
+
```
|
|
42
|
+
[Blocks][warning]-<ns>:<block>: trying to override the Geometry component with blocks.json settings
|
|
43
|
+
for a custom block. This isn't supported.
|
|
44
|
+
Please remove any legacy texture definition or block shape specification for this block.
|
|
45
|
+
```
|
|
46
|
+
真机计数:探针轮 **3** 条 → FZ 全量 **77** 条(= 该项目方块总数)。
|
|
47
|
+
- **依据**(Microsoft Learn · blocks.json File Reference,原文):
|
|
48
|
+
"components in Behavior Packs, specifying `minecraft:geometry` and `minecraft:material_instances`, will override
|
|
49
|
+
configurations here. Components are more powerful, and they're the recommended way to specify visual properties for
|
|
50
|
+
blocks, leaving **blocks.json** as just a sound configuration system."
|
|
51
|
+
<https://learn.microsoft.com/en-us/minecraft/creator/reference/content/blockreference/examples/blocksjsonfilestructure>
|
|
52
|
+
⇒ 自定义方块的 `textures` 是**被 `material_instances` 全面覆盖**的 legacy 机制,写了只会招警告。
|
|
53
|
+
- **贴图到底谁提供**:`minecraft:material_instances`(BP 方块 JSON,6 面或 `*`)+ `terrain_texture.json`
|
|
54
|
+
(`src/core/texture.js` / `cli/tools/textureSet.js` 扫描 `RP/textures/blocks/**.png` + 用户注册项生成)
|
|
55
|
+
—— **不经过** blocks.json。
|
|
56
|
+
- **护栏 `assertBlocksJsonKey` 已删**(不留永远不会触发的死护栏):不再写条目后,"键必须是 `ns:name`"没有触发场景。
|
|
57
|
+
- **⚠️ 未验证(需真机)**:只含 `format_version` 的 `blocks.json` 引擎会不会抱怨;以及矿挖/放置音效是否正常
|
|
58
|
+
(音效本来就没配过 —— 框架从未写过 `sound` 字段,故预期音效行为与改动前一致)。
|
|
59
|
+
真机验证方式:重进世界看那 77 条 `trying to override the Geometry component` 是否消失 + 挖掘/放置音效正常。
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### 1.4 目录名大小写:只允许 `_BP` / `_RP`
|
|
63
|
+
- Windows 大小写不敏感才掩盖了这个问题;Linux/macOS 下 `_bp` 与 `_BP` 会分叉成两个目录(构建写 A、打包读 B → 空包)。
|
|
64
|
+
- **断言**:`src/` 里 grep `_bp`/`_rp` 只应命中注释与无关的 `data_bp.json`/`data_rp.json`。
|
|
65
|
+
|
|
66
|
+
### 1.5 Dev Server 端口可配(多项目并行构建)
|
|
67
|
+
- `build.config` 无关:端口由环境变量 `SAPDON_DEV_SERVER_PORT` 决定,默认 `49037`。
|
|
68
|
+
- 服务端(`src/cli/dev-server/config.js`)与客户端(`src/core/transport/client.ts`)**必须读同一个变量** —— 否则会出现「客户端 POST 到 A、服务端监听 B」的静默失联。
|
|
69
|
+
- 端口被占时 CLI 直接 `exit 1` 并提示「已被其他 sapdon 进程占用」。多 agent/多项目并行构建请各设不同端口。
|
|
70
|
+
|
|
71
|
+
### 1.6 `scripts/custom_components/index.js` 是**合并**而不是「存在即跳过」
|
|
72
|
+
- **症状(历史)**:新增自定义组件后索引不更新;改了 builder 里的 handler 毫无反应。
|
|
73
|
+
- **根因**:`index.js` 在 ts 模板里是**预置占位文件**(还被 `scripts/index.ts` import,删不得),旧逻辑 `if (!fs.existsSync(indexPath))` 于是**永远跳过**。
|
|
74
|
+
- **现在**:标记块 `// >>> sapdon:custom-component-registry >>> … <<<` 内的内容每次重建;标记块外的手写/模板内容**一律保留**(文件里若同时有旧版 `// Auto-generated by sapdon.` 生成段与标记块,会被合并成唯一一份)。
|
|
75
|
+
- **生成块用 `import { system as __sapdon_system }`**:追加场景下文件里可能已有 `import { system }`,同名重复声明会让整个脚本包 rollup 报 `Identifier "system" has already been declared`。
|
|
76
|
+
- **幂等判据**:连跑两次构建,`index.js` 哈希不变、日志出现「已是最新,未改动」。
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### 1.7 物品的 `format_version` 决定「自定义 catalog 组名」的解析行为
|
|
81
|
+
|
|
82
|
+
**症状**:世界每次加载,`RP`/`BP` 里**每个**带自定义创造菜单分组的物品都报一条 warning(N 个物品 = N 条):
|
|
83
|
+
```
|
|
84
|
+
[Item][warning]-.../item_catalog/crafting_item_catalog.json |
|
|
85
|
+
The item fz:coolant_cell_singler was created with the group set to
|
|
86
|
+
'minecraft:fz:itemGroup.name.reactor_items', but is now being set to 'fz:itemGroup.name.reactor_items'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**根因**:物品的 `format_version` 曾是框架默认值 **`1.21.40`**(`src/core/item/item.ts` 的
|
|
90
|
+
`formatVersion ?? "1.21.40"`)。该版本下引擎会把 `menu_category.group` 当"**隐含 `minecraft:` 前缀**"处理,
|
|
91
|
+
于是与 `crafting_item_catalog.json` 里的显式组名不一致 → 每条物品报一次。**产物里根本没有 `minecraft:fz:`**
|
|
92
|
+
(两侧都是裸 `fz:itemGroup.name.X`,与 [Bedrock Wiki · Item Catalog](https://wiki.bedrock.dev/items/item-catalog)
|
|
93
|
+
的 `wiki:itemGroup.name.ore` 同形)—— 这是引擎行为(对应 Mojira MCPE-224150),不是产物写错。
|
|
94
|
+
|
|
95
|
+
**修法(★ 2026-09 已在框架侧根治)**:`src/core/item/item.ts` 的默认值提到 **`1.21.90`**
|
|
96
|
+
(该行旁边有完整注释)。框架**仍支持按物品覆盖**:同时解构 `format_version` 与 `formatVersion`
|
|
97
|
+
两个键名,后者优先;`examples/digitCircuit/main.mjs:101,113,124,133` 一直显式传 `1.21.90`,
|
|
98
|
+
所以它的日志里**零**条该告警。
|
|
99
|
+
|
|
100
|
+
**实测(2026-09-11,真机)**:FZ 项目 157 个物品用默认值 → 每次加载 **156** 条;
|
|
101
|
+
把**单个**物品改成 `1.21.90`(只改已部署副本)→ 同一次加载变成 **155** 条、且该物品不再出现;
|
|
102
|
+
全量改完(`FZ_ITEM_FORMAT_VERSION` 常量)→ 玩家重进世界后告警**清零**。
|
|
103
|
+
|
|
104
|
+
**产物侧影响**:改用默认值的项目,其 `dev/<proj>_BP/items/*.json` 的 `format_version` 会变成 `1.21.90`
|
|
105
|
+
(**预期且期望**的变化);显式传过版本的物品不受影响。
|
|
106
|
+
`ItemCatalog` 自己的 `format_version`(`itemCatalog.ts:40`,默认 `1.26.30`)是 **catalog 文件**的格式版本,
|
|
107
|
+
与物品无关,**不要**跟着改。
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 2. 自定义组件
|
|
113
|
+
|
|
114
|
+
### 2.1 注册时机只有两个合法位置
|
|
115
|
+
- ✅ 运行期路线 B:`@sapdon/runtime` 的 `registerBlockComponent` / `registerItemComponent`(框架内部走 `system.beforeEvents.startup`)。
|
|
116
|
+
- ✅ 构建期路线 A:`BlockCustomComponentBuilder` + CLI 生成的 `scripts/custom_components/*.js`(内部同样走 `system.beforeEvents.startup`)。
|
|
117
|
+
- ❌ `world.beforeEvents.worldInitialize`:**太晚**,症状是启动报
|
|
118
|
+
`this component was found in the input, but is not present in the Schema`(方块 JSON 里写了 `"ns:xxx": {}`,但脚本没在正确时机注册)。
|
|
119
|
+
- **守卫**:`system.beforeEvents.startup` 已触发后再调用注册会**抛错**(那时没有任何注册时机了);同一 id 重复注册也抛错;事件名拼错只 `console.warn`、不阻断。
|
|
120
|
+
- ⚠️ 「同 id 重复注册抛错」只适用于**项目注册之间**。**框架内置**组件走 `registerFallbackBlockComponent`
|
|
121
|
+
(项目没注册才注册;项目注册了 → 项目生效 + 一条 warn)—— 见 §2.4。
|
|
122
|
+
|
|
123
|
+
### 2.4 ★ `sapdon:block_with_entity` 必须由框架注册(否则**整份方块被丢**)
|
|
124
|
+
- **症状**:用 `BlockAPI.createTileBlock` 的方块全部不出现(连创造菜单里都没有),启动报
|
|
125
|
+
```
|
|
126
|
+
-> components -> sapdon:block_with_entity: this component was found in the input,
|
|
127
|
+
but is not present in the Schema
|
|
128
|
+
```
|
|
129
|
+
**不是局部报错 —— 是整份方块 JSON 被引擎丢掉**(真机实测:FZ 的探针与回收机都靠脚本手工注册才活下来)。
|
|
130
|
+
- **根因**:`TileBlock` 的构造无条件给方块挂这个自定义组件
|
|
131
|
+
(`src/core/block/tileBlock.js:232` → `BlockComponent.setCustomComponents(["sapdon:block_with_entity"])`),
|
|
132
|
+
而框架的 `registerBuiltinComponents()` 到 2026-09 为止**只注册 5 个**
|
|
133
|
+
(`crop_growth` / `fallingblock` / `head_rotation` / `intercardinal_orientation` / `guibook`)
|
|
134
|
+
—— **没有** `block_with_entity`。挂组件与注册组件是两件事,只有后者能让 Schema 认识它。
|
|
135
|
+
- **修法(已落地)**:`src/oc/builtin/blocks/blockWithEntity.ts` 提供内置实现,
|
|
136
|
+
由 `registerBuiltinComponents()` 用**兜底**通道登记(`registerFallbackBlockComponent`)。
|
|
137
|
+
内置实现 = **模式 A**:`onPlace` 里 spawn `${block.typeId}_entity`(该坐标已有同种实体则跳过;
|
|
138
|
+
读不出来时**不 spawn** —— 两个承载实体 = 两个容器 = 能复制物品)。
|
|
139
|
+
**刻意不切** `sapdon:block_or_entity` 状态:切到 1 会命中 `TileBlock` 的透明变体,
|
|
140
|
+
外观全交给实体(模式 B),框架无权替项目决定。
|
|
141
|
+
- **为什么是「兜底」而不是「直接注册」**:既有项目(`examples/mob_chest`、FZ)**早就手工注册过**这个 id,
|
|
142
|
+
而且它们的实现是**模式 B / 自定义交互**。直接注册会让 `registry.ts` 的「同 id 重复注册 → 抛错」
|
|
143
|
+
守卫在启动时炸掉这些项目;无条件覆盖又会**静默改掉它们的方块行为**(切透明与否)。
|
|
144
|
+
兜底语义:**项目注册了就以项目为准**(一条 warn 说明)、没注册才用内置。
|
|
145
|
+
判定发生在 `system.beforeEvents.startup` 回调里 —— 只有那时才能确定「所有模块是否都已加载完」。
|
|
146
|
+
- **判据**:
|
|
147
|
+
1. `prod/oc/index.d.ts` 导出 `registerFallbackBlockComponent` / `skippedFallbackComponents`,
|
|
148
|
+
`prod/oc/index.js` 含 `sapdon:block_with_entity`(改完必须断言 `prod/`,别只看 rollup 9/9);
|
|
149
|
+
2. 单测 `node tests/component-registry.test.mjs`(7 条:兜底生效 / 项目优先(两种登记顺序)/ 项目重复注册仍抛错 /
|
|
150
|
+
兜底幂等 / startup 之后抛错 / 两张账互不干扰);
|
|
151
|
+
3. **最小项目实验**:只 `createTileBlock`、**完全不手工注册** → 产物里有 `"sapdon:block_with_entity": {}`,
|
|
152
|
+
且脚本包里有内置实现(见 `.tmp/p0-1/`)。
|
|
153
|
+
|
|
154
|
+
### 2.5 ★ `createTileBlock` 的 `.d.ts` 漏声明 `group` / `hide_in_command`(TS2353)
|
|
155
|
+
- **症状**:项目写 `BlockAPI.createTileBlock(id, cat, tex, { group, hide_in_command })` 编译报
|
|
156
|
+
**TS2353**(对象字面量只能指定已知属性),而**运行期 `BasicBlock` 明明会读 `options.group`**
|
|
157
|
+
(`src/core/block/basicBlock.js:39`)—— 类型与运行期不一致。项目只能写成
|
|
158
|
+
`const opts = { group, ... }; createTileBlock(id, cat, tex, opts)`(传变量绕过多余属性检查)来苟活。
|
|
159
|
+
- **根因**:`.d.ts` 是 `src/core/factory/blockFactory.js` 的 JSDoc 推导出来的;
|
|
160
|
+
`createBasicBlock` 有 `@param {string} options.group` / `@param {boolean} options.hide_in_command`,
|
|
161
|
+
`createTileBlock` **没有**(只有 `inventory_size` / `container_type` / `can_be_siphoned_from`)。
|
|
162
|
+
- **修法**:给 `createTileBlock` 的 JSDoc 补 `[options.group]` / `[options.hide_in_command]` /
|
|
163
|
+
`[options.format_version]` / `[options.entity_texture]`(**都要写成 `[options.x]` 可选**,
|
|
164
|
+
写成 `options.x` 会变成**必填** —— 那会让只传 `{ inventory_size }` 的既有项目反而编译失败)。
|
|
165
|
+
**改源头,不要手改 `prod/`。**
|
|
166
|
+
- **同类缺口(同一次核对的结果,判据 = 运行期读的键 ∉ JSDoc 声明的键)**:
|
|
167
|
+
|
|
168
|
+
| 工厂 | 运行期会读但**未声明** | 处理 |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `createBasicBlock` | `format_version`(`basicBlock.js:33`) | 已补 |
|
|
171
|
+
| `createBlock` | `format_version` | 已补 |
|
|
172
|
+
| `createRotatableBlock` | `format_version` | 已补 |
|
|
173
|
+
| `createTileBlock` | `group` / `hide_in_command` / `format_version` | 已补(本轮 P0-2) |
|
|
174
|
+
| `createHeadBlock` | `tick_interval` / `custom_components` / `format_version`(`headBlock.js:14,19`) | 已补;它原有那三条 JSDoc(`ambient_occlusion`/`face_dimming`/`render_method`)是从 `createCropBlock` 误抄的**无效**声明 —— **保留**(删掉会让传了它们的项目从「被忽略」变成编译错误),但注释里标了「本工厂不读」 |
|
|
175
|
+
| `createOreBlock` / `createGlassBlock` / `createFenceBlock` / `createStairBlock` / `createTrapdoorBlock` / `createGeometryBlock` / `createCropBlock` | 无 JSDoc ⇒ 推导成 `options?: {}` | **无 TS2353 风险**(TS 对 `{}` 不做多余属性检查),故未动 |
|
|
176
|
+
|
|
177
|
+
⚠️ `createBasicBlock` / `createBlock` / `createRotatableBlock` / `createHeadBlock` 的**已声明键是必填**
|
|
178
|
+
(推导成 `group: string` 而不是 `group?: string`),于是 `createBasicBlock(id, cat, tex, {})` 也会报错。
|
|
179
|
+
这是既有的另一类问题(**本轮未改**:把必填改可选是纯放宽,但要单独评估,见「给框架的建议」)。
|
|
180
|
+
|
|
181
|
+
### 2.6 自定义组件的 JSON **写法**:扁平化是现行规范,`minecraft:custom_components` 已废弃
|
|
182
|
+
- 框架 `setCustomComponents(ids)` 产出的是 `"ns:comp": {}`(直接写在 `components` 里)。
|
|
183
|
+
**这是对的**,不要改成 `"minecraft:custom_components": [...]`:
|
|
184
|
+
- Microsoft Learn · Scripting V2 Overview 原文:
|
|
185
|
+
"`minecraft:custom_components` is deprecated in favor of **flattened custom components** …
|
|
186
|
+
Instead, you can write your custom components similar to any other Minecraft component."
|
|
187
|
+
<https://learn.microsoft.com/en-us/minecraft/creator/documents/scriptingv2.0.0overview#custom-components-v2>
|
|
188
|
+
- Microsoft Learn · Block Components · `minecraft:custom_components` 页首 **Important**:
|
|
189
|
+
"This type is now deprecated, and no longer in use in the latest versions of Minecraft."
|
|
190
|
+
<https://learn.microsoft.com/en-us/minecraft/creator/reference/content/blockreference/examples/blockcomponents/minecraftblock_custom_components>
|
|
191
|
+
- 1.21.90 更新说明:"Custom Components V2 is now available with new capabilities."
|
|
192
|
+
<https://learn.microsoft.com/en-us/minecraft/creator/documents/update1.21.90>
|
|
193
|
+
- ⚠️ **`Scripting/…/components-tutorial.md` 那篇教程的「Block custom components」小节仍在用数组写法**
|
|
194
|
+
(`"minecraft:custom_components": ["example:crop_grow"]`)—— 那是**没跟上 V2 的旧正文**
|
|
195
|
+
(该节 `format_version` 还写着 `1.21.10`),别拿它当规范。
|
|
196
|
+
- **产物回归判据**:真机与产物双向确认过框架写法可用(`examples/mob_chest` 的
|
|
197
|
+
`dev/mob_chest_BP/blocks/mob_chest_chest.json` 里就是 `"sapdon:block_with_entity": {}`)。
|
|
198
|
+
**改写法会让所有既有产物变化** ⇒ 除非有反例,**维持现状**。
|
|
199
|
+
|
|
200
|
+
### 2.7 `createTileBlock` 的实体贴图是**资源路径**,不是 terrain 短名
|
|
201
|
+
- **症状**:`createTileBlock(id, cat, ["machineblock_0"])` 的产物里
|
|
202
|
+
`client_entity.textures.default = "machineblock_0"` ⇒ 客户端实体报 `Missing referenced asset`。
|
|
203
|
+
- **根因**:同一个 `textures_arr` 被**两个不同的贴图系统**消费 ——
|
|
204
|
+
- **方块**侧(`material_instances.up/down/…`)要的是 `terrain_texture.json` 的**键**(terrain 短名);
|
|
205
|
+
- **实体**侧(`client_entity.textures.<name>`,`tileBlock.js` → `entity.js:19-21`)要的是**资源路径**
|
|
206
|
+
(官方示例 `"default": "textures/entity/pig/pig"`,省略扩展名):
|
|
207
|
+
<https://learn.microsoft.com/en-us/minecraft/creator/reference/content/entityreference/examples/cliententitydocumentation/cliententitydocumentationintroduction>
|
|
208
|
+
框架此前把 `textures_arr[0]` **原样**写进实体贴图 ⇒ 只给短名的项目必然指向不存在的资源。
|
|
209
|
+
- **修法(已落地)**:`createTileBlock` 新增 `options.entity_texture`(**默认 = `textures_arr[0]`**,
|
|
210
|
+
即不传时产物**逐字节不变**)。只给 terrain 短名的项目传一次
|
|
211
|
+
`{ entity_texture: 'textures/blocks/entity/normal' }` 即可,不必再像 FZ 那样在
|
|
212
|
+
`declareTileBlock` 里 `tile.entity.resource.addTexture(...)` 二次覆盖。
|
|
213
|
+
- **判据**:单测 `tests/block-api.test.mjs` 的三条 `entity_texture` 用例;
|
|
214
|
+
以及 `examples/mob_chest` 重建后 `dev/mob_chest_RP/entity/mob_chest_chest_entity.json`
|
|
215
|
+
与改动前**逐字节一致**(它传的本来就是完整路径)。
|
|
216
|
+
|
|
217
|
+
### 2.8 `createTileBlock` 要求项目**自带** `geometry.cube`(与 terrain 键 `none`)
|
|
218
|
+
- `TileBlock` 的状态 1 变体与承载实体都用 `geometry.cube`
|
|
219
|
+
(`tileBlock.js` 的 `setGeometry("geometry.cube")` / `addGeometry("default","geometry.cube")`),
|
|
220
|
+
而 `geometry.cube` 是**自定义**几何(不是原版几何名)—— 框架只生成 JSON,**生不出 RP 里的几何文件**。
|
|
221
|
+
- **症状**:状态 1 下(以及承载实体)**没有模型**;引擎报 `Missing referenced asset` 一类。
|
|
222
|
+
- **做法**:项目在 `res/models/blocks/` 放一份 `identifier = "geometry.cube"` 的立方体几何
|
|
223
|
+
(16³、pivot 在底面 —— 参考 `examples/mob_chest/res/models/blocks/cube.geo.json`)。
|
|
224
|
+
**模板已随框架提供默认实现**:`src/templates/{js,ts}_sapdon/res/models/blocks/cube.geo.json`
|
|
225
|
+
(`sapdon create` 出来的新项目直接可用;**既有项目要自己拷一份**)。
|
|
226
|
+
透明变体用的 terrain 键 `none` 同样要存在(模板有 `res/textures/blocks/none.png`)。
|
|
227
|
+
|
|
228
|
+
### 2.2 路线 A 的 handler 会被 `toString()` 序列化
|
|
229
|
+
- **症状**:生成的 `scripts/custom_components/<name>.js` 里只有**调用**、没有定义 → 运行期 `ReferenceError: xxx is not defined`。
|
|
230
|
+
- **结论**:路线 A 只适合**自包含**的 handler;要 import 共享模块(S3 的机器基类这类)必须用路线 B。
|
|
231
|
+
- **`scripts/custom_components/<name>.js` 是「生成一次、之后归用户」**:存在即跳过(不覆盖用户改过的实现)。想让框架持续托管就别改它,或者改用路线 B。
|
|
232
|
+
|
|
233
|
+
### 2.3 物品自定义组件
|
|
234
|
+
- 物品要能触发 `onUse`,**必须**加 `minecraft:interact_button`(`ItemComponent.setInteractButton`),否则右键毫无反应。
|
|
235
|
+
- 物品用 `@minecraft/server` 的 `init.itemComponentRegistry`,与方块是两个注册表,别混。
|
|
236
|
+
|
|
237
|
+
### 2.9 ★ `minecraft:block_placer.block` 传**对象**会被引擎拒(schema 允许 ≠ 引擎接受)
|
|
238
|
+
- **症状**:物品带 `"minecraft:block_placer": { "block": { "name": "ns:blk", "states": { … } } }` 时,加载世界报
|
|
239
|
+
```
|
|
240
|
+
[Item][error]- Failed to parse field ' -> components -> minecraft:block_placer -> block: invalid string'
|
|
241
|
+
[Item][error]- Error Parsing Item 'ns:某物品':
|
|
242
|
+
```
|
|
243
|
+
并且**整份物品定义作废** —— 连带 `Missing icon for data-driven item 'ns:某物品'`(每个物品刷几百~上千行,
|
|
244
|
+
实测 5 个物品共 3962 行)⇒ 表现为「物品图标没了 / 右键没反应」,但**报错信息与图标无关**,极易误判成图标问题。
|
|
245
|
+
- **根因**:官方 DataForm(`@minecraft/bedrock-schemas` 的 `forms/item/minecraft_block_placer.form.json`)
|
|
246
|
+
把 `block` 标成 `dataType: "object"`,`forms/item/blockdescriptorproxy.form.json` 的描述也点名
|
|
247
|
+
`minecraft:block_placer` 用 BlockDescriptor —— 但**当前引擎只收字符串**。
|
|
248
|
+
⇒ 通例:**schema 是能力清单,不是可用性保证**;新字段先按最保守形态跑通,再谈花哨写法。
|
|
249
|
+
- **做法**:`block` 传字符串(`ItemComponent.setBlockPlacer('ns:blk')`,落方块默认状态)。
|
|
250
|
+
「同一方块 + 不同状态(多种材质/变体)」要靠**脚本补写**:
|
|
251
|
+
`beforeEvents.playerInteractWithBlock` 记手持物(此时还读得到)→ `afterEvents.playerPlaceBlock`
|
|
252
|
+
按物品映射出状态值并 `setPermutation` + 写后回读。
|
|
253
|
+
必须用 before-event 记:**生存模式放下最后一个时 after-event 里手中已经空了**。
|
|
254
|
+
- **出处**:真机 ContentLog(`%APPDATA%\Minecraft Bedrock\logs\ContentLog*.txt`),两轮加载
|
|
255
|
+
`invalid string` 20 条 / `Error Parsing Item` 10 条 / `Missing icon` 3962 条。
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 3. 持久化(动态属性)
|
|
260
|
+
|
|
261
|
+
### 3.1 单个动态属性值约 32KB 上限,超限**抛错**
|
|
262
|
+
- **★ 绝不要用 try-catch 吞掉**:吞掉 = **静默丢存档**(症状:重进世界后数据回到早期快照)。
|
|
263
|
+
- 用 `@sapdon/runtime` 的 `saveChunked` / `loadChunked` / `clearChunked`(`src/oc/persist/chunked.ts`):
|
|
264
|
+
单值 ≤ `CHUNK_SIZE`(24000) 直存;超限切成 `<key>#0..#N-1`,主 key 存 `{"_chunks":N}`(与 `lr-framework` 的 `BaseEngine`、`digitCircuit` 的 `CIRCUIT_CHUNK=24000` 格式互通)。
|
|
265
|
+
- **提交点**:数据块先写、主 key 后写 —— 中途失败不会留下「半新半旧」的可读结果。
|
|
266
|
+
- **空值语义**:`loadChunked` 返回 `undefined` = 从没存过;返回 `''` = 存过空串。判断请用 `=== undefined`,**不要**用真值判断(`if (!v)` 会把空串当没存过 —— `BaseEngine.load` 就有这个坑)。
|
|
267
|
+
- **元数据歧义**:一个**内容恰好是** `{"_chunks":N}` 的字符串会被强制分块,避免读回时被误判成元数据。
|
|
268
|
+
- **分块损坏**(主 key 声明 N 块但块缺失)会**抛错**,而不是返回半截数据。
|
|
269
|
+
|
|
270
|
+
### 3.2 持久化 helper 在 `@sapdon/runtime`,不在 `@sapdon/core`
|
|
271
|
+
- `src/cli/build.js` 的 `rollupIgnores` 把 `@sapdon/core` 列为 external,**运行期脚本**里 import 它会在产物里留下无法解析的裸 `@sapdon/core`(Bedrock 不是这个模块的宿主)。
|
|
272
|
+
- `@sapdon/runtime`(`src/oc`)不在该列表里 → 会被**打包进**脚本包 → 运行期可用。
|
|
273
|
+
- **判断法则**:构建期(`main.ts` 生成 JSON)= `@sapdon/core`;运行期(`scripts/*`)= `@sapdon/runtime`。
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 4. 方块容器
|
|
278
|
+
|
|
279
|
+
> **一句话结论(2026-09 定型)**:`minecraft:inventory` 是**实体**组件,写在**方块** `components` 里
|
|
280
|
+
> 在任何版本上都不成立;方块侧的规范写法 `minecraft:block_entity.container` 在当前引擎版本上**也被拒**。
|
|
281
|
+
> ⇒ **当前唯一可用的方块容器走「方块 + 承载实体」**(`BlockAPI.createTileBlock(...)` 的 `inventory_size`)。
|
|
282
|
+
|
|
283
|
+
### 4.1 三条路线与它们的真实状态
|
|
284
|
+
|
|
285
|
+
| 路线 | 产物 | 引擎现状 | 框架 API |
|
|
286
|
+
|---|---|---|---|
|
|
287
|
+
| **实体(唯一可用)** | `<ns>:<id>_entity` 的行为文件里挂**实体**组件 `minecraft:inventory`(`inventory_size` / `container_type` / `can_be_siphoned_from`) | 可用 | ★ `BlockAPI.createTileBlock(id, cat, textures, { inventory_size, container_type, can_be_siphoned_from })`(新增,默认 27/`minecart_chest`/true);已有 `TileBlock` 也可 `tile.entity.behavior.addComponent(EntityComponent.setInventoryProperties({...}))` |
|
|
288
|
+
| 方块·规范 | 方块 `components` 的 `minecraft:block_entity: { container: { slot_count }, dynamic_properties }` | **被拒**:`-> minecraft:block_entity -> container: this component was found in the input, but is not present in the Schema`(format_version 1.26.30 / 1.26.40 报同样的错) | `BlockComponent.setBlockEntity(true, { container: { slot_count } })`(新增;`slot_count` 官方文档 `[1,54]`,**超限抛错**) |
|
|
289
|
+
| 方块·历史 | 方块 `components` 的 `minecraft:inventory`(**实体组件放错上下文**) | **必然被拒**:`-> components -> minecraft:inventory: … not present in the Schema` | `BlockComponent.setInventory(...)` —— **已标 @deprecated**,产物**逐字节不变**(不制造"升框架就构建失败"),但每次调用打一条 warn 指向上面两条路 |
|
|
290
|
+
|
|
291
|
+
**依据(官方文档,别再靠猜)**:
|
|
292
|
+
- 方块侧 `minecraft:block_entity`(含 `container.slot_count`,原文 "Value must be >= 1. Value must be <= 54."):
|
|
293
|
+
<https://learn.microsoft.com/en-us/minecraft/creator/reference/content/blockreference/examples/blockcomponents/minecraftblock_block_entity>
|
|
294
|
+
- 实体侧 `minecraft:inventory`(`inventory_size` 只写 "Number of slots the container has",**未给上限**;
|
|
295
|
+
`container_type` 取值 `horse` / `minecart_chest` / `chest_boat` / `minecart_hopper` / `inventory` / `container` / `hopper`):
|
|
296
|
+
<https://learn.microsoft.com/en-us/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_inventory>
|
|
297
|
+
|
|
298
|
+
**为什么 `setInventory` 选"标废弃 + 保持产物不变",而不是改成产出 `block_entity.container`**:
|
|
299
|
+
1. 改成 `block_entity.container` 后,若与用户自己的 `setBlockEntity(...)` 合并,会被 `combineComponents` 的
|
|
300
|
+
「后者覆盖前者」**静默吞掉容器**(同一个 JSON 键);今天两者是不同键,这个坑不存在。
|
|
301
|
+
2. 一旦按方块路线校验 `[1,54]`,`inventory_size: 56` 那类既有项目(FZ 机器需要 56 槽)
|
|
302
|
+
会从"静默无效"**直接变成构建失败** —— 这是明确要避免的。
|
|
303
|
+
3. 真正的错误(把实体组件当方块组件用)已由 warn 明确说出,并给出两条新路线;
|
|
304
|
+
要规范写法的人可以显式用 `setBlockEntity(true, { container: { slot_count } })`。
|
|
305
|
+
|
|
306
|
+
**`inventory_size` 上限**:官方文档**没给**(实体组件那页只写"槽位数")。框架只校验正整数,
|
|
307
|
+
**不要**把方块路线的 `[1,54]` 套到实体容器上。**⚠️ 待真机确认**:56 槽(FZ 机器所需)是否可用。
|
|
308
|
+
|
|
309
|
+
**自检守卫**(`BasicBlock.validate()`,由 `registry.submit()` → `runValidators()` 调用):
|
|
310
|
+
1. 方块 components 里出现 `minecraft:inventory` → warn("它是实体组件,必然被拒" + 两条替代路线)。
|
|
311
|
+
这条能兜住**手写裸 Map** 的写法(FZ 项目当初就是这么绕的)。
|
|
312
|
+
2. `minecraft:block_entity.container.slot_count` 超出 `[1,54]` → warn(兜住手写组件对象)。
|
|
313
|
+
|
|
314
|
+
### 4.2 `minecraft:inventory` / `minecraft:block_entity` 在 Bedrock Wiki 上**没有条目**(引用时别引错)
|
|
315
|
+
2026-09 核对:<https://wiki.bedrock.dev/blocks/block-components> 的
|
|
316
|
+
「List of Vanilla Components」共 36 条(Chest Obstruction → Transformation),**没有 Inventory**;
|
|
317
|
+
全页也搜不到 `minecraft:inventory` / `minecraft:block_entity`。
|
|
318
|
+
⇒ 引用这两者时**不要**标注该 wiki 页为出处;**改引 Microsoft Learn**(上面的两个链接,两页都真有条目)。
|
|
319
|
+
(组件本身是真实存在的 —— 框架生成的产物里有,`minecraft:block_entity` 也与 `TileBlock` 链路相关;
|
|
320
|
+
问题只是**该 wiki 页不覆盖它**。)
|
|
321
|
+
- 该页倒是**有** `minecraft:connection_rule`(`accepts_connections_from`: `all`/`only_fences`/`none` +
|
|
322
|
+
`enabled_directions`),与 `minecraft:connection` trait(提供 `minecraft:connection_*` 状态)配合,
|
|
323
|
+
可能是「让管道连接非同种方块」的正路 —— FZ 的管道/线缆(S6)值得先试这条,**但未验证**。
|
|
324
|
+
|
|
325
|
+
### 4.3 `setInventory` 的 JSDoc 曾指向**不存在**的守卫(2026-09 已修)
|
|
326
|
+
- 原文(旧 `blockComponent.js:688-689`):
|
|
327
|
+
> 若只写了 `setInventory` 而没有 `setBlockEntity`,构建时框架会打印 warn 提醒(见 `blockFactory.registerBlock`)。
|
|
328
|
+
- **指向是错的**:`src/core/factory/blockFactory.js` 里 `block_entity` 命中数 = **0**,`registerBlock` 没有该检查。
|
|
329
|
+
真正的守卫是 `src/core/block/basicBlock.js` 的 `BasicBlock.validate()`(`blockFactory.js:36` 与
|
|
330
|
+
`basicBlock.js` 的注释里现在都写明了这一点),由 `src/core/registry.ts:26` 的 `runValidators()`
|
|
331
|
+
在 `registry.submit()` 时调用 —— **不能**放在 `registerBlock`:`BlockAPI.createXxx()` 是「先注册、后 addComponent」,
|
|
332
|
+
那一刻组件还没挂上。
|
|
333
|
+
- 复核命令(负面断言,必须为 0):
|
|
334
|
+
`Select-String -Path src\core\factory\blockFactory.js -Pattern 'block_entity' -AllMatches | Measure-Object`
|
|
335
|
+
- 另外**旧 `setInventory` 自检的前提也错了**(它查"有 `minecraft:inventory` 但没 `block_entity`",
|
|
336
|
+
而 `minecraft:inventory` 根本不是方块组件)→ 已改成上面 4.1 的两条。
|
|
337
|
+
|
|
338
|
+
### 4.4 承载实体默认外观与方块**共面** ⇒ z-fighting 闪烁(2026-09-12,fz-sapdon 真机)
|
|
339
|
+
- **症状**:`createTileBlock` 的方块在游戏里贴图不停闪烁(两个面在抢深度)。容器功能本身正常。
|
|
340
|
+
- **根因**:框架给承载实体(`` `${identifier}_entity` ``)的客户端定义是
|
|
341
|
+
`geometry.cube`(满方块)+ `textures.default = textures_arr[0]`(`tileBlock.js` → `entity.js:19-21`),
|
|
342
|
+
而实体又 spawn 在**方块正中心** ⇒ 实体与方块是两个**完全共面**的立方体。
|
|
343
|
+
⚠️ `textures_arr[0]` 在方块侧是 terrain 短名、在实体侧必须是资源路径;项目侧通常把它补成
|
|
344
|
+
`textures/blocks/<短名>` —— 那正好就是方块贴图,于是闪烁必现。
|
|
345
|
+
- **做法**:**「方块可见 + 实体只当容器」这种模式(fz-sapdon 叫模式 A)必须给实体一张全透明贴图**
|
|
346
|
+
(16×16 alpha 全 0,`entity_alphatest` 会整张丢弃);「实体接管外观」那套反过来传方块贴图。
|
|
347
|
+
透明贴图放 `textures/entity/` 下,别放 `textures/blocks/` —— 后者会被构建自动注册进
|
|
348
|
+
`terrain_texture.json`(方块地图集),实体贴图按资源路径引用、不该占地图集。
|
|
349
|
+
- **别删实体的几何/材质/渲染控制器**:删了等于堵死「实体接管外观」那条路,静默无外观可用。
|
|
350
|
+
- 出处:fz-sapdon 真机日志(实体 spawn 坐标 = 方块中心)+ 该项目 README §S3b「真机修复」小节。
|
|
351
|
+
|
|
352
|
+
### 4.5 自定义容器界面:门控键 = `UISystem.name`(2026-09-12,fz-sapdon S3e 实测)
|
|
353
|
+
- **机制**(可用部分的正确姿势):`ChestUISystem.registerContainerUI(key, rootPanel)` 会在
|
|
354
|
+
`chest:chest_screen` 里插一条 `requires: ($new_container_title = '<key>')` 的门控,命中就把原版小箱子界面的
|
|
355
|
+
`$screen_content` / `$root_panel` 换成你的根面板。
|
|
356
|
+
- ★ **门控键 = `UISystem` 的 `name`(identifier 冒号后半段),不是 `setTitle()` 的值**
|
|
357
|
+
(`chest.ts:5` 引入 `$container_title`、`:16` 字符串相等、`containerUISystem.ts` 的 `#register()` 传
|
|
358
|
+
`this.system.name`、`system.ts:19` 是 `identifier.split(':')[1]`)。`setTitle()` 只写面板自己的标题文本。
|
|
359
|
+
- ★ **实体容器的标题取自「实体名字」**(原版同路:`ui/horse_screen.json:35`)⇒ 用实体容器时要把键写进
|
|
360
|
+
**承载实体的 nameTag**(写完**立即回读**;写错的表现是**静默退回原版箱子界面、零报错**)。
|
|
361
|
+
- 代价:nameTag 若被渲染,机器上方会出现浮空名字(fz-sapdon 用开关 + 候选键兜底处理)。
|
|
362
|
+
- **追加硬约束**:该键同时是 `ui/<name>.json` 的**文件名** ⇒ 见 §4.10。
|
|
363
|
+
- **坑 1(2026-09-12 已修):`ContainerUISystem.addElementToMain(el)` 加进去的控件不会显示** —— 详见 §4.8。
|
|
364
|
+
- **坑 2(2026-09-12 已改善):版面写死**,表达不了「背景图 + 绝对像素版面」(FZ 那种 256×128 机器面板)。
|
|
365
|
+
现在 `ContainerUISystem` 的槽位与网格是**绝对像素定位**(`setPanel` / `setGridOrigin` / `addSlot`),
|
|
366
|
+
不再只能靠 `ChestUISystem.registerContainerUI` 那一层自己写根面板;
|
|
367
|
+
fz-sapdon 的 S3e(`src/ui/recycler_panel.ts` + `fz_recycler_geometry.ts`)仍是"完全手写根面板"的现成范例。
|
|
368
|
+
- **坑 3:换掉 `$screen_content` 时玩家背包也一起没了** —— 自定义根面板要自己把背包区摆回来,
|
|
369
|
+
且根面板尺寸必须覆盖原版(否则 100% 宽的背包区与自定义区重叠);FZ 面板组还必须排在
|
|
370
|
+
`common_panel` **之后**(层序错 = 原版灰底盖住你的背景图,表现为"面板在、图没了")。
|
|
371
|
+
框架现在把背包区固定为 `inventory_panel`(`bottom_left` 锚、`100%×50%`、layer 2)。
|
|
372
|
+
- 出处:fz-sapdon `README.md` §S3e(含槽位换算、门控 4 条候选键、11 条真机清单)。
|
|
373
|
+
|
|
374
|
+
### 4.6 ★ JSON UI 里「控件不可交互」的属性名是 `enabled`,**不是** `enable`(2026-09-12)
|
|
375
|
+
- **症状**:用输出槽语义声明的格位,真机上照样能往里放东西 —— 标志位**从未生效**;产物里那个键叫
|
|
376
|
+
`"enable": false`,是引擎不认识的拼写。
|
|
377
|
+
- **根因**:`containerUISystem.ts` 的 `addGridItem` 写成 `addProp('enable', …)`。
|
|
378
|
+
- **计数证据(可复现的负面断言)**:
|
|
379
|
+
| 来源 | `"enabled"` | `"enable"` |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| 原版 UI 全树 `resource_pack/ui/*.json`(bedrock-samples 1.21.130.26 preview) | **26** | **0** |
|
|
382
|
+
| `examples/mob_chest/res/ui/cooking_pot.json`(108 KB 手写容器面板) | **3** | **0** |
|
|
383
|
+
- `cooking_pot.json:255` 的 `bot_left@chest.chest_grid_item` 明确写了 `"enabled": false`;
|
|
384
|
+
`:318` / `:345` 两个进度槽写 `"enabled": true` —— 原版真名在同一份真实界面里被用过三次。
|
|
385
|
+
- 对照件 `examples/mob_chest/res/ui/slot_test.json` 是**为这次判定专门写的手写面板**:
|
|
386
|
+
槽 0-3 写 `"enabled": true`、槽 4 写 `"enabled": false`、槽 5 写 `"enable": false`,
|
|
387
|
+
进一次游戏即可看出只有槽 4 与其它槽行为不同。
|
|
388
|
+
- **规避**:一律写 `enabled`。框架侧唯一出口是 `containerLayout.ts` 的 `SlotSpec.enabled` /
|
|
389
|
+
`ResolvedSlot.enabled`:`output` / `display` **缺省**写 `false`、`input` 不写该键(继承原版默认 `true`),
|
|
390
|
+
**显式 `enabled` 一律优先于这个缺省**(2026-09-12 补,见 §4.14)。
|
|
391
|
+
- **后续结论(2026-09-12 真机,已定案)**:该标志位**确实生效**,但它的语义是
|
|
392
|
+
**整体禁用这一格** —— 既拦「放进去」、**也拦「取出来」**。
|
|
393
|
+
⇒ 产物格必须显式写 `enabled: true`,否则**产物拿不到手**(完整证据与修法见 §4.14)。
|
|
394
|
+
- **判据**:`node tests/container-ui-output.test.mjs` —— output / display 内层控件**缺省** `enabled === false`;
|
|
395
|
+
**整个产物递归不存在 `enable` 这个键**(对象键遍历 + 文本层 `/"enable"\s*:/` 双查);
|
|
396
|
+
`node tests/container-layout.test.mjs` —— `resolveSlot` 的覆盖规则(显式值优先)。
|
|
397
|
+
|
|
398
|
+
### 4.7 `ContainerUISystem.setItemMatrix` 为何删除(2026-09-12)
|
|
399
|
+
三个独立缺陷叠在同一个函数里,**任一都不能在不改语义的前提下修好**,故整体删除:
|
|
400
|
+
1. `:153` 把 `indexOf` 当布尔用 —— `if (this.output_grids.indexOf(v))`:`-1` 是**真值**、`0` 是**假值**。
|
|
401
|
+
于是「没声明输出槽」时**所有**格位都被当成输出槽;而第一个输出槽(索引 0)反倒走"普通输入槽"分支。
|
|
402
|
+
2. `:135-150` 把「矩阵格位」与「槽序号」混算 —— 偏移表按 `n` 建(`offset_marix[value - 1]`),
|
|
403
|
+
矩阵值只是标记而非槽号,值 1 / 2 / 3 会算出**同一个** `[-36, 18]`。
|
|
404
|
+
3. `:134` 写死 `this.setGridDimension([1, n])`,丢掉矩阵真实形状(该行注释自己写的是 `n*n列`)。
|
|
405
|
+
- **替代**:`addSlot({ slot, pos, kind })` —— 槽号定槽位、像素坐标定版面,换算集中在 `containerLayout.ts`。
|
|
406
|
+
- **判据**:`node tests/container-ui-output.test.mjs` 断言 `typeof ui.setItemMatrix === 'undefined'`;
|
|
407
|
+
旧调用点若仍在用会在**编译期**报错(不会静默走错分支)。
|
|
408
|
+
|
|
409
|
+
### 4.8 `addElementToMain` 曾经加进去的控件**永远不显示**(2026-09-12 修复)
|
|
410
|
+
- **症状**:`ContainerUISystem.addElementToMain(el)` 返回 `this`、不抛错、产物里也"看不出问题",
|
|
411
|
+
但界面上那个控件**不存在**。
|
|
412
|
+
- **根因**:`#updateSystem()` 组装的 `container_root_panel` 只含 `common_panel` +
|
|
413
|
+
`inventory_selected_icon_button` + 一个 `container_panel`(标题 10% / 网格 40% / 背包 50% 三段堆叠),
|
|
414
|
+
**从来没有把 `this.main_panel` 挂进根面板** —— 控件被加进一个游离 Panel。同类症状见 §4.5 坑 1。
|
|
415
|
+
- **修法**:`main_panel` 现在是根面板的正式子控件(`top_left` 锚、size = 面板尺寸、layer 4),
|
|
416
|
+
`addControl(el, pos?)` 与 `addElementToMain(el)` 都真的落进产物。
|
|
417
|
+
⚠️ 别用 `main_panel.setControl(new Control())` 去改层级 —— 那会换掉 Control 对象、
|
|
418
|
+
把已挂上的 `controls` 丢掉(框架内部因此只就地 `control.setLayer()`)。
|
|
419
|
+
- **判据**:`node tests/container-ui-output.test.mjs` 的「addControl(el, pos) 真的挂进主面板并带定位」
|
|
420
|
+
与「addElementToMain 是 addControl 的别名,同样生效」两条 —— 直接查
|
|
421
|
+
`container_root_panel.controls[*].main_panel.controls`。
|
|
422
|
+
|
|
423
|
+
### 4.9 ★ 容器版面坐标空间:校准点只有一处(2026-09-12;格位 18 与非原版 20 均已真机实测)
|
|
424
|
+
- **现状**:框架把 `pos`(面板左上角原点的像素)换算成格位 `offset`,前提有三条:
|
|
425
|
+
1. 网格锚点在左上角;
|
|
426
|
+
2. 格位基座 = 网格原点 + 该格在单行网格里的序号 × **网格统一格位尺寸**;
|
|
427
|
+
3. 统一格位尺寸 = `setSlotDefaults({ cellSize })`(缺省 = 标定表 `cellSize`);逐槽 `cellSize` 只是视觉尺寸。
|
|
428
|
+
- **`offset` 相对的是格位模板的锚点**:`chest.chest_grid_item` → `common.container_item`
|
|
429
|
+
(`ui_common.json:4770`)的 `anchor_from` / `anchor_to` **默认是 `center`**,框架会显式覆写成标定表的 `anchor`。
|
|
430
|
+
- **规避(校准只改一行)**:全部换算集中在 `src/core/ui/systems/containerLayout.ts` 的 `SLOT_CALIBRATION`
|
|
431
|
+
(`anchor` / `originPadding` / `cellSize` / `columns` / `defaultGridOrigin`)。改 `anchor` 会**同时**改换算与写进产物的
|
|
432
|
+
`anchor_from`/`anchor_to`(内层控件的锚点由 `anchorProps()` 取,不各写一份,避免两处不一致)。
|
|
433
|
+
`originPadding` 是整体平移用的最后手段;`defaultGridOrigin` 是未调 `setGridOrigin` 时的网格原点
|
|
434
|
+
(默认 `[0, 24]`,给顶部标题让开一行)。
|
|
435
|
+
- **真机实测(2026-09-12,`examples/mob_chest` 的 `calib_test` 面板进游戏,GUI scale 3,截图逐像素量取)**:
|
|
436
|
+
探针面板 = `setPanel({ size:[180,166] })` / `setGridOrigin([8,40])` / `setSlotDefaults({ cellSize:[18,18] })`,
|
|
437
|
+
4 槽 `pos` = `[8,40]` / `[44,40]` / `[8,76]` / `[44,76]`。量得:
|
|
438
|
+
- 面板原点落在截图 (72, 75) px,缩放 **3.0 px/UI px**(由「渲染行距 108 px ÷ 声明 36」定出,两轴同尺度);
|
|
439
|
+
- **四槽渲染位置与声明 `pos` 的残差均为 0.00 UI px**:两列各自解出同一 x 原点、两行各自解出同一 y 原点;
|
|
440
|
+
- 标题(`offset [0,0]` + `100%` 宽 + `center` 对齐,走 label / `main_panel` 通路)**独立**解出原点 x = 71.5,
|
|
441
|
+
与网格给出的 72 相差 0.5 px ⇒ 两条互不相干的代码路径互证,说明这不是拟合出来的巧合;
|
|
442
|
+
- **统一格位尺寸被三路独立解出 = 18.00**:`slot1`(行 1,`offset.y = −18`)落回声明的 40 ⇒ cellH = 18;
|
|
443
|
+
`slot2`(行 2)落回 76 ⇒ 2·cellH = 36;`slot3`(行 3,`offset.y = −18`)落回 76 ⇒ 3·cellH = 54
|
|
444
|
+
⇒ **§4.11 的「网格几何只认统一格位」在真机成立**;
|
|
445
|
+
- **`anchor = top_left` 成立**:若真实锚点是 `center`,每个槽会整体偏半格 = 9 UI px ≈ 27 屏幕 px,未见;
|
|
446
|
+
- 槽盒实测 54×54 px = **18.00 UI px** = 声明的 `cellSize`。
|
|
447
|
+
⇒ **结论:`pos` → `offset` → 渲染 这条链是逐像素正确的**(本条是 `cellSize` = 18 = 模板原生尺寸;
|
|
448
|
+
非原版 20 的实测见下面「真机实测 ②」)。
|
|
449
|
+
- **真机实测 ②(2026-09-12,用户随后提供的对照截图):★ 非原版统一格位确实被引擎采纳。**
|
|
450
|
+
这一次量的是 `examples/mob_chest` 的**系统 A**(`sapdon_furnace`):`setGridOrigin([8,40])`、
|
|
451
|
+
`setSlotDefaults({ cellSize: [20,20] })`、4 槽 `pos` 全为 `[8,40] / [30,40] / [52,40] / [84,40]`。
|
|
452
|
+
因为 4 槽同在 `pos.y = 40` 而分属网格第 0/1/2/3 行,框架算出的 `offset.y` 依次是 `0 / −20 / −40 / −60`,
|
|
453
|
+
于是渲染 y = `40 + k·(引擎格高 − 20)`:
|
|
454
|
+
- 实测四槽(含第 3 槽那个 `cellSize: [36,10]` 的宽条)**全部落在同一个 y**,间距完全是声明的
|
|
455
|
+
`22 / 22 / 32`(截图 66 / 66 px ÷ 3.0)⇒ `引擎格高 − 20 = 0` 对 k = 1,2,3 同时成立
|
|
456
|
+
⇒ **引擎格高 = 20.00,正是 `setSlotDefaults` 声明的值**(若引擎只按模板原生 18 排版,
|
|
457
|
+
k=1/2/3 会分别上移 2/4/6 UI px = 6/12/18 屏幕 px,肉眼可见)。
|
|
458
|
+
- 同时,第 3 槽的视觉尺寸是 `36×10`(≠ 18×18 的格位),它**没有**因此偏离声明的 `pos`
|
|
459
|
+
⇒ 逐槽 `cellSize` 确实只是视觉尺寸、**不移动本槽的格位基座**。槽盒实测 60×60 px(= 20 UI)
|
|
460
|
+
与 108×30 px(= 36×10 UI)都与声明逐像素相符。
|
|
461
|
+
⇒ **`setSlotDefaults({ cellSize })` 是有效的几何接口,`SLOT_CALIBRATION.cellSize` 只是缺省值。**
|
|
462
|
+
- **仍未验证**:
|
|
463
|
+
1. **逐槽 `cellSize` 是否会影响它之后各行的格高** —— 实测只证明「不影响自己的基座」;
|
|
464
|
+
要证明「不影响后续行」需要在一个怪尺寸槽**之后再放一行**(现有面板的怪尺寸槽都在最后一行)。
|
|
465
|
+
2. 其它 GUI 缩放下的复现(两次实测都是 scale 3)。
|
|
466
|
+
3. ~~`enabled: false` 能否真拦下「往槽里放东西」~~ → **已定案**(2026-09-12 真机):它拦得住,
|
|
467
|
+
而且是**双向一起拦**(连「取出来」也拦)⇒ 见 §4.14。
|
|
468
|
+
- **附带量取(2026-09-12,同一批截图,GUI scale 3)**:原版熔炉界面的排布换算成面板内 UI 坐标是
|
|
469
|
+
—— 两格输入同列上下叠放、**间距 38**(18×18),产物格在右侧 **+58**(26×26,比输入大),
|
|
470
|
+
且**垂直居中对齐于两输入的跨度**;火焰在输入列正中(13×13),箭头在输入与产物之间(22×15),
|
|
471
|
+
两者垂直中心与两输入跨度中心重合。`examples/mob_chest` 的系统 A 即按这套比例排布。
|
|
472
|
+
- 火焰/箭头**不是**从 GUI 大图里切的:原版 `furnace_screen.json` 用的是两张独立贴图
|
|
473
|
+
`textures/ui/flame_empty_image`(13×13)与 `textures/ui/arrow_inactive`(22×15),
|
|
474
|
+
直接 `setTexture` 引用即可,**不需要 uv/uv_size 切片**。
|
|
475
|
+
- ⚠️ 原版的进度显示靠**另一对**贴图 + 绑定裁剪:`furnace.flame_full_image`
|
|
476
|
+
(`textures/ui/flame_full_image`,`clip_direction: down`) 与
|
|
477
|
+
`furnace.furnace_arrow_full_image` (`textures/ui/arrow_active`,`clip_direction: left`),
|
|
478
|
+
它们的 `#clip_ratio` 来自 `bindings` 的 `#furnace_flame_ratio` / `#furnace_arrow_ratio`。
|
|
479
|
+
这两个绑定由**熔炉界面**提供;把面板挂在别的容器界面(如 `chest_screen`)上时取不到,
|
|
480
|
+
`clip_ratio` 会留在默认值 ⇒ **只能放静态的「空态」贴图,进度要用别的手段**
|
|
481
|
+
(例如用一个 `display` 槽、由脚本每 tick 换物品;见 §4.6 的 `kind` 语义)。
|
|
482
|
+
- **实测 ① 暴露的版面坑:自定义内容必须压在背包区之上(H = 166 时约 `y < 86`)**。
|
|
483
|
+
面板下半区是原版背包(`inventory_panel`:`bottom_left` + `100%×50%`),其原版内容高约 88 UI px 且贴着底边,
|
|
484
|
+
⇒ **内容顶明显高于半区上边界**(H = 166 时半区从 y = 83 起,而背包槽首行顶实测在 **y ≈ 86**、
|
|
485
|
+
其标签文字更探到 **y ≈ 76**,即内容**溢出**了半区)。
|
|
486
|
+
⚠️ 把槽声明在 `y = 76` 会与玩家背包首行**纵向重叠 7.7 UI px、横向仅差 1 UI px**(实测)——
|
|
487
|
+
表现为你的槽正好盖在背包格上、背包标签的字从两槽缝隙里漏出来。H = 166 的可用安全区只有约 `y ∈ [24, 86)`,
|
|
488
|
+
**只放得下 3 行 20px 格位**;要放更多行必须加大 `setPanel({ size })`(`H` 加大后安全区如何变化尚无实测,
|
|
489
|
+
只有一个 H = 166 的数据点,别照公式外推)。
|
|
490
|
+
- **判据**:`node tests/container-layout.test.mjs`(锁住当前假设下的换算值);
|
|
491
|
+
`node tests/container-ui-output.test.mjs`(锁住产物里的 `offset` / `anchor_*`)。
|
|
492
|
+
|
|
493
|
+
### 4.10 ★ 门控键同时是 `ui/<name>.json` 的**文件名**:带点的键 = UI 静默不加载(2026-09-12)
|
|
494
|
+
- **机制**:`UISystem` 的 `name` 有两个身份 —— `chest:chest_screen` 里 gate 的比较值
|
|
495
|
+
(`chest.ts:16` 字符串相等),以及 UI 文件名(`uiSystemRegistry.ts:12` 的 `path + name + '.json'`)。
|
|
496
|
+
- **坑**:`GRegistry.register` 会把**文件名**里的非法字符换成 `_`(`registry.ts:44` 的 `safeName`),
|
|
497
|
+
而 `_ui_defs.json` 里记的是**未替换**的 `ui/<name>.json`(`uiSystemRegistry.ts:14` 用的是原始
|
|
498
|
+
`ui_system.name`)⇒ 清单与磁盘文件不一致,UI **静默不加载、零报错**。
|
|
499
|
+
写文件那一侧用的一直是替换后的名字(`cli/load.js:249` 的 `` `${name}.json` ``)。
|
|
500
|
+
- **规避**:门控键只允许 `A-Z a-z 0-9 _ -`。`containerLayout.ts` 的 `checkUIName()` 在
|
|
501
|
+
`ContainerUISystem` 构造时打一条 warn(**只 warn 不抛**,避免直接打断既有项目的构建)。
|
|
502
|
+
- **判据**:`node tests/container-ui-output.test.mjs` 的「非法门控键只 warn 不抛错」。
|
|
503
|
+
|
|
504
|
+
### 4.11 逐槽 `cellSize` 曾把格位基座算歪 —— 网格几何必须取**统一值**(2026-09-12)
|
|
505
|
+
- **症状**:同一个面板里混用不同 `cellSize`(例如普通 18×18 槽 + 一个 36×10 的长条进度槽)时,
|
|
506
|
+
某些槽的实际渲染位置与声明的 `pos` 差 `(统一格高 − 本槽格高) × 行号` 像素。
|
|
507
|
+
- **原因**:两套算法各取一个尺寸 —— 网格尺寸取 `max(各槽 cellSize)`(`containerUISystem.ts` 的 `#buildGrid`),
|
|
508
|
+
而格位基座取**该槽自己的** `cellSize` 递推(`containerLayout.ts` 的 `cellBase`)。
|
|
509
|
+
引擎的网格格位是**均匀**的(只有一个格位尺寸),所以混合尺寸下两者必然矛盾。
|
|
510
|
+
量到的例子:`gridOrigin [8,34]` + 4 槽、第 4 槽 `cellSize [36,10]`、`pos [10,40]`
|
|
511
|
+
⇒ 旧算法给出 offset `[2,-24]`(隐含基座 y=64),而网格真实格高 20 ⇒ 基座应是 94(偏 30px)。
|
|
512
|
+
- **规避(现行语义)**:几何只认一处 —— `setSlotDefaults({ cellSize })`(缺省 = 标定表 `cellSize`),
|
|
513
|
+
网格尺寸与基座换算都用它;逐槽 `cellSize` **只**写内层控件的 `$cell_image_size` / `size`,
|
|
514
|
+
允许溢出格位(原版槽位模板不裁剪,长条进度槽就是这么画的)。
|
|
515
|
+
- **判据**:`node tests/container-layout.test.mjs` 的「逐槽视觉尺寸不参与基座」与
|
|
516
|
+
`node tests/container-ui-output.test.mjs` 的「网格尺寸也只用统一格位」。
|
|
517
|
+
|
|
518
|
+
### 4.12 ★ 自定义容器面板**能/不能**拿到哪些格子数据(2026-09-12,原版包 bedrock-samples 1.21.130.26 通读)
|
|
519
|
+
|
|
520
|
+
问法通常是「能不能读容器某个格子的内容,做一条由真实数据驱动的进度条」。答案是**分两层**:
|
|
521
|
+
|
|
522
|
+
- **格子内部 —— 能**。`common.container_item` 的子控件声明了一批 `binding_type: "collection"` +
|
|
523
|
+
`binding_collection_name: "$item_collection_name"` 的每格绑定(`ui_common.json`):
|
|
524
|
+
堆叠数量 `#inventory_stack_count`(:3615,原始名 `#item_stack_count`)、物品 id `#item_id_aux`(:3796)、
|
|
525
|
+
整份 stack 数据 `#item_renderer_data`(:3758)、耐久 `#item_durability_visible|total_amount|current_amount`
|
|
526
|
+
(:3644/3650/3656)、容量 `#item_storage_*`(:3704/3710/3716)。
|
|
527
|
+
能驱动的目标属性以 `#visible` 最成熟;`#texture` 有确证先例(`inventory_screen.json:1578-1585`
|
|
528
|
+
的 `#container_item_background_texture` → `#texture`)。
|
|
529
|
+
- **格子外部(贯穿面板的进度条)—— 不能**。四条互相独立的证据:
|
|
530
|
+
1. 全 `ui\` 里形如 `#*_ratio` 的**供给名只有 7 个**(furnace_arrow / furnace_flame / brewing_bubbles /
|
|
531
|
+
brewing_arrow / brewing_fuel / bundle_weight_bar / progressive_select_bar),**无一属于 chest/container**;
|
|
532
|
+
2. `#furnace_arrow_ratio` / `#furnace_flame_ratio` 在整个原版包 grep **只有 2 命中,且两处都是消费端**
|
|
533
|
+
(`furnace_screen.json:42`、`:62`)⇒ 引擎按界面硬编码,资源包只能消费、**不能自己提供**;
|
|
534
|
+
3. `chest_screen.json` 全文**没有任何 `bindings` 声明**,`container_items` 上不存在 `#progress_percentage`;
|
|
535
|
+
4. **`#clip_ratio` 不接受算术**,且 `binding_name_override: "#size"` 在全包 **0 命中**
|
|
536
|
+
⇒ 就算读到数量也换算不成长度。
|
|
537
|
+
格子外唯一能读的是**光标上那一格**(`#inventory_selected_item` / `#inventory_selected_item_stack_count`,
|
|
538
|
+
全局绑定、无 collection 限定,`chest_screen.json:117/162` 已挂载该按钮)与集合级总数 `#collection_total_items`。
|
|
539
|
+
- **★ 替代路线:借引擎自带的耐久条当进度条(零新贴图)**。`common.container_item` **自身就内联了**
|
|
540
|
+
`durability_bar@common.durability_bar`(`ui_common.json:4838`)与 `storage_bar@common.storage_bar`(:4843),
|
|
541
|
+
唯一门控是 `ignored: "(not $durability_bar_required)"`(:3629),而父级默认 **true**(:4778-4779)
|
|
542
|
+
⇒ 容器格子**默认就带**这两条(不是 HUD 专属;显式关闭的先例见 `inventory_screen.json:1951-1952`)。
|
|
543
|
+
条的比例由引擎按每格 `#item_durability_*` 算好;`$durability_bar_size` / `$durability_bar_offset`
|
|
544
|
+
**可被外部覆盖**(:3633-3634;原版口袋版就覆盖了它们 :4895-4896)⇒ 框架的 `addSlot({ vars })`
|
|
545
|
+
写的正是这一类 `$x|default`。于是**脚本往槽里写一个「剩余耐久 = 进度」的可损耗物品,条就会自己动**,
|
|
546
|
+
不需要任何进度条贴图。示例实现:`examples/mob_chest/main.mjs`(进度物品 + `vars`)与
|
|
547
|
+
`examples/mob_chest/scripts/progress_bar.js`(`system.runInterval` 驱动)。
|
|
548
|
+
- **真机已确认它会画出来**(2026-09-12,见下面实测 ②);仍未知的只有:`#item_durability_visible`
|
|
549
|
+
的判定条件(原版包内**查不到**,纯引擎内部计算)、`progress_bar_renderer` 怎么把 current/total
|
|
550
|
+
合成宽度(引擎渲染器、包内无定义)。
|
|
551
|
+
- 附:`$item_renderer_size` 默认 `[16,16]`(:4782),归零即可把物品图标藏掉、只留条。
|
|
552
|
+
- **★ 真机实测 ②(2026-09-12):条确实会画出来,但尺寸改不动 —— `$x|default` 覆盖不到
|
|
553
|
+
「后代自己声明了 `|default`」的同名变量。** 现象:框架用
|
|
554
|
+
`addSlot({ vars: { durability_bar_size: [36,4], durability_bar_offset: [0,3] } })`(写成 `$durability_bar_size|default`)
|
|
555
|
+
之后,真机截图里条**仍按原版默认 12×1** 画在格子底部中央(量得条宽 12.33 UI px、
|
|
556
|
+
位置 = 格心 +5,正是默认 `$durability_bar_offset` `[0,5]`)。
|
|
557
|
+
原因:这两个变量是**后代控件** `common.durability_bar` **自己**用 `|default` 声明的(`ui_common.json:3633-3634`),
|
|
558
|
+
后代自身的 `|default` 胜过祖先实例层的 `|default`。旁证:`$cell_image_size|default` 覆盖得动,是因为消费它的
|
|
559
|
+
`common.cell_image` 没自己声明同名 `|default`。原版口袋版之所以改得动,是它**裸写**这两个变量
|
|
560
|
+
(**不带** `|default`,`ui_common.json:4895-4896`)—— 而**框架的 `vars` 只会写 `$x|default`**
|
|
561
|
+
(`containerUISystem.ts:466-469`)。
|
|
562
|
+
- **判据(一个变量能不能被 `vars` 覆盖)**:看它由谁声明。由**被实例化的那个控件自己**声明
|
|
563
|
+
(如 `$durability_bar_required` 在 `container_item:4778`)⇒ 实例层能覆盖;
|
|
564
|
+
由**更深的后代**自己声明 ⇒ 覆盖不到,只能改用下面的注入法。
|
|
565
|
+
- **★ 注入自定控件:`$cell_overlay_ref` / `$background_images`(都在 `item_cell` 内,保留每格 collection 上下文)**。
|
|
566
|
+
这两个变量都是 `container_item` **自己**声明的(`:4775` / `:4784`,后者配 `$background_image_control_name` `:4785`),
|
|
567
|
+
故实例层可覆盖。`$cell_overlay_ref` 默认是空壳 `common.cell_overlay`(`:3315` 只有 `ignored: true`),
|
|
568
|
+
用在 `item_cell` 的 `item_cell_overlay_ref@$cell_overlay_ref`(`:4851`)。
|
|
569
|
+
⇒ 项目可注册一个自定控件(`type: "custom"` + `renderer: "progress_bar_renderer"` + 自己的三条 `bindings`:
|
|
570
|
+
`binding_type: "collection"` / `binding_collection_name: "container_items"`,照抄 `ui_common.json:3642-3661`),
|
|
571
|
+
再用 `vars: { durability_bar_required: false, cell_overlay_ref: "<ns>.<控件名>" }` 把自带那条关掉、换成自己这条
|
|
572
|
+
—— **尺寸与绑定都自己说了算**。`examples/mob_chest/main.mjs` 即此法(先铺满整格的色块条,后改为箭头,见下条)。
|
|
573
|
+
- ★ **框架已把这一整套封成 `ContainerUISystem.addProgressSlot()`**(参数语义见接口 JSDoc、
|
|
574
|
+
用法见 `doc/dev/ui-architecture.md` §4.3),项目不必再手写 overlay 控件、三条绑定与那两个变量;
|
|
575
|
+
判据 `node tests/container-ui-output.test.mjs` 的第 19–22 条。
|
|
576
|
+
- **★ `progress_bar_renderer` 只能画色块、给不了贴图 ⇒ 想要「原版箭头」那种形状必须自己裁。**
|
|
577
|
+
该渲染器的全部可用属性只有 `size` / `offset` / `property_bag`(`is_durability` / `is_storage_bar` /
|
|
578
|
+
`round_value` / `primary_color` / `full_storage_color`)与 `primary_color` / `secondary_color`
|
|
579
|
+
(见 `ui_common.json:3631-3641`、`toast_screen.json:346-354`)—— **没有任何 texture 属性**。
|
|
580
|
+
原版箭头是**两张图 + 裁切**:`arrow_inactive`(打底)与 `arrow_active`
|
|
581
|
+
(`clip_direction: "left"`,`#clip_ratio` ← `#furnace_arrow_ratio`,`furnace_screen.json:30-46`)。
|
|
582
|
+
本面板拿不到 `#furnace_arrow_ratio`,**比例只能自己算**:把每格的
|
|
583
|
+
`#item_durability_current_amount` / `#item_durability_total_amount` 两条 collection 绑定
|
|
584
|
+
(**不带 override**,让名字进入该控件的属性作用域)挂在同一个控件上,再加一条 view 绑定
|
|
585
|
+
`source_property_name: "((#item_durability_total_amount - #item_durability_current_amount) / #item_durability_total_amount)"`
|
|
586
|
+
→ `target_property_name: "#clip_ratio"`。**Molang 算术写在 view 绑定里是框架自己用过的**:
|
|
587
|
+
`src/core/ui/systems/hud/hud.ts:35` 就是 `"(not (%.7s * #hud_title_text_string = 'PREFIX'))"`。
|
|
588
|
+
- ⚠️ **真机已验证(2026-09-12):Molang 表达式读得到 collection 绑定** —— 箭头随进度动起来了(先前只在框架的 HUD 里见过它读 **global** 绑定)。
|
|
589
|
+
兜底设计:把静态的 `arrow_inactive` 垫在下面,即使裁切不生效也还看得见一支箭头。
|
|
590
|
+
- **★ 真机已验证:`#item_durability_current_amount` 是「已损耗量」,不是「剩余量」。**
|
|
591
|
+
判据是**方向**:按 `current / total` 写箭头会**越走越短**,正确写法是取反 `((total - current) / total)`。
|
|
592
|
+
引擎自带的 `durability_bar` 不取反也不反,是因为它的 `property_bag` 带了 `is_durability: true`
|
|
593
|
+
(`ui_common.json:3637-3641`),方向由渲染器内部处理。
|
|
594
|
+
- **`clip_direction: "left"` 的语义 = 显示左侧 `ratio` 那一部分**(不是裁掉左侧)。佐证:XP 条
|
|
595
|
+
`full_progress_bar`(`hud_screen.json:510-522`)用 `clip_direction: "left"` + `#exp_progress`,
|
|
596
|
+
而 XP 条是从左往右长的。同理 right / up / down 各显示对应那一侧:原版火焰
|
|
597
|
+
`flame_full_image` 用 `down`(从下往上烧),`examples/mob_chest` 的燃烧槽用的就是它。
|
|
598
|
+
- 附带:想让格子**没有浅灰底**,可把 `$background_images`(`container_item` 自己声明,`:4784`)
|
|
599
|
+
指向一个 0×0 的空面板。
|
|
600
|
+
- **框架侧硬约束**:`ContainerUISystem` 把模板硬编码成 `chest.chest_grid_item`
|
|
601
|
+
(`containerUISystem.ts:422`),槽位内层控件内部构造,对外只给
|
|
602
|
+
`cellSize / background / itemRenderer / vars` ⇒ **今天无法给槽位挂 bindings**;
|
|
603
|
+
`UIElement` 自己有 `dataBinding.addDataBinding()`(`dataBinding.ts:15`),但容器 API 没开口子。
|
|
604
|
+
上面那条「借耐久条」的路线之所以可行,正是因为它**不需要自定义绑定**(模板自带)。
|
|
605
|
+
- **出处**:`examples/mob_chest` 的进度槽设计(2026-09-12),三路只读调研 + 原版包逐行核对;
|
|
606
|
+
相关:§4.6(`enabled`)、§4.9(坐标/版面)。
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
### 4.13 ★ 格位落点跟 `controls` **数组顺序**走,`grid_position` 不参与定位(2026-09-12,fz-sapdon 真机)
|
|
611
|
+
|
|
612
|
+
- **症状**:容器面板里几个槽**整体错位**,但**产物完全正确** —— `offset` 逐条与声明 `pos` 吻合、
|
|
613
|
+
`grid_dimensions` / `grids.size` / `grid_position` 全对;错的只有渲染位置,而且**只有 y 错、x 一直对**
|
|
614
|
+
(实测 4 个槽的 x 全部落在声明值上)。
|
|
615
|
+
- **根因**:引擎把 grid item **依次铺进格位**,落点 = 网格原点 +
|
|
616
|
+
**该 item 在 `controls` 数组里的序号** × 统一格位尺寸 + 该 item 的 `offset`;
|
|
617
|
+
**`grid_position` 字段不参与定位**。框架此前按**声明顺序**发布格位 ⇒ 项目只要不是
|
|
618
|
+
「按槽号顺序声明」(fz-sapdon 就是**先声明进度槽、后声明输出槽**),整块版面就会错开。
|
|
619
|
+
- **判据(4/4 吻合)**:fz-sapdon 回收机 4 槽,产物数组序 0/1/2/3 依次是
|
|
620
|
+
输入(slot 0) / 箭头(slot 2) / 能量(slot 3) / 输出(slot 1),真机实测渲染 y ≈ `29 / 12 / -14 / 62`:
|
|
621
|
+
- 按**数组序**算 ⇒ `31 / 14 / -8 / 63` —— **全中**;
|
|
622
|
+
- 按 **`grid_position`** 算 ⇒ `31 / 32 / 10 / 27` —— 三个错。
|
|
623
|
+
(能量条那一格因此越出面板顶边 14 UI px —— 截图里那根红柱子就是它。)
|
|
624
|
+
- **规避(框架已修)**:`containerUISystem.#buildGrid()` 现在**先按 `grid_position` 行优先排序**
|
|
625
|
+
再发布 ⇒ 「`pos` = 渲染位置」对任何声明顺序都成立。
|
|
626
|
+
`examples/mob_chest` 一直没暴露这个问题,**纯属它的声明顺序恰好等于格位顺序**(巧合掩盖了坑)。
|
|
627
|
+
- **副作用提醒**:既然落点靠序号,就**不能有空洞** —— 只用 slot 0 与 slot 2 会让实际落点整体前移。
|
|
628
|
+
`SLOT_CALIBRATION.columns` 为 1 时序号 = 槽号,所以「槽号连续」就是安全区。
|
|
629
|
+
- **出处**:fz-sapdon 回收机界面真机截图(2026-09-12),4 槽反解行号 4/4 吻合;相关 §4.9(坐标标定)、§4.12。
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
### 4.14 ★ `enabled: false` 是**整体禁用这一格**:产物格的产物也取不出来(2026-09-12,fz-sapdon 真机)
|
|
634
|
+
|
|
635
|
+
- **症状**:容器面板里那个**输出格**看得到产物,但**点不动、拿不出来**。用户原话:
|
|
636
|
+
> 「帮我把输出槽改成启用,不然拿不了物品」
|
|
637
|
+
- **根因**:框架对 `kind: 'output'` / `kind: 'display'` 的槽位写内层控件的 `enabled: false`
|
|
638
|
+
(`resolveSlot` 的 `isGatedKind()`)。这个 JSON UI 属性是**禁用整个控件**,
|
|
639
|
+
不是「只拦放入、放行取出」—— 它把**双向交互一起**关掉了。
|
|
640
|
+
§4.6 留的「该标志位能否真拦下"往槽里放东西"尚未真机确认」在这条上得到了**反向**答案:
|
|
641
|
+
它拦得住,而且**连"取出来"也一起拦**。对一个输出槽来说这是**致命的**:
|
|
642
|
+
产物永远拿不到手。
|
|
643
|
+
- **规避(框架已改)**:`resolveSlot` 里显式 `enabled` 现在**一律优先**于 `kind` 的缺省门控
|
|
644
|
+
(`merged.enabled ?? (isGatedKind(kind) ? false : undefined)`)。想做出「产物能取走」的输出格:
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
ui.addSlot({ slot: 1, pos: [108, 27], kind: 'output', enabled: true })
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**缺省行为一个字没变**(不传 `enabled` 时 `output` / `display` 仍写 `false`)⇒ 既有项目产物逐字节不变
|
|
651
|
+
(`kind` 只影响这一个键,见 `containerLayout.ts` 的 `ResolvedSlot`)。
|
|
652
|
+
- **什么时候该用哪种**:
|
|
653
|
+
- **产物格 / 输出槽** ⇒ `enabled: true`(**必须**,否则产物烂在格子里);
|
|
654
|
+
「只出不进」改由**加工逻辑**保证(只往它写),别再指望界面标志位。
|
|
655
|
+
- **进度槽 / 纯显示格** ⇒ 保留缺省 `false`(正是想要的效果:玩家既放不进也取不走那件进度载体物品)。
|
|
656
|
+
- **副作用提醒**:`enabled: false` 会把整格从交互链里摘掉,所以**脚本仍必须每拍读回真实格子**
|
|
657
|
+
(`readSlot` → 不是自己的物品就无条件补写)——不能因为「反正玩家动不了它」就省掉这步:
|
|
658
|
+
结构快照还原、旧存档、调试工具都可能让格子里不是预期的东西。
|
|
659
|
+
- **出处**:fz-sapdon 回收机输出槽(2026-09-12 真机,用户反馈);判据在
|
|
660
|
+
`tests/container-layout.test.mjs`(`resolveSlot` 覆盖规则)与 `tests/container-ui-output.test.mjs`(产物形状)。
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
### 4.15 ★★ `item_despawn` 那一组**一个字都不能改**:加 `delay` 会让**整容器一个都不掉**(2026-09-12 真机,丢了真物品)
|
|
665
|
+
|
|
666
|
+
- **原始症状(需求起点)**:玩家破坏机器,地上除真物品外**还多出一个内部物品**
|
|
667
|
+
(FZ 的进度 / 能量**显示载体** `fz:machine_progress`)。用户原话:
|
|
668
|
+
> 「破坏方块的时候不要把进度物品掉落出来」
|
|
669
|
+
- **机制**:`TileBlock` 给每个承载实体挂的实体数据里
|
|
670
|
+
|
|
671
|
+
```json
|
|
672
|
+
"item_despawn": {
|
|
673
|
+
"minecraft:despawn": {},
|
|
674
|
+
"minecraft:instant_despawn": { "remove_child_entities": false },
|
|
675
|
+
"minecraft:transformation": { "drop_inventory": true, "into": "minecraft:air" }
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
破坏方块 ⇒ `minecraft:block_sensor.on_break` → `despawn_event` → 这一组 ⇒ **整个容器倒出来**。
|
|
679
|
+
官方字段全集(`metadata/doc_modules/entities.json`,`minecraft:transformation`):
|
|
680
|
+
`add` / `begin_transform_sound` / `delay` / `drop_equipment` / **`drop_inventory`** / `into` /
|
|
681
|
+
`keep_level` / `keep_owner` / `preserve_equipment` / `transformation_sound`
|
|
682
|
+
—— **没有**任何「按槽位 / 按物品过滤掉落」的入口;`drop_inventory` 的原文是
|
|
683
|
+
"Cause the entity to drop all items in inventory upon transformation"。
|
|
684
|
+
|
|
685
|
+
- **★ 第一次的修法(错的,真机翻了车)**:往 `minecraft:transformation` 上加
|
|
686
|
+
`delay: { value: 0.1 }`,想用这段延迟让「破坏事件里的脚本先清掉内部格」。
|
|
687
|
+
结果:**地上什么都不掉,玩家的真物品一起没了**。用户原话:
|
|
688
|
+
> 「我的真物品也没有了」
|
|
689
|
+
|
|
690
|
+
**原因**:同一组里还有 `minecraft:instant_despawn`(**立即**移除实体)。
|
|
691
|
+
加了 delay 之后,实体先被立即删掉,**推迟的 transformation 再也没机会执行** ⇒
|
|
692
|
+
连 `drop_inventory` 都不发生。之前之所以能掉,正是因为 transformation 与那两个移除组件
|
|
693
|
+
**在同一瞬间**生效 —— 一旦把它推迟,它就落在那两者之后,等于没写。
|
|
694
|
+
|
|
695
|
+
- **规避(最终做法)**:
|
|
696
|
+
|
|
697
|
+
1. ★ **绝对不要碰 `item_despawn` 组**(不要加 `delay`、不要删 `instant_despawn`、
|
|
698
|
+
不要改 `drop_inventory`)。框架**不提供**任何「延迟 despawn」入口,`tileBlock.js` 顶部有警告。
|
|
699
|
+
2. **脚本侧**:破坏路径的**第一步**读容器 → 把内部格(进度 / 能量)`writeSlot(i, undefined)`。
|
|
700
|
+
⚠️ 读容器时**必须传「被破坏前那个方块的 id」**,不能传 `event.block.typeId`
|
|
701
|
+
(破坏后它恒为 `minecraft:air`)。
|
|
702
|
+
⚠️ 这一步与引擎的 despawn **谁先谁后未验证** ⇒ 它只是**尽力**,不能当成保证。
|
|
703
|
+
3. **★ 真正保证结果的是「掉落物过滤」(2026-09-12 用户提的,实测方向正确)**:
|
|
704
|
+
不去阻止掉落,而是**掉了之后删掉**。两个钩子,都只按物品 id 判:
|
|
705
|
+
|
|
706
|
+
| 事件 | 说明 |
|
|
707
|
+
|---|---|
|
|
708
|
+
| `world.afterEvents.entityItemDrop` | 最贴切:`event.items` **直接给出被掉出来的物品实体**(`Entity[]`) |
|
|
709
|
+
| `world.afterEvents.entitySpawn` | 兜底:覆盖没走前者的路径(爆炸 / `/setblock` / 活塞等**非玩家破坏**也走它) |
|
|
710
|
+
|
|
711
|
+
```ts
|
|
712
|
+
world.afterEvents.entityItemDrop.subscribe((e) => {
|
|
713
|
+
for (const item of e.items) if (isOurs(item)) item.remove()
|
|
714
|
+
})
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
**为什么可以无条件删、不必判位置**:内部载体物品(本例是 `fz:machine_progress`)
|
|
718
|
+
是 `category: none`、不进创造菜单、没有配方 ⇒ 正常途径**拿不到**它,
|
|
719
|
+
「世界上出现这个物品实体」本身就是泄漏。
|
|
720
|
+
**为什么不可能误删真物品**:判据是**物品 id 逐字相等**。
|
|
721
|
+
⚠️ 热路径要求:`entitySpawn` 是**每一次实体生成**都触发的事件 ⇒ 判据必须
|
|
722
|
+
「一次字符串比较 + 不命中立刻返回」,绝不能对每只怪都 `getComponent`。
|
|
723
|
+
⚠️ 掉落物实体的 `typeId` **通常就是物品 id**;若某版本给的是通用的
|
|
724
|
+
`minecraft:item`,真正的物品 id 在 `minecraft:item` 组件的 `itemStack` 里
|
|
725
|
+
—— 先比 `typeId`、**只在它是 `minecraft:item` 时**才去翻组件(省掉热路径开销)。
|
|
726
|
+
|
|
727
|
+
- **反面做法(都不要用)**:
|
|
728
|
+
- 把 `drop_inventory` 改成 `false` 再由脚本自己掉真物品 ⇒ 失败模式是
|
|
729
|
+
「脚本没跑 ⇒ 玩家的东西凭空消失」;
|
|
730
|
+
- 给 transformation 加 `delay` ⇒ **就是本条目踩的那一脚**,失败模式同样是真物品消失;
|
|
731
|
+
- 依赖「破坏事件里的脚本一定先于引擎 despawn 跑完」⇒ 实测**赶不上**(第一层清格试过,没赶上)。
|
|
732
|
+
**判断准则**:任何改动只要可能让「真物品不掉」,就一律不做 ——
|
|
733
|
+
最坏情况只允许是「多掉一个内部物品」(最终由上面第 3 条删掉)。
|
|
734
|
+
|
|
735
|
+
- **出处**:FZ 回收机(2026-09-12 用户两轮反馈:先是「不要把进度物品掉出来」,后是「我的真物品也没有了」);
|
|
736
|
+
字段定义取自原版包 `bedrock-samples-1.21.130.26-preview/metadata/doc_modules/entities.json`
|
|
737
|
+
(`minecraft:transformation` 小节);判据在 `tests/block-api.test.mjs`
|
|
738
|
+
(`item_despawn` 组与历史产物**逐字节一致**、`transformation` 上不许出现 `delay`)。
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## 5. 本仓库的构建方式(受限环境)
|
|
743
|
+
|
|
744
|
+
`npm run build` / `node scripts/build.cjs` 在受限沙箱里跑不了(`cp.exec` 走管道 → `spawn EPERM`)。
|
|
745
|
+
等价拆成 4 步直跑:
|
|
746
|
+
|
|
747
|
+
```
|
|
748
|
+
tsc # node_modules/typescript/bin/tsc
|
|
749
|
+
tsc-alias # node_modules/tsc-alias/dist/bin/index.js
|
|
750
|
+
node scripts/buildTask.cjs # rollup → prod/
|
|
751
|
+
# 拷贝 src/templates → prod/templates,然后删除 dist/
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
- ⚠️ **`tsc-alias` 不能漏**:漏掉它,`dist/` 里会残留 `@sapdon/utils/...` 裸别名,
|
|
755
|
+
rollup 解析不到就当成 external → **`prod/cli/start.js` 会 import 无法解析的 `@sapdon/utils`**,
|
|
756
|
+
表现为 `ERR_MODULE_NOT_FOUND: Cannot find package '@sapdon/utils'`。
|
|
757
|
+
- ⚠️ **构建"9/9 成功"不等于 prod 是新的**:改完要**断言 prod 内容**(例如 `prod/oc/index.js` 里有没有新导出),别只看 `Failed: 0`。
|
|
758
|
+
- 单测:`node --test` 会 fork 子进程(受限环境 `EPERM`)→ 直接跑文件:`node tests/persist.test.mjs`(需先 `tsc` 生成 `dist/`,测试 import 的是 `../dist/...`)。
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## 6. 待真机确认(本环境无法启动 Minecraft)
|
|
763
|
+
|
|
764
|
+
- [ ] 容器(★ 现在只剩**实体路线**可用):用 `createTileBlock(..., { inventory_size })` 放一个带容器的方块,
|
|
765
|
+
右键能打开、能存取;**并确认大槽位**(FZ 机器需要 56)被引擎接受(实体组件文档没给上限)。
|
|
766
|
+
- [x] ~~**★ `"enabled": false` 能否拦住「往这个槽里放东西」**~~ → **2026-09-12 真机定案**:
|
|
767
|
+
它拦得住「放进去」,**但同时也拦住了「取出来」** —— 语义是**整体禁用这一格**,
|
|
768
|
+
不是「只拦放入」。⇒ 产物格必须显式写 `enabled: true`(框架已改成「显式值优先」),
|
|
769
|
+
完整证据 / 修法 / 取舍见 §4.14。
|
|
770
|
+
- [ ] **★ 容器版面坐标空间校准(§4.9)**:拿一个 `setGridOrigin([0,0])` + 2~3 个 `pos` 取整十数的探针面板,
|
|
771
|
+
量实际渲染位置与 `pos` 的差 ⇒ 决定 `SLOT_CALIBRATION.anchor` 取 `top_left` 还是 `center`、
|
|
772
|
+
`originPadding` 要不要补偏移。**在此之前所有 `offset` 数值都只是"按假设算出来的"**。
|
|
773
|
+
- [ ] 容器面板的**层序**:`common_panel`(原版灰底)→ `panel_background`(layer 1) → `container_panel`/`inventory_panel`(layer 2)
|
|
774
|
+
→ `grids`(layer 3) → `main_panel`(layer 4) → `title`(layer 12) 这套层号在真机上是否真的按预期叠放
|
|
775
|
+
(「面板在、图没了」就是层序错的典型表现,见 §4.5 坑 3)。
|
|
776
|
+
- [ ] 方块路线的 `minecraft:block_entity.container`:当前引擎版本报
|
|
777
|
+
`-> minecraft:block_entity -> container: … not present in the Schema`(1.26.30 / 1.26.40 实测同样);
|
|
778
|
+
等引擎支持后 `setBlockEntity(true, { container: { slot_count } })` 是否即可用(`slot_count` 需在 `[1,54]`)。
|
|
779
|
+
- [ ] **物品告警清零**(P0-1 的最终验收):用自定义 item catalog 的项目重进世界,
|
|
780
|
+
确认 `The item <X> was created with the group set to 'minecraft:…'` 这类 warning 变成 **0 条**(默认值改 1.21.90 后预期)。
|
|
781
|
+
- [ ] **方块几何告警清零**(P0-3 的最终验收):`blocks.json` 不再写方块条目后,
|
|
782
|
+
确认 `trying to override the Geometry component with blocks.json settings for a custom block` 的 N 条(= 方块数)变成 **0 条**;
|
|
783
|
+
并确认挖掘/放置**音效**与改动前一致(框架从未写过 `sound` 字段,预期无变化),以及只含
|
|
784
|
+
`format_version` 的 `blocks.json` 引擎会不会有别的抱怨。贴图应仍走 `material_instances` + `terrain_texture.json`。
|
|
785
|
+
- [ ] **部署 prune 的真机效果**:从项目里删掉一个方块/配方/`res/` 资源后重新构建,
|
|
786
|
+
确认游戏里对应的旧文件**真的消失**(不再报 `not present in the Schema`),且玩家自己放进开发包的文件仍在。
|
|
787
|
+
- [ ] 路线 B:`registerBlockComponent` 注册的组件在游戏内事件是否真的触发(本环境只用桩验证了注册时机与注册表)。
|
|
788
|
+
- [ ] **★ `sapdon:block_with_entity` 内置实现(模式 A)的真机行为**(本轮新增,全部未验证):
|
|
789
|
+
① 只 `createTileBlock`、不手工注册的项目,方块**不再被引擎丢**(`not present in the Schema` 消失);
|
|
790
|
+
② `onPlace` spawn 出来的 `${typeId}_entity` **落点正确**(脚底贴方块底面 = `block.center().y - 0.5`,
|
|
791
|
+
参考形状出自 `examples/mob_chest`,但该参考只在本环境外被验证过);
|
|
792
|
+
③ **右键能打开容器**(界面是引擎原生开的,`@minecraft/server` 没有「给玩家打开容器」的 API);
|
|
793
|
+
④ `onPlace` 是否会因 `setPermutation` 再次触发 —— 内置实现靠 `getEntitiesAtBlockLocation` 查同种实体**防重复**,
|
|
794
|
+
若引擎在放置瞬间还没把实体登记进查询结果,仍可能出双容器(**本环境无法证伪**);
|
|
795
|
+
⑤ 破坏方块后 `minecraft:block_sensor` → `despawn_event` 是否真的不留幽灵实体。
|
|
796
|
+
- [ ] `options.entity_texture` 的真机效果:给 terrain 短名的项目传完整路径后,实体贴图 `Missing referenced asset` 是否消失。
|
|
797
|
+
- [ ] 内置 `geometry.cube`(模板)在新项目里是否真的被 RP 收录(`sapdon create` + 一次构建后看 `dev/<proj>_RP/models/blocks/cube.geo.json`)。
|
|
798
|
+
- [ ] i18n:`labels` 传 lang 键时,JSON UI 是否按预期解析(需要 `RP/texts/*.lang` 里定义该键)。
|
|
799
|
+
|
|
800
|
+
---
|
|
801
|
+
|
|
802
|
+
## 7. 仓库里**已知损坏**的东西(不是框架回归,别误判)
|
|
803
|
+
|
|
804
|
+
按 2026-09 那次全量核对的口径记录(判据:`git log ae6ae16..HEAD --name-only` 未触及相关模块)。
|
|
805
|
+
|
|
806
|
+
- **`examples/hello_ui` 构建必失败**(exit 1、0 行 `处理数据:`):`main.mjs` import 了本框架**不存在**的 `ServerUISystem`,还调用了 `bindingTitlewithContent` —— 该示例停留在旧 API。**不是框架回归**;修它要改 examples 源码(本轮按"examples 是范本、不改源码"的约束未动)。
|
|
807
|
+
- 它的 `dev/hello_ui_*` 里躺着 07-11/08-21 的陈旧产物:这是**长期构建失败**造成的(没有成功构建 → 没有清单 → 清不掉),**不是**改项目名残留,故未删。修好该示例后建议手工清一次 `dev/`。
|
|
808
|
+
- **`tests/ui-buttonpanel.test.mjs` 失败**:import 了早已不存在的 `dist/core/ui/systems/sapdon/sapdonButtonPanel.js`(该模块在 `63262bf` 之后就不在 `src/` 里)。
|
|
809
|
+
- **`tests/item.test.mjs` 失败**:`src/core/entity/componets/entityComponet.js` 把 `type.ts` 的 **type-only** 导出 `RideableComponentDesc` 当**值** import → 运行期 `does not provide an export named 'RideableComponentDesc'`(rollup 构建日志里也有同名 warning)。
|
|
810
|
+
- 上面两个测试**在 `ae6ae16` 之前就已损坏**,与本轮改动无关;本轮未修(超出范围)。
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
## 8. ★ `sapdon lib` 只在**框架仓库内部**可用(已知缺陷,待修)
|
|
815
|
+
|
|
816
|
+
**症状**(2026-09 由 FZ 项目实测复现,exit 1):
|
|
817
|
+
```
|
|
818
|
+
Error: src and dest cannot be the same \\?\<proj>\node_modules\@sapdon\core
|
|
819
|
+
code: 'ERR_FS_CP_EINVAL'
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**根因**(`lib` 的实现,见 `prod/cli/start.js` 的 `ge()`):
|
|
823
|
+
```js
|
|
824
|
+
const t = path.join(path.dirname(fileURLToPath(import.meta.url)), '../') // ← 框架源 = CLI 自己的兄弟目录
|
|
825
|
+
cpSync(path.join(t,'core'), path.join(n,'@sapdon/core'), {recursive:true, force:true})
|
|
826
|
+
cpSync(path.join(t,'cli'), path.join(n,'@sapdon/cli'), {recursive:true, force:true})
|
|
827
|
+
cpSync(path.join(t,'oc'), path.join(n,'@sapdon/runtime'), {recursive:true, force:true}) // 注意 oc → runtime
|
|
828
|
+
```
|
|
829
|
+
`lib` 把**「CLI 所在目录的父目录」**当成框架源码根。这对框架自己的 `prod/` 成立
|
|
830
|
+
(`prod/{core,cli,oc}` 就是框架源),但对**任何从项目 `node_modules/@sapdon/cli` 解析到 CLI 的项目**
|
|
831
|
+
都不成立 —— 那时 `t` 就是项目自己的 `node_modules/@sapdon`,于是 `src === dest`。
|
|
832
|
+
|
|
833
|
+
**范围**:npm 安装的正常用户布局**正是**「CLI 在项目的 `node_modules/@sapdon/cli`」,
|
|
834
|
+
所以 `sapdon lib` 实际上**对所有按正常方式装依赖的用户项目都不工作**,不只是某一个仓库。
|
|
835
|
+
|
|
836
|
+
**第二重问题(读码 + 列目录,未实跑)**:目标名是 `runtime` 而源目录名是 `oc`。
|
|
837
|
+
`prod/` 下有 `oc/`;`D:\Projects\sapdon\node_modules\@sapdon\` 与
|
|
838
|
+
`examples/guidebook_demo/node_modules/@sapdon/` 下**都只有 `cli`/`core`/`runtime`、没有 `oc`**。
|
|
839
|
+
⇒ 即使 `src === dest` 被修掉,只要 CLI 来自任何 `node_modules/@sapdon/` 布局,
|
|
840
|
+
`cpSync(t/'oc', …)` 仍会因**源不存在**失败(ENOENT)。
|
|
841
|
+
|
|
842
|
+
**临时绕过**(项目侧可用,不改框架):显式指定框架构建产物的 CLI,例如
|
|
843
|
+
`SAPDON_CLI=D:/Projects/sapdon/prod/cli/start.js node tools/sapdon.mjs lib`
|
|
844
|
+
(`prod/` 是**唯一**可用的 lib 源:那里同时有 `core/`、`cli/`、`oc/`)。
|
|
845
|
+
|
|
846
|
+
**修法方向(待定)**:给 `lib` 一个显式来源(环境变量 / 框架根参数 / 从依赖解析 `@sapdon/core`
|
|
847
|
+
的真实安装位置),并把内部的 `oc` 与发布名 `runtime` 的映射落定。
|
|
848
|
+
|
|
849
|
+
**★ 更简的修法建议(2026-09 S3b 复核后补充,本轮只记不做)** —— 不引入任何新参数即可让 `lib`
|
|
850
|
+
在正常用户布局下**不炸**,代价只是「按布局决定做多少事」:
|
|
851
|
+
|
|
852
|
+
1. **先判「源 == 目标」再拷贝**:对每个包算 `path.resolve(src)` 与 `path.resolve(dest)`,
|
|
853
|
+
若两者相同(含 `dest` 落在 `src` **内部**的情形)→ **跳过该包**并打印一行说明
|
|
854
|
+
(例如 `跳过 @sapdon/core:CLI 就来自项目自己的 node_modules/@sapdon,源与目标同一处`)。
|
|
855
|
+
这条直接消灭 `ERR_FS_CP_EINVAL: src and dest cannot be the same`,
|
|
856
|
+
且**不改变**「CLI 来自框架 `prod/`」时(框架仓库内部)的既有行为。
|
|
857
|
+
2. **源目录探测同时接受 `oc` 与 `runtime`**:先试 `path.join(root, 'oc')`,不存在再试
|
|
858
|
+
`path.join(root, 'runtime')`;两者都不存在 → 跳过该包 + 一行说明。
|
|
859
|
+
理由:目标名一直是 `runtime`(发布名),而框架 `prod/` 下的源目录叫 `oc` ——
|
|
860
|
+
任何从 `node_modules/@sapdon/` 解析到 CLI 的布局都只有 `runtime`、没有 `oc`,
|
|
861
|
+
现状会以 ENOENT 失败(§8 第二重问题)。
|
|
862
|
+
3. 两条合起来的语义:**「能同步的就同步,同步不了的(源就是目标 / 源不存在)明确说明并跳过,
|
|
863
|
+
而不是整条命令 exit 1」**。真正的错误(权限、磁盘满)仍然抛。
|
|
864
|
+
|
|
865
|
+
> ⚠️ 判据(将来实现时必须给):框架仓库内部 `node prod/cli/start.js lib` 行为不变
|
|
866
|
+
> (`examples/*/node_modules/@sapdon/{core,cli,runtime}` 时间戳更新);
|
|
867
|
+
> 从项目自己的 `node_modules/@sapdon/cli` 解析时**exit 0** 且打印「跳过」而不是 `ERR_FS_CP_EINVAL`。
|
|
868
|
+
|
|
869
|
+
---
|