sapdon 3.4.0 → 3.5.1

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,302 @@
1
+ # SapdonGuideBook —— 帕秋莉式手册框架类
2
+
3
+ `SapdonGuideBook` 是 Sapdon 提供的**帕秋莉式手册(Guidebook)**框架类。它让你用**纯数据声明**的方式,快速做出一本"分类索引 → 词条列表 → 内容页"的三层手册,并内置多种页类型、分页与导航。
4
+
5
+ - **三层结构**:`INDEX`(分类索引)→ `CAT`(词条列表)→ `ENT`(词条内容页)。
6
+ - **浏览器式导航**:每屏固定 `prev / home / next`,`home` 随时回首页。
7
+ - **多种页类型**:`text` / `crafting` / `spotlight` / `image`。
8
+ - **自动分页**:正文超过一屏自动翻页;分类词条超过 8 条自动分页。
9
+ - **路由驱动**:运行时通过 Server Form 的 `body` 路径 + 按钮槽位显隐,无需每个页面单独写路由。
10
+
11
+ > 示例见 `examples/guidebook_demo`(打开游戏手持木棍右键即可看到成品)。
12
+
13
+ ---
14
+
15
+ ## 1. 引入
16
+
17
+ ```ts
18
+ import { SapdonGuideBook } from '@sapdon/core'
19
+ import { registry } from '@sapdon/core'
20
+ ```
21
+
22
+ 配套类型:
23
+ ```ts
24
+ import type { GuideBookCategory, GuideBookChapter, GuideBookPageType } from '@sapdon/core'
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 2. 快速开始
30
+
31
+ 在项目的 `main.ts`(框架构建入口)里:
32
+
33
+ ```ts
34
+ import { SapdonGuideBook, registry } from '@sapdon/core'
35
+
36
+ const book = new SapdonGuideBook('mymod:book', [320, 207], 'textures/ui/book_back')
37
+
38
+ book.build([
39
+ {
40
+ id: 'intro', title: '介绍', icon: 'textures/items/book_writable',
41
+ introLines: ['欢迎使用这本手册。', '它由 SapdonGuideBook 构建。'],
42
+ chapters: [
43
+ { name: '这是什么', icon: 'textures/items/book_writable', lines: ['这是一本帕秋莉式手册。', '分三层:索引→列表→内容。'] },
44
+ { name: '如何打开', icon: 'textures/items/paper', lines: ['手持木棍右键打开本手册。'] },
45
+ ],
46
+ },
47
+ ])
48
+
49
+ registry.submit()
50
+ ```
51
+
52
+ ### 构造函数签名
53
+
54
+ ```ts
55
+ new SapdonGuideBook(
56
+ identifier: string, // "namespace:name",如 "mymod:book"
57
+ size: [number, number] = [320, 207], // 手册画布尺寸
58
+ background: string = 'textures/ui/book_back' // 背景贴图
59
+ )
60
+ ```
61
+
62
+ ### 常用方法
63
+
64
+ | 方法 | 说明 |
65
+ |---|---|
66
+ | `.build(categories: GuideBookCategory[])` | 传入分类数据,生成全部页面;返回 `this` |
67
+ | `.setCover(title, lines)` | 自定义封面标题(可含 `\n`)与简介行,如 `.setCover(' 我的手册 \\n by Me', ['第一行简介', '第二行简介'])` |
68
+ | `.enableDebug()` | 开启调试(显示 `#form_text` 当前值 / 格子描边) |
69
+ | `.getSystem()` | 返回内部 `UISystem` |
70
+
71
+ 调用链结束前记得 `registry.submit()`,把注册的 UI 数据提交给构建工具生成 `book.json`。
72
+
73
+ ---
74
+
75
+ ## 3. 数据结构
76
+
77
+ ### `GuideBookCategory`(分类)
78
+
79
+ ```ts
80
+ interface GuideBookCategory {
81
+ id: string // 路由 id(英文,如 "intro"),唯一
82
+ title: string // 中文标题(索引卡名称 / 左页标题)
83
+ icon: string // 索引卡图标贴图路径
84
+ introLines: string[] // 分类简介(左半页逐行渲染)
85
+ chapters: GuideBookChapter[] // 词条列表
86
+ }
87
+ ```
88
+
89
+ ### `GuideBookChapter`(词条)
90
+
91
+ ```ts
92
+ interface GuideBookChapter {
93
+ name: string // 词条名(列表行 / 内容页标题)
94
+ icon: string // 列表行图标
95
+ lines: string[] // 正文(text 页逐行渲染)
96
+ pageType?: 'text' | 'crafting' | 'spotlight' | 'image' // 默认 text
97
+ craft?: { grid: string[]; output: string } // crafting 页
98
+ spotlight?: { icon: string; desc: string } // spotlight 页
99
+ image?: { texture: string; caption: string } // image 页
100
+ }
101
+ ```
102
+
103
+ > ⚠️ 词条名 **不要以 `#` 开头**(如 `#foo 门控`)。Bedrock 会把以 `#` 开头的文本当作绑定,渲染成空。需要表现 `#` 时放在句子中间或写成 `foo 门控`。
104
+
105
+ ---
106
+
107
+ ## 4. 页类型(`pageType`)
108
+
109
+ | `pageType` | 说明 | 相关字段 |
110
+ |---|---|---|
111
+ | `text`(默认) | 逐行渲染正文,支持分页 | `lines` |
112
+ | `crafting` | 3×3 合成台 + 箭头 + 单个产物格 | `craft.grid`(9 项,空位 `''`)+ `craft.output` |
113
+ | `spotlight` | 大图标 + 描述 | `spotlight.icon` + `spotlight.desc`(含 `\n` 会多行) |
114
+ | `image` | 整页图 + 说明 | `image.texture` + `image.caption` |
115
+
116
+ `crafting` 示例:
117
+
118
+ ```ts
119
+ {
120
+ name: '合成示例', icon: 'textures/items/iron_ingot', pageType: 'crafting',
121
+ craft: {
122
+ grid: ['textures/items/iron_ingot','textures/items/iron_ingot','textures/items/iron_ingot',
123
+ 'textures/items/iron_ingot','','textures/items/iron_ingot',
124
+ 'textures/items/iron_ingot','','textures/items/iron_ingot'],
125
+ output: 'textures/items/iron_leggings',
126
+ },
127
+ lines: ['铁锭 → 铁护腿'],
128
+ }
129
+ ```
130
+
131
+ ---
132
+
133
+ ## 5. 路由协议(运行时)
134
+
135
+ 手册内容由**固定布局 + 门控**驱动:布局容器按 `body` 路径显隐,按钮按 `form_button_text` 精确显隐。具体由项目的 `scripts/index.ts` 用 `ActionFormData` 发射。
136
+
137
+ - **`title`**:固定为 `sapdon_ui:<name>`(如 `sapdon_ui:book`)。
138
+ - **`body`(路径)**:
139
+ - `"INDEX"` → 分类索引页
140
+ - `"CAT:<id>|p<N>"` → 分类页(`N` 为分类页码,`p0` 左简介右列表)
141
+ - `"ENT:<id>:<gi>|p<N>"` → 词条内容页(`gi` 为词条序号,`N` 为内容页码)
142
+
143
+ ### 按钮槽位(顺序固定)
144
+
145
+ | 页面 | 槽位 |
146
+ |---|---|
147
+ | INDEX | `[no_prev, no_home, no_next, idx0..3]`(三导航全隐藏) |
148
+ | CAT | `[prev\|no_prev, home, next\|no_next, <id>_e<num>...]` |
149
+ | ENT | `[prev, home, next\|no_next]` |
150
+
151
+ 占位键 `no_prev / no_home / no_next` 不代表任何注册按钮,从而让对应导航按钮**隐藏**。
152
+
153
+ ### 分页规则
154
+
155
+ - **CAT 列表**:每列最多 8 行。`p0` 右列 8 行;`p1+` 左 8 + 右 8(=16 行/页)。
156
+ - **ENT 正文(text)**:左右半页各最多 5 行,先填左半页、超出再填右半页;**整体超过 10 行才分页**。
157
+
158
+ 运行时脚本里需要维护两个与 `main.ts` 数据对齐的量:
159
+
160
+ ```ts
161
+ // scripts/index.ts
162
+ const CATS = ["intro", "pages", "routing", "controls"]; // 与 main.ts 分类 id 对齐(含顺序)
163
+ const CAT_CHAPTERS: Record<string, number> = { intro: 4, pages: 6, routing: 6, controls: 6 }; // 每分类词条数
164
+ const ENT_PAGES: Record<string, number> = { pages_e4: 2 }; // 需要多页的 text 词条 → 页数(ceil(lines/10))
165
+ ```
166
+
167
+ > 若某 text 词条行数超过 10,`main.ts` 会用 `ENT_PAGES` 里的页数来让 next/prev 生效。忘加会导致分页无法翻动。
168
+
169
+ ---
170
+
171
+ ## 6. 手写运行时路由(`scripts/index.ts` 参考)
172
+
173
+ 项目里还需一个"脚本入口"(build.config 的 `scriptEntry`),示例为 `scripts/index.ts`,用木棍右键打开手册:
174
+
175
+ ```ts
176
+ import { world, Player } from "@minecraft/server";
177
+ import { ActionFormData } from "@minecraft/server-ui";
178
+
179
+ const CATS = ["intro", "pages", "routing", "controls"];
180
+ const CAT_CHAPTERS = { intro: 4, pages: 6, routing: 6, controls: 6 };
181
+ const ENT_PAGES = { pages_e4: 2 };
182
+ const TITLE = "sapdon_ui:book";
183
+ const NO_PREV = "no_prev", NO_HOME = "no_home", NO_NEXT = "no_next";
184
+
185
+ function openIndex(p: Player): void {
186
+ const f = new ActionFormData().title(TITLE).body("INDEX");
187
+ f.button(NO_PREV); f.button(NO_HOME); f.button(NO_NEXT);
188
+ CATS.forEach((_, i) => f.button(`idx${i}`));
189
+ f.show(p).then((r) => {
190
+ if (r.canceled) return;
191
+ const s = r.selection!;
192
+ if (s >= 3 && s - 3 < CATS.length) openCat(p, CATS[s - 3], 0);
193
+ else openIndex(p);
194
+ });
195
+ }
196
+
197
+ function openCat(p: Player, id: string, page: number): void {
198
+ const total = CAT_CHAPTERS[id] ?? 0;
199
+ const start = page === 0 ? 0 : 8 + (page - 1) * 16;
200
+ const end = Math.min(start + (page === 0 ? 8 : 16), total);
201
+ const f = new ActionFormData().title(TITLE).body(`CAT:${id}|p${page}`);
202
+ f.button(page > 0 ? "prev_button" : NO_PREV);
203
+ f.button("home_button");
204
+ f.button(end < total ? "next_button" : NO_NEXT);
205
+ for (let i = start; i < end; i++) f.button(`${id}_e${i}`);
206
+ f.show(p).then((r) => {
207
+ if (r.canceled) return;
208
+ const s = r.selection!;
209
+ if (s === 0 && page > 0) openCat(p, id, page - 1);
210
+ else if (s === 1) openIndex(p);
211
+ else if (s === 2 && end < total) openCat(p, id, page + 1);
212
+ else if (s >= 3) { const gi = start + (s - 3); if (gi < total) openEnt(p, id, gi, page, 0); }
213
+ });
214
+ }
215
+
216
+ function openEnt(p: Player, id: string, gi: number, fromPage: number, ep: number): void {
217
+ const pc = ENT_PAGES[`${id}_e${gi}`] ?? 1;
218
+ const f = new ActionFormData().title(TITLE).body(`ENT:${id}:${gi}|p${ep}`);
219
+ f.button("prev_button"); f.button("home_button");
220
+ f.button(ep < pc - 1 ? "next_button" : NO_NEXT);
221
+ f.show(p).then((r) => {
222
+ if (r.canceled) return;
223
+ const s = r.selection!;
224
+ if (s === 0) ep > 0 ? openEnt(p, id, gi, fromPage, ep - 1) : openCat(p, id, fromPage);
225
+ else if (s === 1) openIndex(p);
226
+ else if (s === 2 && ep < pc - 1) openEnt(p, id, gi, fromPage, ep + 1);
227
+ });
228
+ }
229
+
230
+ world.afterEvents.itemUse.subscribe((e) => {
231
+ if (e.itemStack.typeId === "minecraft:stick" && e.source.typeId === "minecraft:player")
232
+ openIndex(e.source as Player);
233
+ });
234
+ ```
235
+
236
+ > **打开触发**默认是手持**木棍右键**(`minecraft:stick`)。若手册由某个具体物品打开(如 more-golem 的指南书),把 `e.itemStack.typeId` 换成该物品的 id,或改为在物品的 `onUse` 自定义组件里直接调 `openIndex(player)`。
237
+
238
+ ---
239
+
240
+ ## 7. 完整教程(从零做一个手册)
241
+
242
+ **步骤 1:创建项目**
243
+ ```bash
244
+ sapdon create my_guide
245
+ cd my_guide
246
+ ```
247
+
248
+ **步骤 2:在 `main.ts` 里声明手册**
249
+ ```ts
250
+ import { SapdonGuideBook, registry } from '@sapdon/core'
251
+
252
+ const book = new SapdonGuideBook('my_guide:book', [320, 207])
253
+
254
+ book.build([
255
+ {
256
+ id: 'start', title: '开始', icon: 'textures/items/book_writable',
257
+ introLines: ['我的第一本手册。'],
258
+ chapters: [
259
+ { name: '序言', icon: 'textures/items/book_writable', lines: ['欢迎使用 SapdonGuideBook。'] },
260
+ { name: '合成演示', icon: 'textures/items/iron_ingot', pageType: 'crafting',
261
+ craft: { grid: ['textures/items/iron_ingot','','','','','','','',''], output: 'textures/items/iron_ingot' },
262
+ lines: ['一格铁锭 → 输出铁锭。'] },
263
+ ],
264
+ },
265
+ ])
266
+
267
+ registry.submit()
268
+ ```
269
+
270
+ **步骤 3:写运行时路由**(见第 6 节 `scripts/index.ts`),并把 `CATS` / `CAT_CHAPTERS` / `ENT_PAGES` 对齐到你的分类与词条数。
271
+
272
+ **步骤 4:构建 & 进游戏**
273
+ ```bash
274
+ sapdon build ./
275
+ ```
276
+ 进入游戏手持**木棍**右键即可打开手册。
277
+
278
+ **步骤 5(可选):`build.config` 配置**
279
+ ```json
280
+ {
281
+ "buildOptions": {
282
+ "buildEntry": "main.ts",
283
+ "scriptEntry": "scripts/index.ts",
284
+ "scriptOutput": "scripts/index.js",
285
+ "buildMode": "dev",
286
+ "dependencies": [
287
+ { "module_name": "@minecraft/server-ui", "version": "2.1.0" },
288
+ { "module_name": "@minecraft/server", "version": "2.8.0" }
289
+ ]
290
+ }
291
+ }
292
+ ```
293
+ > `dependencies` 里的 `@minecraft/server-ui` 是运行时路由(`ActionFormData`)必需的,别忘了。
294
+
295
+ ---
296
+
297
+ ## 8. 常见问题
298
+
299
+ - **词条文字为空 / 显示异常**:词条名或文案以 `#` 开头会被当作绑定。去掉开头的 `#`。
300
+ - **合成页输出是品红/黑格**:`craft.output` 引用了一个不存在的贴图。换成有效的(如 `textures/items/iron_leggings`)。
301
+ - **文本词条点 next 翻不动**:`ENT_PAGES` 里没给它配页数。`ENT_PAGES[`${catId}_e${gi}`] = Math.ceil(lines.length / 10)`。
302
+ - **想显示字面 `#`**:不要放在字符串开头,如 `form_text 门控`。
@@ -1,5 +1,7 @@
1
1
  # Sapdon UI 页面壳系统 API 参考
