sapdon 3.3.3 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ # L-R 编程范式(sapdon 存量类 Addon 通用骨架)
2
+
3
+ > 服务对象:`examples/digitCircuit`(数电)、`examples/fluid_pipe`(流体)、`examples/power_grid`(电力)——一个可复用的「装备类/生态类」Addon 编程范式。
4
+
5
+ 这三类系统在玩法与数据形态上完全不同,但骨架高度一致:**都走一条"逻辑求解 → 渲染同步"的 L/R 分离流水线**,且都由 **连接(C₁)、源(S)、功能(F)、消费(Cc)、储量(R)** 五元组构成一个可更新的"资源网络"。
6
+
7
+ ---
8
+
9
+ ## 1. 五元组映射
10
+
11
+ | 元组 | digitCircuit 数电 | fluid_pipe 流体 | power_grid 电力 |
12
+ |---|---|---|---|
13
+ | **连接** C₁ | 导线 Net(逐面 `wire_connect:*` 手臂) | 段 Segment(逐面 `pipe_connect:*`) | 段(正交自动连通) |
14
+ | **源** S | on/off/switch 信号源 | 泵输出(+△) | 燃煤发电机 / 太阳能 |
15
+ | **功能** F | 门 / 分线器 / 合并器 | 阀门 / 三通 | 继电器(可控桥) |
16
+ | **消费** Cc | 显示灯(仅显示) | 罐纯吸收 / 空气汇 | 电力熔炉(真实熔炼) |
17
+ | **储量** R | 寄存器 store / 芯片 | 罐(32 格液位) | 电池(0..MAX 电量) |
18
+
19
+ > 关键洞察:F(功能块)本质是**受控的连接/变换器**——数电的门变换信号、流体的阀透传/断流、电力的继电器合并/分隔电网。绝大多数新生态系统只需把 F 当"可通断的耦合节点"即可起步。
20
+
21
+ ## 2. 分层约束(铁的边界)
22
+
23
+ ```
24
+ ┌───────────────────────────────────────────────────────────┐
25
+ │ L 层 scripts/core/(纯逻辑,禁止 import @minecraft/server)│
26
+ │ graph.ts 建立"连接块图":洪水填充成段/网,收设备端点 │
27
+ │ resource.ts/ settle.ts 资源在场上的传播/归并 + 逐 tick 结算│
28
+ │ → 把结果写进 seg.xxx(powered / front / covered / value) │
29
+ ├───────────────────────────────────────────────────────────┤
30
+ │ R 层 scripts/engine/(MC 引擎,读世界方块) │
31
+ │ const/world/state/log/diag │
32
+ │ graph.ts 实现 L 层 FloodGraph 接口(世界实现) │
33
+ │ rebuild.ts 放置/破坏/开关 → 重建段 + 加载后渐进重建 │
34
+ │ render.ts 只读 seg.xxx 同步写方块状态(发光/液位/带电) │
35
+ │ persist.ts 只存小状态(设备表/连接位置),图=重建不落盘 │
36
+ │ tick.ts 主循环:L 结算 → R 渲染 → 设备存活检查 │
37
+ │ index.ts 注册:命令 / 自定义组件 / 事件 / 主循环 │
38
+ └───────────────────────────────────────────────────────────┘
39
+ ```
40
+
41
+ 铁律:
42
+ 1. **L 层零 MC 依赖** → 可在 Node 直接镜像测试。
43
+ 2. **L 写 `seg.*`,R 只读** → 状态单一来源,渲染层可缓存去重。
44
+ 3. **连接块手臂/朝向 = 方块状态**(世界自动持久化),**图与传播场不落盘**,靠事件 + 加载后 `rebuildPending` 逐批(每 tick 64 个)渐进洪水重建。
45
+ 4. **持久化只存小状态**:设备表(储量 level、源燃料、消费进度)+ 连接位置;液位/流动态只进内存 + 方块状态视觉兜底。
46
+ 5. **写动态属性不可吞异常**(吞掉 = 重进世界静默丢存档);用 `loaded` 门闩防启动早期空表覆盖存档。
47
+
48
+ ## 3. 资源网络结算模板
49
+
50
+ ```
51
+ 每 tick(如 20t≈1s):
52
+ 1. 连接图(L) : 沿连接块洪水 → 段/网
53
+ 2. 场/结算(L) : 若含共享设备的耦合(F/S/R 把邻段并网,F 用受控 open/closed 决定是否并)
54
+ → 网格级(或势场级)结算:源供给侧 vs 消费需求侧 + 储量缓冲
55
+ → 写 seg.powered / seg.covered 等
56
+ 3. 渲染(R) : 读 seg.* → 写方块状态(电线发光、设备液位/燃烧/带电)
57
+ 4. 存量(R) : 充电/放电/喂料扣除 → save(仅结构事件)
58
+ ```
59
+
60
+ 三种结算粒度的取舍:
61
+ - **网格级全局**(power_grid):供/需对账 + 全有/全无。最简、稳压直觉,丢物理空间感。
62
+ - **势/距离场**(fluid_pipe):源势沿图传播、衰减,决定"能不能到、到多高"。物理感强但复杂度高(需构思成本函数)。
63
+ - **布尔/位宽**(digitCircuit):信号值 + 位宽固定点,天然适合布尔逻辑合成。
64
+
65
+ ## 4. 镜像测试约束(母子同步)
66
+
67
+ L 层用 TS 写、Node 不能直接跑,因此采用**镜像副本**:`test/<name>.test.mjs` 复制 L 层纯逻辑(JS 版)。**改 L 层逻辑必须同步测试副本并跑绿**。这同时约束了 L 层必须保持"纯函数、可独立 import、无副作用",否则镜像就同步不动。
68
+
69
+ ## 5. 可复用工具箱(从三项目沉淀)
70
+
71
+ - `world.ts`:`blockKey / keyParts / getBlockByKey / getAdjacent` 一致性 key(`dim:x,y,z`)。
72
+ - `rebuildAround / rebuildPending / rebuildStale`:结构重建三件套(事件重建 + 加载渐进重建 + 失效重建)。
73
+ - `persist.ts`:动态属性分块读写通用骨架(单块/多块 `_chunks`)。
74
+ - 旋转设备:局部参考系语义面 + `ROT_FACE(facing, local)` 映射(见 fluid_pipe AGENTS),杜绝写死世界面。
75
+ - 诊断:`console.warn` ContentLog + 运行日志开关 + dump 就近转储。
76
+
77
+ ## 6. 已知坑(三项目通用)
78
+
79
+ - 方块状态 **≤16 有效值**(整数范围 max-min ≤15)。
80
+ - 单元用普通立方体或命名材质 geo;同一方块所有 material instance 必须同一 `render_method`。
81
+ - Molang 变量名统一小写;粒子 `basic_*` 需传 `variable.direction`。
82
+ - 调试证据优先 ContentLog(`console.warn`),`world.sendMessage` 不进日志。
83
+
84
+ ---
85
+ > 新生态示例(如"供水系统""机械传动""雨洪管网")建议直接套本范式:先定 L 层 graph+settle 并写镜像测试(绿后进 R),再补 R 层引擎与贴图。
@@ -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`。
@@ -106,7 +106,7 @@ hello_sapdon/
106
106
  ]
107
107
  }
108
108
  ```
109
- - **buildMode**:构建模式。设置为 `"development"` 时,构建程序将根据 `main.mjs` 中的内容构建 Addon 包,并将输出到 `buildDir` 指定的文件夹中;设置为 `"debug"` 时,仅将 `dev` 文件夹的内容输出到指定路径。
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", "items", "masterball");
123
+ ItemAPI.createItem("hello_sapdon:my_item", ItemCategory.Items, "masterball");
124
124
  ```
125
125
  这段代码将创建一个名为 `my_item` 的物品,其命名空间为 `hello_sapdon`,类型为 `items`,并使用 `masterball` 作为图标。
126
126