2
2
 
3
+ > 按钮组件采用 `FormButton`(纯样式,`@common.button` 无文字)+ `FormButtonGrid`(格盘,注入集合/门控绑定)。背景与踩坑见 `doc/dev/ui-lessons.md`。
4
+
3
5
  Sapdon UI 是一套「sapdon_ui: 前缀标题路由 + 页面壳」的自定义 Server Form UI 系统。它用 TypeScript 声明式生成 `server_form.json` 路由结构与每个页面的独立 UI 文件,运行时通过 `ActionFormData` 的 title 前缀自动分流:`sapdon_ui:` 开头的标题渲染自定义全屏 UI,其它标题走原版原生表单。
4
6
 
5
7
  ---
@@ -9,8 +11,8 @@ Sapdon UI 是一套「sapdon_ui: 前缀标题路由 + 页面壳」的自定义 S
9
11
  1. [架构总览](#1-架构总览)
10
12
  2. [SapdonServerUI 类](#2-sapdonserverui-类)
11
13
  3. [SapdonPanel 类](#3-sapdonpanel-类)
12
- 4. [SapdonButtonPanel 类](#4-sapdonbuttonpanel-类)
13
- 5. [SapdonButton 类](#5-sapdonbutton-类)
14
+ 4. [FormButtonGrid 类](#4-formbuttongrid-类)
15
+ 5. [FormButton 类](#5-formbutton-类)
14
16
  6. [运行时触发](#6-运行时触发)
15
17
  7. [已知注意点](#7-已知注意点)
16
18
 
@@ -22,7 +24,7 @@ Sapdon UI 是一套「sapdon_ui: 前缀标题路由 + 页面壳」的自定义 S
22
24
 
23
25
  | 文件 | 内容 |
24
26
  |------|------|
25
- | `ui/server_form.json` | 路由壳:屏幕、native/自定义分流、页面壳 `custom_panel_content`、按钮模板 `form_button` |
27
+ | `ui/server_form.json` | 路由壳:屏幕、native/自定义分流、扁平化的 `sapdon_long_form_panel`(modifications 注入各注册页) |
26
28
  | `ui/sapdon_ui_xxx.json` | 每个页面一个独立文件(内容面板 + 按键面板) |
27
29
  | `ui/_ui_defs.json` | 自动登记所有 UI 文件 |
28
30
 
@@ -35,9 +37,10 @@ third_party_server_screen@common.base_screen (type: screen)
35
37
  │ visible = title 不含 'sapdon_ui:'
36
38
  └─ sapdon_custom_full@sapdon_screen_content
37
39
  visible = title 含 'sapdon_ui:'
38
- └─ (每个注册页) @custom_panel_content ← $panel_id 精确匹配 title
39
- ├─ content@$user_content_panel ()
40
- └─ buttons@$user_buttons_panel (上, 后绘制覆盖)
40
+ └─ @server_form.sapdon_long_form_panel (size:[fill,fill])
41
+ └─ (modifications controls.insert_back) 每个注册页 Panel ← $panel_id 前缀匹配 title
42
+ ├─ content@$user_content_panel ()
43
+ └─ buttons@$user_buttons_panel (上, 后绘制覆盖)
41
44
  ```
42
45
 
43
46
  ### 脚本 ↔ JSON UI 绑定
@@ -51,7 +54,7 @@ third_party_server_screen@common.base_screen (type: screen)
51
54
 
52
55
  ## 2. SapdonServerUI 类
53
56
 
54
- 负责生成 `server_form.json` 的完整路由壳(屏幕、分流、页面壳、form_button 模板),并提供页面注册入口。
57
+ 负责生成 `server_form.json` 的完整路由壳(屏幕、分流、扁平化的 `sapdon_long_form_panel`),并提供页面注册入口。
55
58
 
56
59
  ```typescript
57
60
  import { SapdonServerUI } from '@sapdon/core'
@@ -78,8 +81,7 @@ SapdonServerUI.registerPage({
78
81
  - `third_party_server_screen@common.base_screen`:`$screen_content` 指向 `custom_full_screen`,附带退出动画抑制与 `menu_cancel → menu_exit` 映射。
79
82
  - `custom_full_screen`:`native_form`(`((#title_text - 'sapdon_ui:') = #title_text)` → 非自定义显示)与 `sapdon_screen_content`(取反)分流。
80
83
  - `sapdon_screen_content`:`sapdon_ui:` 前缀可见,承载所有注册页。
81
- - `custom_panel_content`:通用页面壳,`(#title_text = $panel_id)` 精确匹配,内容(下)+按键(上)两块。
82
- - `form_button@common_buttons.light_text_button`:框架固定提供的表单按钮模板(16×16,集合绑定 `form_buttons`)。
84
+ - `sapdon_long_form_panel`:`main_screen_content` 内(`size:[fill,fill]`),用 `modifications controls.insert_back` **扁平化注入**每个注册页 Panel(`$panel_id` 前缀门控,内容(下)+按键(上)),不再经过 `custom_panel_content` 中壳。
83
85
 
84
86
  ---
85
87
 
@@ -106,48 +108,50 @@ new SapdonPanel("sapdon_ui_apple") // 生成 ui/sapdon_ui_apple.json (ns: s
106
108
 
107
109
  ---
108
110
 
109
- ## 4. SapdonButtonPanel
111
+ ## 4. FormButtonGrid
110
112
 
111
- 构建按键网格面板(grid)。网格通过 `collection_name: form_buttons` 提供每项数据上下文,`place()` 把按钮按 `grid_position` 摆到指定格(内部即 pos_wrap:面板包裹 + `grid_position`)。
113
+ 构建按键格盘(内部一个 `Grid`,`collection_name: form_buttons`)。构造函数必填 `dimensions` + `size`;`addButton(index, btn, pos?)` 逐枚**注入集合/门控绑定**并定位(`FormButton` 只有加进格盘才生效)。
112
114
 
113
115
  ```typescript
114
- import { SapdonButtonPanel, SapdonButton } from '@sapdon/core'
115
-
116
- const buttons = new SapdonButtonPanel("apple_buttons_panel")
117
- .setDimensions([2, 1]) // [列, 行]
118
- .setCollection("form_buttons") // 注入表单按钮集合
119
- .setSize(["100%", "100%"])
120
- .place([0, 0], new SapdonButton("bt0").setAnchor("bottom_left"))
121
- .place([1, 0], new SapdonButton("bt1").setAnchor("bottom_right"))
116
+ import { FormButton, FormButtonGrid } from '@sapdon/core'
117
+
118
+ const buttons = new FormButtonGrid("apple_buttons_grid", { dimensions: [2, 1], size: ["100%", "100%"] })
119
+ .addButton(0, new FormButton("bt0").setAnchor("bottom_left"))
120
+ .addButton(1, new FormButton("bt1").setAnchor("bottom_right"))
122
121
  .build()
123
122
  ```
124
123
 
125
124
  | 方法 | 说明 |
126
125
  |------|------|
127
- | `setDimensions([cols, rows])` | 网格尺寸(**列 × 行**) |
128
- | `setCollection(name)` | 注入集合(表单场景固定 `form_buttons`) |
129
- | `setSize(size)` | 网格尺寸 |
130
- | `place([col, row], element)` | 摆一个按钮/控件到指定格(自动包一层 pos_wrap) |
126
+ | `constructor(id, { dimensions, size })` | `dimensions`=**[列, 行]**、`size`=面板大小,均必填 |
127
+ | `addButton(index, btn, pos?)` | `index` 决定基准格(`index%cols, index/cols`),`pos` 叠加;注入 collection 三件套绑定 |
128
+ | `enableDebug()` | 给每个格子描红调试框 |
131
129
  | `build()` | 返回 `Grid` |
132
130
 
133
- > **布局技巧**:`grid_dimensions [2,1]` 把面板切成左右两份;格子内控件默认左上对齐,改锚点 `bottom_left` / `bottom_right` / `top_right` 即可把按钮贴到对应角。
131
+ > **布局技巧**:`dimensions [2,1]` 把面板切成左右两份;按钮自身用 `setAnchor("bottom_left"/"bottom_right"/"top_right")` 贴到对应角。
134
132
 
135
133
  ---
136
134
 
137
- ## 5. SapdonButton
135
+ ## 5. FormButton
138
136
 
139
- 表单按钮的封装:默认模板固定为 `server_form.form_button`(吃 `form_buttons` 集合数据),无需手写模板引用。
137
+ 表单按钮(纯样式)封装:基底固定 `common.button`(**无文字 label**,绕开 `light_text_button` 的空 `binding_name` 坑),三态纹理由 `setTexture` 提供;集合/门控绑定由 `FormButtonGrid.addButton` 注入。
140
138
 
141
139
  ```typescript
142
- import { SapdonButton } from '@sapdon/core'
140
+ import { FormButton } from '@sapdon/core'
143
141
 
144
- new SapdonButton("bt0") // → bt0@server_form.form_button
145
- .setAnchor("bottom_left") // 锚点对齐,默认 32×32
142
+ new FormButton("bt0")
143
+ .setTexture("textures/ui/..._default", "textures/ui/..._hover", "textures/ui/..._pressed")
144
+ .setBinding("bt0") // 门控键:== #form_button_text 时可见(绑定由 Grid 注入)
145
+ .setAnchor("bottom_left") // 锚点对齐
146
+ .setSize(24, 24)
146
147
  ```
147
148
 
148
149
  | 方法 | 说明 |
149
150
  |------|------|
150
- | `setAnchor(anchor)` | 设置 `anchor_from`/`anchor_to` 对齐(如 `bottom_left`、`bottom_right`、`top_right`),默认尺寸 32×32 |
151
+ | `setTexture(d, h, p)` | 三态纹理(default / hover / pressed) |
152
+ | `setBinding(key)` | 门控键(仅记变量;真正绑定由 `FormButtonGrid` 注入) |
153
+ | `setAnchor(anchor)` | 设置锚点对齐(原地改,保留尺寸/offset) |
154
+ | `setSize(w, h)` | 设置尺寸(原地改,保留锚点/offset) |
151
155
 
152
156
  > 右上角的「退出」这类非集合按钮,请用普通 `Button("exit", "common.button")` + `$pressed_button_name: "button.menu_exit"` 手写,不占用表单按钮集合。
153
157
 
@@ -117,6 +117,5 @@ dev/
117
117
  - [物品教程](./tutorials/item.md) — 学习创建各类物品
118
118
  - [实体教程](./tutorials/entity.md) — 学习创建实体
119
119
  - [方块教程](./tutorials/block.md) — 学习创建方块
120
- - [指南书教程](./tutorials/neo-guidebook.md) — API 用法
121
- - [指南书实战经验](./tutorials/neo-guidebook-experience.md) — 接入流程与踩坑清单
120
+ - [指南书(SapdonGuideBook)](../guidebook.md) — 手册框架类文档与教程
122
121
  - [API 参考](./api/item.md) — 完整的 API 文档
@@ -70,7 +70,7 @@ test_ui/
70
70
 
71
71
  ```typescript
72
72
  import {
73
- Label, Layout, Panel, SapdonButton, SapdonButtonPanel, SapdonPanel,
73
+ Button, FormButton, FormButtonGrid, Label, Layout, Panel, SapdonPanel,
74
74
  SapdonServerUI, StackPanel, Text, registry
75
75
  } from '@sapdon/core'
76
76
  ```
@@ -100,20 +100,17 @@ apple_content_panel.addControl(
100
100
 
101
101
  ## 4. 定义按键面板
102
102
 
103
- 按键面板画在内容面板**上层**。两个表单按钮用 `SapdonButtonPanel`(grid 2×1) 摆到左右下角;右上角放一个普通退出键(不占表单集合)。
103
+ 按键面板画在内容面板**上层**。两个表单按钮用 `FormButtonGrid`(grid 2×1) 摆到左右下角;右上角放一个普通退出键(不占表单集合)。
104
104
 
105
105
  ```typescript
106
- import { Button } from '@sapdon/core'
106
+ import { Button, FormButton, FormButtonGrid } from '@sapdon/core'
107
107
 
108
108
  const apple_buttons_panel = new Panel("apple_buttons_panel")
109
109
  .setLayout(new Layout().setSize(["40%", "40%"]))
110
110
  .addControl(
111
- new SapdonButtonPanel("apple_buttons_grid")
112
- .setDimensions([2, 1]) // 左右两份
113
- .setCollection("form_buttons")
114
- .setSize(["100%", "100%"])
115
- .place([0, 0], new SapdonButton("bt0").setAnchor("bottom_left"))
116
- .place([1, 0], new SapdonButton("bt1").setAnchor("bottom_right"))
111
+ new FormButtonGrid("apple_buttons_grid", { dimensions: [2, 1], size: ["100%", "100%"] })
112
+ .addButton(0, new FormButton("bt0").setAnchor("bottom_left")) // 左边一枚
113
+ .addButton(1, new FormButton("bt1").setAnchor("bottom_right")) // 右边一枚
117
114
  .build()
118
115
  )
119
116
  .addControl(
@@ -125,8 +122,8 @@ const apple_buttons_panel = new Panel("apple_buttons_panel")
125
122
  ```
126
123
 
127
124
  要点:
128
- - `grid_dimensions [2,1]` 把按键面板切成左右两半;格子内默认左上对齐,`setAnchor("bottom_left")` / `("bottom_right")` 把按钮落到两个下角。
129
- - 表单按钮走 `SapdonButton`(自动引用框架模板 `server_form.form_button`,吃 `form_buttons` 集合数据)。
125
+ - `FormButtonGrid` 的 `dimensions [2,1]` 把按键面板切成左右两半;`addButton(index, btn)` 逐枚注入集合/门控绑定并定位,按钮自身用 `setAnchor("bottom_left")` / `("bottom_right")` 落到两个下角。
126
+ - 表单按钮走 `FormButton`(基底 `@common.button` 无文字,纹理用 `setTexture`,门控用 `setBinding`;绑定由格盘注入)。
130
127
  - 退出键用 `common.button` + `button.menu_exit`,点击关闭表单。
131
128
 
132
129
  ---
@@ -190,8 +187,8 @@ npm run build # 构建并复制到开发包目录
190
187
  构建产物(`dev/test_ui_RP/ui/`):
191
188
 
192
189
  ```
193
- server_form.json # 路由壳(custom_full_screen / sapdon_screen_content / custom_panel_content / form_button)
194
- sapdon_ui_apple.json # 页面:内容面板 + 按键面板
190
+ server_form.json # 路由壳(third_party_server_screen / main_screen_content / sapdon_long_form_panel,扁平化注入)
191
+ sapdon_ui_apple.json # 页面:内容面板 + 按键面板(含 FormButtonGrid)
195
192
  _ui_defs.json # 自动登记
196
193
  ```
197
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapdon",
3
- "version": "3.4.0",
3
+ "version": "3.5.1",
4
4
  "scripts": {
5
5
  "build": "node scripts/build.cjs",
6
6
  "test": "tsc && tsc-alias && node --test \"tests/*.test.mjs\"",
package/prod/cli/start.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{program as e}from"commander";import n from"inquirer";import o from"path";import{fileURLToPath as t}from"url";import i from"fs";import{randomUUID as r}from"crypto";import{fileURLToPath as s}from"node:url";import c from"child_process";import a from"fs/promises";import l from"http";import{rollup as u}from"rollup";import p from"@rollup/plugin-commonjs";import{nodeResolve as d}from"@rollup/plugin-node-resolve";import m from"@rollup/plugin-typescript";import{typescriptPaths as f}from"rollup-plugin-typescript-paths";import g from"@rollup/plugin-json";import"rollup-plugin-visualizer";import y from"@rollup/plugin-terser";import h from"os";import j from"lodash";import b from"archiver";const S=()=>r();function v(e,n){try{i.copyFileSync(e,n)}catch(e){console.error("文件复制失败:",e)}}const w=e=>!i.existsSync(e),_=e=>{try{return i.readFileSync(e,"utf8")}catch(e){console.log(e)}return null},O=(e,n)=>{i.mkdirSync(o.dirname(e),{recursive:!0}),i.writeFileSync(e,n)},k=(e,n)=>{if(i.mkdirSync(o.dirname(n),{recursive:!0}),!i.existsSync(e))return void console.log(`Source path ${e} does not exist.`);i.existsSync(n)||i.mkdirSync(n);i.readdirSync(e).forEach(t=>{const r=o.join(e,t),s=o.join(n,t);i.lstatSync(r).isDirectory()?k(r,s):i.copyFileSync(r,s)})};function x(e){const n=s(e.url);return o.dirname(n)}const N=new Map;function $(e,n){let o=N.get(e);return o||(o=n(e),N.set(e,o),o)}function J(){const e=E(),n=o.join(e,"build.config");if(!i.existsSync(n))throw new Error("未找到项目配置文件,请先初始化项目");return $(n,e=>function(e){const n=(o=i.readFileSync(e),JSON.parse(String(o).replace(/\/\/.*|\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/g,"$1")));var o;if(2===n.formatVersion)return n;const t=function({defaultConfig:e,resources:n,scripts:o}){const t={formatVersion:2,buildOptions:{useHMR:!0,buildMode:"dev",buildEntry:e.buildEntry,scriptEntry:e.scriptEntry,scriptOutput:e.scriptEntry.replace(".ts",".js"),useJs:"ts"!==o[0].type,buildDir:e.buildDir,dependencies:e.dependencies,resource:{path:n[0].path,resourceHints:!0}},versionType:"release"};return t}(n);return i.writeFileSync(e,JSON.stringify(t,null,2)),t}(e))}const F=t(import.meta.url),R=o.dirname(F),P={js:"js_sapdon",ts:"ts_sapdon"},D=e=>{const n=o.join(e,"package.json");if(!i.existsSync(n))return null;try{const t=JSON.parse(i.readFileSync(n,"utf-8"));return{name:o.basename(e),description:t.description||"A new sapdon project",author:t.author||"Sapdon",version:t.version||"1.0.0"}}catch(e){return console.error("读取package.json文件时出错:",e),null}},M={};function E(){const e=M.projectPath??process.cwd();if(!i.existsSync(o.join(e,"build.config")))throw new Error("无效的项目路径");return e}function B(){const{buildDir:e}=J().buildOptions,n=$("projectName",()=>o.basename(E()));return o.join(E(),e,n+"_bp")}const H=async(e,n,t="")=>{const i={};try{await a.access(e)}catch{return i}const r=await a.readdir(e);for(const s of r){const r=o.join(e,s);if((await a.stat(r)).isDirectory()){const e=await H(r,n,t);Object.assign(i,e)}else if(s.endsWith(".png")){const e=o.basename(s,".png"),c=`${t}${o.relative(n,r).replace(/\.png$/,"").replace(/\\/g,"/")}`;i[e]={textures:c}}}return i},L=async(e,n,o)=>{try{const t=JSON.stringify(n,null,2);await a.writeFile(e,t),console.log(o)}catch(e){console.error("Error writing JSON file:",e)}};Symbol.metadata||(Symbol.metadata=Symbol("[[metadata]]"));const A=Symbol("isRawJSON");const I=["boolean","number"],C=["string","undefined"];function W(e,n){const o=typeof n;if(null===o)return null;if(C.includes(o))return n;if(I.includes(o))return JSON.rawJSON(n);if("object"===o)return JSON.isRawJSON(n)?n:function(e){return!0===e?.[A]}(n)?JSON.rawJSON(n.rawJSON):n;if("bigint"===o)return JSON.rawJSON(n.toString());throw new Error("Unexpected value")}const T={encode:e=>JSON.stringify(e,W),decode:JSON.parse};const{port:U}={port:49037};const z=new class{cliServerHandlers=new Map;listening=!1;isListening(){return this.listening}bootstrap(){this.listening=!0;const e=l.createServer(async(e,n)=>{const o=this.cliServerHandlers.get((e.url??"/").slice(1));if(o){try{const{promise:n,resolve:t,reject:i}=Promise.withResolvers();let r=Buffer.alloc(0);e.on("data",e=>r=Buffer.concat([r,e])),e.on("end",()=>{try{t(function(e,n=T){return n.decode(e)}(r))}catch(e){i(e)}}),await o(...await n)}catch(e){return console.error(e),n.writeHead(500),void n.end()}n.writeHead(200),n.end()}else n.writeHead(404),n.end()}).listen(U,()=>console.log(`Dev Server listening on port ${U}`));return e.on("error",e=>{throw this.listening=!1,function(e){return"object"==typeof e&&null!==e&&"EADDRINUSE"===e.code}(e)&&(console.error(`[sapdon] Dev Server 端口 ${U} 已被其他 sapdon 进程占用。`),console.error("[sapdon] 请先结束残留的 sapdon 进程,再重新构建,否则本次构建的数据可能被写入错误的包目录。"),process.exit(1)),e}),e}handle(e,n){this.cliServerHandlers.set(e,n)}getHandler(e){return this.cliServerHandlers.get(e)}interceptHandler(e,n){const o=n(this.getHandler(e)??Function.prototype);return this.cliServerHandlers.set(e,o),o}};async function V({level:e,message:n,timeStamp:o,stack:t}){console[e](n,t,`\nat ${new Date(o).toLocaleString()}`)}class G{static dataList=[];static getDataList(){return[...this.dataList]}static startServer(){z.handle("submitGregistry",e=>{this.dataList=e}),z.handle("remote-logger",V)}}const q=async(e,n,t)=>{try{o.join(n,`${t}_BP`);const r=o.join(n,`${t}_RP`),s=(...e)=>o.join(r,...e),c=G.getDataList(),a={item:null,block:null,flipbook:[]},l=[];for(const{name:r,root:s,path:u,data:p}of c){switch(console.log("处理数据:",r,s,u),r){case"item_texture":a.item=p,console.log("用户物品贴图数据:",a.item);continue;case"terrain_texture":a.block=p,console.log("用户方块贴图数据:",a.block);continue;case"flipbook_textures":a.flipbook=p,console.log("用户翻书贴图数据:",a.flipbook);continue}if(p._scriptSource){const n=o.dirname(e),t=o.join(n,u,`${r}.js`);i.existsSync(t)?console.log(`自定义组件脚本已存在,跳过: ${t}`):(O(t,p.source),console.log(`已生成自定义组件脚本: ${t}`)),l.push({safeName:r,componentId:p.componentId});continue}const c="behavior"===s?`${t}_BP`:`${t}_RP`,d=o.join(n,c),m=o.join(d,u,`${r}.json`);O(m,JSON.stringify(p,null,2))}if(l.length>0){const n=o.dirname(e),t=o.join(n,"scripts","custom_components"),r=o.join(t,"index.js");if(i.existsSync(r))console.log(`自定义组件注册索引已存在,跳过: ${r}`);else{const e=l.map(({safeName:e})=>`import { ${e} } from './${e}.js';`),n=l.map(({safeName:e,componentId:n})=>` init.blockComponentRegistry.registerCustomComponent('${n}', ${e});`),o=["// Auto-generated by sapdon.","import { system } from '@minecraft/server';","",...e,"","system.beforeEvents.startup.subscribe((init) => {",...n,"});",""].join("\n");O(r,o),console.log(`已生成自定义组件注册索引: ${r}`)}}await async function(e,n,o){const t=e("textures/items"),i=e("textures/item_texture.json");await async function(e,n,o,t){try{const i=await H(e,e,"textures/items/");t&&Object.assign(i,t);const r={resource_pack_name:o,texture_name:"atlas.items",texture_data:i};await L(n,r,"Item texture JSON file generated successfully.")}catch(e){console.error("Error generating item texture JSON:",e)}}(t,i,n,o.item);const r=e("textures/blocks"),s=e("textures/terrain_texture.json");await async function(e,n,o,t){try{const i=await H(e,e,"textures/blocks/");t&&Object.assign(i,t);const r={texture_name:"atlas.terrain",resource_pack_name:o,padding:8,num_mip_levels:4,texture_data:i};await L(n,r,"Block texture JSON file generated successfully.")}catch(e){console.error("Error generating block texture JSON:",e)}}(r,s,n,o.block);const c=e("textures/flipbook_textures.json");O(c,JSON.stringify(o.flipbook,null,2))}(s,t,a),console.log(`已加载并执行 ${e} 文件!`)}catch(n){console.error(`加载或执行 ${e} 失败:${n.message}`),console.error(n.stack)}};class K{name;description;version;uuid;allow_random_seed;lock_template_options;pack_scope;base_game_version;min_engine_version;constructor(e,n,o,t,i={}){this.name=e,this.description=n,this.version=o,this.uuid=t,this.allow_random_seed=i.allow_random_seed,this.lock_template_options=i.lock_template_options,this.pack_scope=i.pack_scope,this.base_game_version=i.base_game_version,this.min_engine_version=i.min_engine_version}}class Q{description;type;uuid;version;constructor(e,n,o,t){this.description=e,this.type=n,this.uuid=o,this.version=t}}class X{authors;license;generated_with;product_type;url;constructor(e,n,o,t,i){this.authors=e,this.license=n,this.generated_with=o,this.product_type=t,this.url=i}}class Y{format_version;header;modules;dependencies;capabilities;metadata;constructor(e,n,o,t,i=null,r=null){this.format_version=e,this.header=n,this.modules=o,this.dependencies=t,null!=i&&(this.capabilities=i),null!=r&&(this.metadata=r)}}const Z=process.env.MC_PATH,ee=process.env.MC_BETA_PATH,ne="AppData/Roaming/Minecraft Bedrock/Users/Shared/games/com.mojang",oe="AppData/Local/Packages/Microsoft.MinecraftWindowsBeta_8wekyb3d8bbwe/LocalState/games/com.mojang/";function te(){return"beta"===J().versionType?$("McInstallPath.Beta",()=>ee||o.join(h.homedir(),oe)):$("McInstallPath.Main",()=>Z||o.join(h.homedir(),ne))}var ie={name:"sapdon",version:"3.4.0",scripts:{build:"node scripts/build.cjs",test:'tsc && tsc-alias && node --test "tests/*.test.mjs"',"build:demo":"cd examples/block_demo && npm run build",pub:"npm run build && npm publish"},keywords:["bedrock","tools","addon"],author:"Meteage",license:"ISC",type:"module",bin:{sapdon:"prod/cli/start.js"},description:"Sapdon is a Node.js toolkit designed for building Bedrock Edition Minecraft addon packs.",devDependencies:{"@minecraft/server":"^2.0.0-beta.1.21.80-preview.20","@minecraft/server-ui":"^1.3.0","@types/archiver":"^6.0.0","@types/d3-array":"^3.2.1","@types/lodash":"^4.17.16","@types/node":"^22.14.0","tsc-alias":"^1.8.13"},dependencies:{"@rollup/plugin-commonjs":"^28.0.2","@rollup/plugin-json":"^6.1.0","@rollup/plugin-node-resolve":"^16.0.0","@rollup/plugin-terser":"^0.4.4","@rollup/plugin-typescript":"^12.1.2",chalk:"^5.4.1",commander:"^13.0.0","d3-array":"^3.2.4",archiver:"^7.0.0","download-git-repo":"^3.0.2",inquirer:"^12.3.1",lodash:"^4.17.21",ora:"^8.1.1",rollup:"^4.39.0","rollup-plugin-dts":"^6.2.1","rollup-plugin-typescript-paths":"^1.5.0","rollup-plugin-visualizer":"^5.14.0","tsconfig-paths":"^4.2.0",tslib:"^2.8.1",typescript:"^5.8.2"},files:["prod/**/*","doc/**/*"]};function re(){return $(o.join(x(import.meta),"../../../package.json"),()=>ie)}async function se(e,n){const t=J(),r=o.join(e,t.buildOptions.buildDir),s=o.join(r,`${n}_BP`),c=o.join(r,`${n}_RP`);i.cpSync(s,o.join(te(),"development_behavior_packs/",`${n}_BP/`),{recursive:!0,force:!0}),i.cpSync(c,o.join(te(),"development_resource_packs/",`${n}_RP/`),{recursive:!0,force:!0})}async function ce(e){const n=o.join(e,"node_modules"),t=o.join(x(import.meta),"../"),r=o.join(t,"core"),s=o.join(t,"cli"),c=o.join(t,"oc"),a=o.join(n,"@sapdon/core"),l=o.join(n,"@sapdon/cli"),u=o.join(n,"@sapdon/runtime"),p=re();i.cpSync(r,a,{recursive:!0,force:!0}),i.cpSync(s,l,{recursive:!0,force:!0}),i.cpSync(c,u,{recursive:!0,force:!0}),i.writeFileSync(o.join(a,"package.json"),JSON.stringify({name:"@sapdon/core",type:"module",main:"index.js",types:"index.d.ts",version:p.version})),i.writeFileSync(o.join(l,"package.json"),JSON.stringify({name:"@sapdon/cli",type:"module",main:"index.js",types:"index.d.ts",version:p.version})),i.writeFileSync(o.join(u,"package.json"),JSON.stringify({name:"@sapdon/runtime",type:"module",main:"index.js",types:"index.d.ts",version:p.version}))}const ae=t(import.meta.url);o.dirname(ae);const le=["rollup","typescript","@sapdon/core","@sapdon/cli","@minecraft"];function ue(e){const n=(e=>o.join(e,".sapdon_uuid.json"))(e);if(i.existsSync(n))return JSON.parse(i.readFileSync(n,"utf-8"));const t={bp:S(),rp:S()};return i.mkdirSync(o.dirname(n),{recursive:!0}),i.writeFileSync(n,JSON.stringify(t,null,2)),console.log("已生成持久化 UUID 用于 BP/RP 交叉绑定"),t}const pe={js:async(e,n,o)=>{const t=J();try{const i=await u({input:e,plugins:[d({preferBuiltins:!0}),p(),g(),..."prod"===t.buildOptions.buildMode?[y()]:[]],external(e){for(const n of le)if(e.includes(n))return!0;return!1}});await i.write({file:n,format:"esm",sourcemap:o}),i.close()}catch(e){console.error(e)}},ts:async(e,n,t)=>{const i=E(),r=J(),s={file:n,format:"esm",sourcemap:t};try{const n=await u({input:e,plugins:[f(),d({preferBuiltins:!0}),m({tsconfig:o.join(i,"tsconfig.json"),compilerOptions:{outDir:o.dirname(s.file)}}),p(),g(),..."prod"===r.buildOptions.buildMode?[y()]:[]],external(e){for(const n of le)if(e.includes(n))return!0;return!1}});await n.write(s),n.close()}catch(e){console.error(e)}},any:async(e,n)=>{J().buildOptions.useJs?await pe.js(e,n):await pe.ts(e,n)}};async function de(e){const n="."+crypto.randomUUID()+".js",t=o.join(o.dirname(e),".tmp",n);await pe.any(e,t);try{await async function(e){const{promise:n,resolve:o}=Promise.withResolvers();return c.fork(e,{stdio:"inherit"}).on("exit",o),n}(t)}catch(e){console.error(e)}finally{i.rmSync(t,{force:!0})}}function me(e){return console.log("开始构建项目"),console.log("项目路径:"+e),w(e)?(console.log("项目不存在"),!1):!w(o.join(e,"build.config"))||(console.log("项目没有build.config文件"),!1)}const fe=async(e,n)=>{const t=J(),r=o.join(e,"mod.info"),s=JSON.parse(_(r)),c=ge(s.min_engine_version),a=o.join(e,t.buildOptions.buildDir),l=o.join(a,`${n}_BP/`),u=o.join(a,`${n}_RP/`),p=o.join(e,t.buildOptions.buildEntry);if(!z.isListening()){const i=o.join(l,"manifest.json");if(w(i)){const e=ue(a),n=ge(s.version),i=ye(s.name,s.description,s.version,{min_engine_version:c},t.buildOptions.dependencies,t.buildOptions.scriptOutput,e.bp,e.rp,n),r=he(s.name,s.description,s.version,{min_engine_version:c},[],e.bp,e.rp,n);O(o.join(l,"manifest.json"),i),O(o.join(u,"manifest.json"),r)}const r=o.join(e,"pack_icon.png");v(r,o.join(l,"pack_icon.png")),v(r,o.join(u,"pack_icon.png"));const d=t.buildOptions.resource,m=o.join(e,d.path);k(m,u),z.isListening()||z.bootstrap(),G.startServer(),z.handle("submit",async e=>{"debug"!==t.buildOptions.buildMode&&(G.dataList=e,await q(p,a,n))})}await de(p),await async function(e=!1){const n=e?"js":"ts",t=E(),r=J(),{scriptEntry:s,scriptOutput:c,buildMode:a}=r.buildOptions,l=o.join(B(),c);i.mkdirSync(o.dirname(l),{recursive:!0}),await pe[n](o.join(t,s),l,"dev"===a)}(t.buildOptions.useJs),await se(e,n),J().buildOptions.keepServer||(console.log("[sapdon] 构建完成,开发服务器已自动退出。"),console.log('[sapdon] 如需保持服务器常开(HMR / 热更新),请在 build.config 中设置 "buildOptions.keepServer": true'),process.exit(0))};function ge(e){return e.split(".").map(e=>Number(e))}const ye=(e,n,o,t={},i=[],r,s,c,a)=>{console.log("开始生成behavior_packs/manifest.json"),console.log("Entry:",r);const l=new K(e+"_BP",n,a,s,t),u=new Q("行为模块","data",S(),a),p=new Q("脚本模块","script",S(),a);r&&(p.entry=r);const d=new X(["@sapdon"],"MIT",{sapdon:["1.0.0"]},"addon","https://github.com/junjun260/sapdon"),m=new Y(2,l,[u,p],[...i,{uuid:c,version:a}],null,d);return JSON.stringify(m,null,2)},he=(e,n,o,t={},i=[],r,s,c)=>{const a=new K(e+"_RP",n,c,s,t),l=new Q("资源模块","resources",S(),c),u=new X(["@sapdon"],"MIT",{sapdon:["1.0.0"]},"addon","https://github.com/junjun260/sapdon"),p=new Y(2,a,[l],[...i,{uuid:r,version:c}],null,u);return JSON.stringify(p,null,2)},je="import fs from 'fs'\nexport class FileResource {\n static cache: Record<string, FileResource> = {}\n static fileSystemLoader = (uri: string) => fs.readFileSync(uri)\n\n private _res: any\n\n /**\n * 不要调用构造器!\n * 使用 FileResource.get(uri) 方法获取实例\n * @param origin \n */\n constructor(public origin: string) {\n this.origin = origin\n FileResource.cache[origin] = this\n }\n\n /**\n * @param uri \n * @returns {FileResource}\n */\n static get(uri: string) {\n if (uri in FileResource.cache) {\n return FileResource.cache[uri]\n }\n return new FileResource(uri)\n }\n\n load(loader=FileResource.fileSystemLoader) {\n if (this._res) {\n return this._res\n }\n const content = loader(this.origin)\n this._res = content\n return content\n }\n\n clear() {\n delete FileResource.cache[this.origin]\n }\n\n loadWithoutCache(loader=FileResource.fileSystemLoader) {\n return loader(this.origin)\n }\n\n ptr() {\n const func = () => this.load()\n return Object.assign(func, this)\n }\n}",be=j.debounce;function Se(e,n,t){i.readdirSync(e).forEach(r=>{const s=o.join(e,r);i.statSync(s).isDirectory()?(t(r,e),Se(s,n,t)):n(r,e)})}var ve;function we(e){return e.replaceAll(".","_").replaceAll("-","_").replaceAll("@","$")}function _e(e){const n={type:ve.Dir,name:e,children:{}},t={[e]:n};let i=n;return Se(e,(e,n)=>{const t=o.basename(e),r=we(t.slice(0,t.lastIndexOf("."))),s={type:ve.File,name:r,origin:o.join(n,e)};i.children[r]=s},(e,n)=>{const r=we(e),s=t[n],c={type:ve.Dir,name:r,children:{}};s.children[r]=c,t[o.join(n,e)]=c,i=c}),n}function Oe(e){return!!i.existsSync(o.join(e,"build.config"))||(console.log("无法生成资源目录,请 cd 到项目根目录下执行 sapdon res"),!1)}function ke(){const{buildOptions:e}=J();if(e.buildEntry.endsWith("js"))return;const n=process.cwd(),t=o.join(n,"res");if(!Oe(n))return;i.existsSync(t)||i.mkdirSync(t);!function(e,n){const o={};!function e(n,o){for(const t in n.children){const i=n.children[t];if(i.type===ve.File)o[i.name]=`<fn>${i.origin}</fn>`;else{const n={};o[i.name]=n,e(i,n)}}}(e,o);const t=[je].join(";\n"),r=`\n;export default ${JSON.stringify(o,null,2)}`.replace(/"\<fn\>(.*)\<\/fn\>"/g,"FileResource.get('$1').ptr()");i.writeFileSync(n,t+r)}(_e(t),o.join(n,"res.hint.ts"))}!function(e){e[e.File=0]="File",e[e.Dir=1]="Dir"}(ve||(ve={}));const xe=j.debounce;function Ne(e,n){const{buildDir:t,useHMR:r}=J().buildOptions;!function(){const e=process.cwd();Oe(e)&&i.watch(o.join(e,"res"),{recursive:!0},be((e,n)=>{c.execSync("sapdon res")},3e3))}(),r&&i.watch(e,{recursive:!0},xe(async(i,r)=>{i&&r&&(r.startsWith(".")||r.startsWith("node_modules")||r.startsWith(o.join(t,"./"))||r.includes(".tmp")||(r.endsWith(".js")||r.endsWith(".ts")||"build.config"===r||"mod.info"===r)&&(process.stdout.write(`File ${r} changed, reloading...\r`),await fe(e,n),await se(e,n),console.log(`Reloaded ${r}`)))}),1e3)}const $e=t(import.meta.url),Je=o.dirname($e);process.removeAllListeners("warning"),e.command("init").description("初始化一个基于NodeJS的项目").action(()=>{const e=process.cwd(),t=o.join(e,"package.json");let r;if(i.existsSync(t)){try{r=JSON.parse(i.readFileSync(t,"utf-8"))}catch(e){return console.error("读取package.json文件时出错:",e),void console.log("请使改用create命令进行创建。")}r.scripts={...r.scripts,init:"sapdon init",compile:"sapdon compile",config:"sapdon config"};try{i.writeFileSync(t,JSON.stringify(r,null,2),"utf-8")}catch(e){return void console.error("写入package.json文件时出错:",e)}n.prompt([{type:"input",name:"min_engine_version",message:"最低引擎版本:",default:"1.19.50"}]).then(n=>{((e,n)=>{if(!(w(o.join(e,"mod.info"))&&w(o.join(e,"main.mjs"))&&w(o.join(e,"scripts"))&&w(o.join(e,"build.config"))))return void console.log("项目已存在...");const t=o.join(R,"../templates/js_sapdon");k(t,e),O(o.join(e,"mod.info"),JSON.stringify(n,null,2))})(e,{...D(e),...n}),console.log("请使用命令sapdon config配置框架的build.config文件。")})}else console.error("没有找到package.json文件,请使改用create命令进行创建。")}),e.command("create <project-name>").description("Create a new project").action(e=>{n.prompt([{type:"input",name:"name",message:"Project Name:",default:o.basename(e)},{type:"input",name:"description",message:"Project Description:",default:"A new sapdon project"},{type:"input",name:"author",message:"Author Name:",default:"Sapdon"},{type:"input",name:"version",message:"Project Version:",default:"1.0.0"},{type:"input",name:"min_engine_version",message:"Minimum Engine Version:",default:"1.19.50"},{type:"input",name:"language",message:"Language:(js/ts)",default:"ts"}]).then(n=>{const t=o.join(process.cwd(),e);console.log("项目路径:",t),((e,n)=>{if(console.log(1,n),!w(e))return void console.log("项目名称已存在,创建项目目录失败");const t=o.join(R,`../../src/templates/${P[n.language||"js"]}`);k(t,e),O(o.join(e,"mod.info"),JSON.stringify(n,null,2)),c.execSync("npm i",{cwd:e,stdio:"inherit"})})(t,n),ce(t)})}),e.command("build <project-name>").description("Build the project").action(e=>{console.log("Building the project...");const n=o.join(process.cwd(),e),t=o.basename(n);M.projectPath=n,ke(),me(n)&&(fe(n,t),Ne(n,t))}),e.command("compile").description("Compile the current project (build without HMR)").action(()=>{console.log("Compiling the current project...");const e=process.cwd();me(e)&&fe(e,o.basename(e))}),e.command("pack").description("Package build output into .mcaddon file").action(async()=>{const e=process.cwd(),n=o.basename(e);M.projectPath=e,await(async e=>{const n=E(),t=J(),r=o.join(n,t.buildOptions.buildDir),s=o.join(r,`${e}_BP`),c=o.join(r,`${e}_RP`);if(!i.existsSync(s)||!i.existsSync(c))return void console.error("构建输出目录不存在,请先运行 sapdon compile");const a=o.join(r,`${e}.mcaddon`),l=i.createWriteStream(a),u=b("zip",{zlib:{level:9}});l.on("close",()=>{console.log(`打包完成: ${a} (${u.pointer()} bytes)`)}),u.on("error",e=>{throw e}),u.pipe(l),u.directory(s,`${e}_BP`),u.directory(c,`${e}_RP`),await u.finalize()})(n)}),e.command("lib").description("Generate lib files for development server.").action(()=>{ce(process.cwd())}),e.command("res").description("Generate resource hints.").action(()=>{ke()}),e.command("config").description("Configure build.config file").action(()=>{const e=o.join(Je,"./build.config");let n;try{n=JSON.parse(_(e))}catch(e){return void console.error("读取build.config文件时出错:",e)}}),e.parse(e.argv);
2
+ import{program as e}from"commander";import n from"inquirer";import o from"path";import{fileURLToPath as t}from"url";import i from"fs";import{randomUUID as r}from"crypto";import{fileURLToPath as s}from"node:url";import c from"child_process";import a from"fs/promises";import l from"http";import{rollup as u}from"rollup";import p from"@rollup/plugin-commonjs";import{nodeResolve as d}from"@rollup/plugin-node-resolve";import m from"@rollup/plugin-typescript";import{typescriptPaths as f}from"rollup-plugin-typescript-paths";import g from"@rollup/plugin-json";import"rollup-plugin-visualizer";import y from"@rollup/plugin-terser";import h from"os";import j from"lodash";import b from"archiver";const S=()=>r();function v(e,n){try{i.copyFileSync(e,n)}catch(e){console.error("文件复制失败:",e)}}const w=e=>!i.existsSync(e),_=e=>{try{return i.readFileSync(e,"utf8")}catch(e){console.log(e)}return null},O=(e,n)=>{i.mkdirSync(o.dirname(e),{recursive:!0}),i.writeFileSync(e,n)},k=(e,n)=>{if(i.mkdirSync(o.dirname(n),{recursive:!0}),!i.existsSync(e))return void console.log(`Source path ${e} does not exist.`);i.existsSync(n)||i.mkdirSync(n);i.readdirSync(e).forEach(t=>{const r=o.join(e,t),s=o.join(n,t);i.lstatSync(r).isDirectory()?k(r,s):i.copyFileSync(r,s)})};function x(e){const n=s(e.url);return o.dirname(n)}const N=new Map;function $(e,n){let o=N.get(e);return o||(o=n(e),N.set(e,o),o)}function J(){const e=E(),n=o.join(e,"build.config");if(!i.existsSync(n))throw new Error("未找到项目配置文件,请先初始化项目");return $(n,e=>function(e){const n=(o=i.readFileSync(e),JSON.parse(String(o).replace(/\/\/.*|\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/g,"$1")));var o;if(2===n.formatVersion)return n;const t=function({defaultConfig:e,resources:n,scripts:o}){const t={formatVersion:2,buildOptions:{useHMR:!0,buildMode:"dev",buildEntry:e.buildEntry,scriptEntry:e.scriptEntry,scriptOutput:e.scriptEntry.replace(".ts",".js"),useJs:"ts"!==o[0].type,buildDir:e.buildDir,dependencies:e.dependencies,resource:{path:n[0].path,resourceHints:!0}},versionType:"release"};return t}(n);return i.writeFileSync(e,JSON.stringify(t,null,2)),t}(e))}const F=t(import.meta.url),R=o.dirname(F),P={js:"js_sapdon",ts:"ts_sapdon"},D=e=>{const n=o.join(e,"package.json");if(!i.existsSync(n))return null;try{const t=JSON.parse(i.readFileSync(n,"utf-8"));return{name:o.basename(e),description:t.description||"A new sapdon project",author:t.author||"Sapdon",version:t.version||"1.0.0"}}catch(e){return console.error("读取package.json文件时出错:",e),null}},M={};function E(){const e=M.projectPath??process.cwd();if(!i.existsSync(o.join(e,"build.config")))throw new Error("无效的项目路径");return e}function B(){const{buildDir:e}=J().buildOptions,n=$("projectName",()=>o.basename(E()));return o.join(E(),e,n+"_bp")}const H=async(e,n,t="")=>{const i={};try{await a.access(e)}catch{return i}const r=await a.readdir(e);for(const s of r){const r=o.join(e,s);if((await a.stat(r)).isDirectory()){const e=await H(r,n,t);Object.assign(i,e)}else if(s.endsWith(".png")){const e=o.basename(s,".png"),c=`${t}${o.relative(n,r).replace(/\.png$/,"").replace(/\\/g,"/")}`;i[e]={textures:c}}}return i},L=async(e,n,o)=>{try{const t=JSON.stringify(n,null,2);await a.writeFile(e,t),console.log(o)}catch(e){console.error("Error writing JSON file:",e)}};Symbol.metadata||(Symbol.metadata=Symbol("[[metadata]]"));const A=Symbol("isRawJSON");const I=["boolean","number"],C=["string","undefined"];function W(e,n){const o=typeof n;if(null===o)return null;if(C.includes(o))return n;if(I.includes(o))return JSON.rawJSON(n);if("object"===o)return JSON.isRawJSON(n)?n:function(e){return!0===e?.[A]}(n)?JSON.rawJSON(n.rawJSON):n;if("bigint"===o)return JSON.rawJSON(n.toString());throw new Error("Unexpected value")}const T={encode:e=>JSON.stringify(e,W),decode:JSON.parse};const{port:U}={port:49037};const z=new class{cliServerHandlers=new Map;listening=!1;isListening(){return this.listening}bootstrap(){this.listening=!0;const e=l.createServer(async(e,n)=>{const o=this.cliServerHandlers.get((e.url??"/").slice(1));if(o){try{const{promise:n,resolve:t,reject:i}=Promise.withResolvers();let r=Buffer.alloc(0);e.on("data",e=>r=Buffer.concat([r,e])),e.on("end",()=>{try{t(function(e,n=T){return n.decode(e)}(r))}catch(e){i(e)}}),await o(...await n)}catch(e){return console.error(e),n.writeHead(500),void n.end()}n.writeHead(200),n.end()}else n.writeHead(404),n.end()}).listen(U,()=>console.log(`Dev Server listening on port ${U}`));return e.on("error",e=>{throw this.listening=!1,function(e){return"object"==typeof e&&null!==e&&"EADDRINUSE"===e.code}(e)&&(console.error(`[sapdon] Dev Server 端口 ${U} 已被其他 sapdon 进程占用。`),console.error("[sapdon] 请先结束残留的 sapdon 进程,再重新构建,否则本次构建的数据可能被写入错误的包目录。"),process.exit(1)),e}),e}handle(e,n){this.cliServerHandlers.set(e,n)}getHandler(e){return this.cliServerHandlers.get(e)}interceptHandler(e,n){const o=n(this.getHandler(e)??Function.prototype);return this.cliServerHandlers.set(e,o),o}};async function V({level:e,message:n,timeStamp:o,stack:t}){console[e](n,t,`\nat ${new Date(o).toLocaleString()}`)}class G{static dataList=[];static getDataList(){return[...this.dataList]}static startServer(){z.handle("submitGregistry",e=>{this.dataList=e}),z.handle("remote-logger",V)}}const q=async(e,n,t)=>{try{o.join(n,`${t}_BP`);const r=o.join(n,`${t}_RP`),s=(...e)=>o.join(r,...e),c=G.getDataList(),a={item:null,block:null,flipbook:[]},l=[];for(const{name:r,root:s,path:u,data:p}of c){switch(console.log("处理数据:",r,s,u),r){case"item_texture":a.item=p,console.log("用户物品贴图数据:",a.item);continue;case"terrain_texture":a.block=p,console.log("用户方块贴图数据:",a.block);continue;case"flipbook_textures":a.flipbook=p,console.log("用户翻书贴图数据:",a.flipbook);continue}if(p._scriptSource){const n=o.dirname(e),t=o.join(n,u,`${r}.js`);i.existsSync(t)?console.log(`自定义组件脚本已存在,跳过: ${t}`):(O(t,p.source),console.log(`已生成自定义组件脚本: ${t}`)),l.push({safeName:r,componentId:p.componentId});continue}const c="behavior"===s?`${t}_BP`:`${t}_RP`,d=o.join(n,c),m=o.join(d,u,`${r}.json`);O(m,JSON.stringify(p,null,2))}if(l.length>0){const n=o.dirname(e),t=o.join(n,"scripts","custom_components"),r=o.join(t,"index.js");if(i.existsSync(r))console.log(`自定义组件注册索引已存在,跳过: ${r}`);else{const e=l.map(({safeName:e})=>`import { ${e} } from './${e}.js';`),n=l.map(({safeName:e,componentId:n})=>` init.blockComponentRegistry.registerCustomComponent('${n}', ${e});`),o=["// Auto-generated by sapdon.","import { system } from '@minecraft/server';","",...e,"","system.beforeEvents.startup.subscribe((init) => {",...n,"});",""].join("\n");O(r,o),console.log(`已生成自定义组件注册索引: ${r}`)}}await async function(e,n,o){const t=e("textures/items"),i=e("textures/item_texture.json");await async function(e,n,o,t){try{const i=await H(e,e,"textures/items/");t&&Object.assign(i,t);const r={resource_pack_name:o,texture_name:"atlas.items",texture_data:i};await L(n,r,"Item texture JSON file generated successfully.")}catch(e){console.error("Error generating item texture JSON:",e)}}(t,i,n,o.item);const r=e("textures/blocks"),s=e("textures/terrain_texture.json");await async function(e,n,o,t){try{const i=await H(e,e,"textures/blocks/");t&&Object.assign(i,t);const r={texture_name:"atlas.terrain",resource_pack_name:o,padding:8,num_mip_levels:4,texture_data:i};await L(n,r,"Block texture JSON file generated successfully.")}catch(e){console.error("Error generating block texture JSON:",e)}}(r,s,n,o.block);const c=e("textures/flipbook_textures.json");O(c,JSON.stringify(o.flipbook,null,2))}(s,t,a),console.log(`已加载并执行 ${e} 文件!`)}catch(n){console.error(`加载或执行 ${e} 失败:${n.message}`),console.error(n.stack)}};class K{name;description;version;uuid;allow_random_seed;lock_template_options;pack_scope;base_game_version;min_engine_version;constructor(e,n,o,t,i={}){this.name=e,this.description=n,this.version=o,this.uuid=t,this.allow_random_seed=i.allow_random_seed,this.lock_template_options=i.lock_template_options,this.pack_scope=i.pack_scope,this.base_game_version=i.base_game_version,this.min_engine_version=i.min_engine_version}}class Q{description;type;uuid;version;constructor(e,n,o,t){this.description=e,this.type=n,this.uuid=o,this.version=t}}class X{authors;license;generated_with;product_type;url;constructor(e,n,o,t,i){this.authors=e,this.license=n,this.generated_with=o,this.product_type=t,this.url=i}}class Y{format_version;header;modules;dependencies;capabilities;metadata;constructor(e,n,o,t,i=null,r=null){this.format_version=e,this.header=n,this.modules=o,this.dependencies=t,null!=i&&(this.capabilities=i),null!=r&&(this.metadata=r)}}const Z=process.env.MC_PATH,ee=process.env.MC_BETA_PATH,ne="AppData/Roaming/Minecraft Bedrock/Users/Shared/games/com.mojang",oe="AppData/Local/Packages/Microsoft.MinecraftWindowsBeta_8wekyb3d8bbwe/LocalState/games/com.mojang/";function te(){return"beta"===J().versionType?$("McInstallPath.Beta",()=>ee||o.join(h.homedir(),oe)):$("McInstallPath.Main",()=>Z||o.join(h.homedir(),ne))}var ie={name:"sapdon",version:"3.5.0",scripts:{build:"node scripts/build.cjs",test:'tsc && tsc-alias && node --test "tests/*.test.mjs"',"build:demo":"cd examples/block_demo && npm run build",pub:"npm run build && npm publish"},keywords:["bedrock","tools","addon"],author:"Meteage",license:"ISC",type:"module",bin:{sapdon:"prod/cli/start.js"},description:"Sapdon is a Node.js toolkit designed for building Bedrock Edition Minecraft addon packs.",devDependencies:{"@minecraft/server":"^2.0.0-beta.1.21.80-preview.20","@minecraft/server-ui":"^1.3.0","@types/archiver":"^6.0.0","@types/d3-array":"^3.2.1","@types/lodash":"^4.17.16","@types/node":"^22.14.0","tsc-alias":"^1.8.13"},dependencies:{"@rollup/plugin-commonjs":"^28.0.2","@rollup/plugin-json":"^6.1.0","@rollup/plugin-node-resolve":"^16.0.0","@rollup/plugin-terser":"^0.4.4","@rollup/plugin-typescript":"^12.1.2",chalk:"^5.4.1",commander:"^13.0.0","d3-array":"^3.2.4",archiver:"^7.0.0","download-git-repo":"^3.0.2",inquirer:"^12.3.1",lodash:"^4.17.21",ora:"^8.1.1",rollup:"^4.39.0","rollup-plugin-dts":"^6.2.1","rollup-plugin-typescript-paths":"^1.5.0","rollup-plugin-visualizer":"^5.14.0","tsconfig-paths":"^4.2.0",tslib:"^2.8.1",typescript:"^5.8.2"},files:["prod/**/*","doc/**/*"]};function re(){return $(o.join(x(import.meta),"../../../package.json"),()=>ie)}async function se(e,n){const t=J(),r=o.join(e,t.buildOptions.buildDir),s=o.join(r,`${n}_BP`),c=o.join(r,`${n}_RP`);i.cpSync(s,o.join(te(),"development_behavior_packs/",`${n}_BP/`),{recursive:!0,force:!0}),i.cpSync(c,o.join(te(),"development_resource_packs/",`${n}_RP/`),{recursive:!0,force:!0})}async function ce(e){const n=o.join(e,"node_modules"),t=o.join(x(import.meta),"../"),r=o.join(t,"core"),s=o.join(t,"cli"),c=o.join(t,"oc"),a=o.join(n,"@sapdon/core"),l=o.join(n,"@sapdon/cli"),u=o.join(n,"@sapdon/runtime"),p=re();i.cpSync(r,a,{recursive:!0,force:!0}),i.cpSync(s,l,{recursive:!0,force:!0}),i.cpSync(c,u,{recursive:!0,force:!0}),i.writeFileSync(o.join(a,"package.json"),JSON.stringify({name:"@sapdon/core",type:"module",main:"index.js",types:"index.d.ts",version:p.version})),i.writeFileSync(o.join(l,"package.json"),JSON.stringify({name:"@sapdon/cli",type:"module",main:"index.js",types:"index.d.ts",version:p.version})),i.writeFileSync(o.join(u,"package.json"),JSON.stringify({name:"@sapdon/runtime",type:"module",main:"index.js",types:"index.d.ts",version:p.version}))}const ae=t(import.meta.url);o.dirname(ae);const le=["rollup","typescript","@sapdon/core","@sapdon/cli","@minecraft"];function ue(e){const n=(e=>o.join(e,".sapdon_uuid.json"))(e);if(i.existsSync(n))return JSON.parse(i.readFileSync(n,"utf-8"));const t={bp:S(),rp:S()};return i.mkdirSync(o.dirname(n),{recursive:!0}),i.writeFileSync(n,JSON.stringify(t,null,2)),console.log("已生成持久化 UUID 用于 BP/RP 交叉绑定"),t}const pe={js:async(e,n,o)=>{const t=J();try{const i=await u({input:e,plugins:[d({preferBuiltins:!0}),p(),g(),..."prod"===t.buildOptions.buildMode?[y()]:[]],external(e){for(const n of le)if(e.includes(n))return!0;return!1}});await i.write({file:n,format:"esm",sourcemap:o}),i.close()}catch(e){console.error(e)}},ts:async(e,n,t)=>{const i=E(),r=J(),s={file:n,format:"esm",sourcemap:t};try{const n=await u({input:e,plugins:[f(),d({preferBuiltins:!0}),m({tsconfig:o.join(i,"tsconfig.json"),compilerOptions:{outDir:o.dirname(s.file)}}),p(),g(),..."prod"===r.buildOptions.buildMode?[y()]:[]],external(e){for(const n of le)if(e.includes(n))return!0;return!1}});await n.write(s),n.close()}catch(e){console.error(e)}},any:async(e,n)=>{J().buildOptions.useJs?await pe.js(e,n):await pe.ts(e,n)}};async function de(e){const n="."+crypto.randomUUID()+".js",t=o.join(o.dirname(e),".tmp",n);await pe.any(e,t);try{await async function(e){const{promise:n,resolve:o}=Promise.withResolvers();return c.fork(e,{stdio:"inherit"}).on("exit",o),n}(t)}catch(e){console.error(e)}finally{i.rmSync(t,{force:!0})}}function me(e){return console.log("开始构建项目"),console.log("项目路径:"+e),w(e)?(console.log("项目不存在"),!1):!w(o.join(e,"build.config"))||(console.log("项目没有build.config文件"),!1)}const fe=async(e,n)=>{const t=J(),r=o.join(e,"mod.info"),s=JSON.parse(_(r)),c=ge(s.min_engine_version),a=o.join(e,t.buildOptions.buildDir),l=o.join(a,`${n}_BP/`),u=o.join(a,`${n}_RP/`),p=o.join(e,t.buildOptions.buildEntry);if(!z.isListening()){const i=o.join(l,"manifest.json");if(w(i)){const e=ue(a),n=ge(s.version),i=ye(s.name,s.description,s.version,{min_engine_version:c},t.buildOptions.dependencies,t.buildOptions.scriptOutput,e.bp,e.rp,n),r=he(s.name,s.description,s.version,{min_engine_version:c},[],e.bp,e.rp,n);O(o.join(l,"manifest.json"),i),O(o.join(u,"manifest.json"),r)}const r=o.join(e,"pack_icon.png");v(r,o.join(l,"pack_icon.png")),v(r,o.join(u,"pack_icon.png"));const d=t.buildOptions.resource,m=o.join(e,d.path);k(m,u),z.isListening()||z.bootstrap(),G.startServer(),z.handle("submit",async e=>{"debug"!==t.buildOptions.buildMode&&(G.dataList=e,await q(p,a,n))})}await de(p),await async function(e=!1){const n=e?"js":"ts",t=E(),r=J(),{scriptEntry:s,scriptOutput:c,buildMode:a}=r.buildOptions,l=o.join(B(),c);i.mkdirSync(o.dirname(l),{recursive:!0}),await pe[n](o.join(t,s),l,"dev"===a)}(t.buildOptions.useJs),await se(e,n),J().buildOptions.keepServer||(console.log("[sapdon] 构建完成,开发服务器已自动退出。"),console.log('[sapdon] 如需保持服务器常开(HMR / 热更新),请在 build.config 中设置 "buildOptions.keepServer": true'),process.exit(0))};function ge(e){return e.split(".").map(e=>Number(e))}const ye=(e,n,o,t={},i=[],r,s,c,a)=>{console.log("开始生成behavior_packs/manifest.json"),console.log("Entry:",r);const l=new K(e+"_BP",n,a,s,t),u=new Q("行为模块","data",S(),a),p=new Q("脚本模块","script",S(),a);r&&(p.entry=r);const d=new X(["@sapdon"],"MIT",{sapdon:["1.0.0"]},"addon","https://github.com/junjun260/sapdon"),m=new Y(2,l,[u,p],[...i,{uuid:c,version:a}],null,d);return JSON.stringify(m,null,2)},he=(e,n,o,t={},i=[],r,s,c)=>{const a=new K(e+"_RP",n,c,s,t),l=new Q("资源模块","resources",S(),c),u=new X(["@sapdon"],"MIT",{sapdon:["1.0.0"]},"addon","https://github.com/junjun260/sapdon"),p=new Y(2,a,[l],[...i,{uuid:r,version:c}],null,u);return JSON.stringify(p,null,2)},je="import fs from 'fs'\nexport class FileResource {\n static cache: Record<string, FileResource> = {}\n static fileSystemLoader = (uri: string) => fs.readFileSync(uri)\n\n private _res: any\n\n /**\n * 不要调用构造器!\n * 使用 FileResource.get(uri) 方法获取实例\n * @param origin \n */\n constructor(public origin: string) {\n this.origin = origin\n FileResource.cache[origin] = this\n }\n\n /**\n * @param uri \n * @returns {FileResource}\n */\n static get(uri: string) {\n if (uri in FileResource.cache) {\n return FileResource.cache[uri]\n }\n return new FileResource(uri)\n }\n\n load(loader=FileResource.fileSystemLoader) {\n if (this._res) {\n return this._res\n }\n const content = loader(this.origin)\n this._res = content\n return content\n }\n\n clear() {\n delete FileResource.cache[this.origin]\n }\n\n loadWithoutCache(loader=FileResource.fileSystemLoader) {\n return loader(this.origin)\n }\n\n ptr() {\n const func = () => this.load()\n return Object.assign(func, this)\n }\n}",be=j.debounce;function Se(e,n,t){i.readdirSync(e).forEach(r=>{const s=o.join(e,r);i.statSync(s).isDirectory()?(t(r,e),Se(s,n,t)):n(r,e)})}var ve;function we(e){return e.replaceAll(".","_").replaceAll("-","_").replaceAll("@","$")}function _e(e){const n={type:ve.Dir,name:e,children:{}},t={[e]:n};let i=n;return Se(e,(e,n)=>{const t=o.basename(e),r=we(t.slice(0,t.lastIndexOf("."))),s={type:ve.File,name:r,origin:o.join(n,e)};i.children[r]=s},(e,n)=>{const r=we(e),s=t[n],c={type:ve.Dir,name:r,children:{}};s.children[r]=c,t[o.join(n,e)]=c,i=c}),n}function Oe(e){return!!i.existsSync(o.join(e,"build.config"))||(console.log("无法生成资源目录,请 cd 到项目根目录下执行 sapdon res"),!1)}function ke(){const{buildOptions:e}=J();if(e.buildEntry.endsWith("js"))return;const n=process.cwd(),t=o.join(n,"res");if(!Oe(n))return;i.existsSync(t)||i.mkdirSync(t);!function(e,n){const o={};!function e(n,o){for(const t in n.children){const i=n.children[t];if(i.type===ve.File)o[i.name]=`<fn>${i.origin}</fn>`;else{const n={};o[i.name]=n,e(i,n)}}}(e,o);const t=[je].join(";\n"),r=`\n;export default ${JSON.stringify(o,null,2)}`.replace(/"\<fn\>(.*)\<\/fn\>"/g,"FileResource.get('$1').ptr()");i.writeFileSync(n,t+r)}(_e(t),o.join(n,"res.hint.ts"))}!function(e){e[e.File=0]="File",e[e.Dir=1]="Dir"}(ve||(ve={}));const xe=j.debounce;function Ne(e,n){const{buildDir:t,useHMR:r}=J().buildOptions;!function(){const e=process.cwd();Oe(e)&&i.watch(o.join(e,"res"),{recursive:!0},be((e,n)=>{c.execSync("sapdon res")},3e3))}(),r&&i.watch(e,{recursive:!0},xe(async(i,r)=>{i&&r&&(r.startsWith(".")||r.startsWith("node_modules")||r.startsWith(o.join(t,"./"))||r.includes(".tmp")||(r.endsWith(".js")||r.endsWith(".ts")||"build.config"===r||"mod.info"===r)&&(process.stdout.write(`File ${r} changed, reloading...\r`),await fe(e,n),await se(e,n),console.log(`Reloaded ${r}`)))}),1e3)}const $e=t(import.meta.url),Je=o.dirname($e);process.removeAllListeners("warning"),e.command("init").description("初始化一个基于NodeJS的项目").action(()=>{const e=process.cwd(),t=o.join(e,"package.json");let r;if(i.existsSync(t)){try{r=JSON.parse(i.readFileSync(t,"utf-8"))}catch(e){return console.error("读取package.json文件时出错:",e),void console.log("请使改用create命令进行创建。")}r.scripts={...r.scripts,init:"sapdon init",compile:"sapdon compile",config:"sapdon config"};try{i.writeFileSync(t,JSON.stringify(r,null,2),"utf-8")}catch(e){return void console.error("写入package.json文件时出错:",e)}n.prompt([{type:"input",name:"min_engine_version",message:"最低引擎版本:",default:"1.19.50"}]).then(n=>{((e,n)=>{if(!(w(o.join(e,"mod.info"))&&w(o.join(e,"main.mjs"))&&w(o.join(e,"scripts"))&&w(o.join(e,"build.config"))))return void console.log("项目已存在...");const t=o.join(R,"../templates/js_sapdon");k(t,e),O(o.join(e,"mod.info"),JSON.stringify(n,null,2))})(e,{...D(e),...n}),console.log("请使用命令sapdon config配置框架的build.config文件。")})}else console.error("没有找到package.json文件,请使改用create命令进行创建。")}),e.command("create <project-name>").description("Create a new project").action(e=>{n.prompt([{type:"input",name:"name",message:"Project Name:",default:o.basename(e)},{type:"input",name:"description",message:"Project Description:",default:"A new sapdon project"},{type:"input",name:"author",message:"Author Name:",default:"Sapdon"},{type:"input",name:"version",message:"Project Version:",default:"1.0.0"},{type:"input",name:"min_engine_version",message:"Minimum Engine Version:",default:"1.19.50"},{type:"input",name:"language",message:"Language:(js/ts)",default:"ts"}]).then(n=>{const t=o.join(process.cwd(),e);console.log("项目路径:",t),((e,n)=>{if(console.log(1,n),!w(e))return void console.log("项目名称已存在,创建项目目录失败");const t=o.join(R,`../../src/templates/${P[n.language||"js"]}`);k(t,e),O(o.join(e,"mod.info"),JSON.stringify(n,null,2)),c.execSync("npm i",{cwd:e,stdio:"inherit"})})(t,n),ce(t)})}),e.command("build <project-name>").description("Build the project").action(e=>{console.log("Building the project...");const n=o.join(process.cwd(),e),t=o.basename(n);M.projectPath=n,ke(),me(n)&&(fe(n,t),Ne(n,t))}),e.command("compile").description("Compile the current project (build without HMR)").action(()=>{console.log("Compiling the current project...");const e=process.cwd();me(e)&&fe(e,o.basename(e))}),e.command("pack").description("Package build output into .mcaddon file").action(async()=>{const e=process.cwd(),n=o.basename(e);M.projectPath=e,await(async e=>{const n=E(),t=J(),r=o.join(n,t.buildOptions.buildDir),s=o.join(r,`${e}_BP`),c=o.join(r,`${e}_RP`);if(!i.existsSync(s)||!i.existsSync(c))return void console.error("构建输出目录不存在,请先运行 sapdon compile");const a=o.join(r,`${e}.mcaddon`),l=i.createWriteStream(a),u=b("zip",{zlib:{level:9}});l.on("close",()=>{console.log(`打包完成: ${a} (${u.pointer()} bytes)`)}),u.on("error",e=>{throw e}),u.pipe(l),u.directory(s,`${e}_BP`),u.directory(c,`${e}_RP`),await u.finalize()})(n)}),e.command("lib").description("Generate lib files for development server.").action(()=>{ce(process.cwd())}),e.command("res").description("Generate resource hints.").action(()=>{ke()}),e.command("config").description("Configure build.config file").action(()=>{const e=o.join(Je,"./build.config");let n;try{n=JSON.parse(_(e))}catch(e){return void console.error("读取build.config文件时出错:",e)}}),e.parse(e.argv);