modsmith 0.6.2 → 0.7.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.
- package/AGENTS.md +6 -0
- package/README.md +131 -20
- package/bin/modsmith.mjs +573 -405
- package/docs/conformance/README.md +62 -0
- package/docs/conformance/farmers-delight-1.21.1-3.3.6.json +209 -0
- package/docs/conformance/farmers-delight-dev-live.json +321 -0
- package/docs/conformance/moredelight-26.04.30-1.21.json +47 -0
- package/docs/conformance/supplementaries-1.21.1-4.0.2.json +227 -0
- package/docs/conformance/twilightforest-1.21.1-4.8.796.json +317 -0
- package/package.json +16 -3
- package/schema/manifest.schema.json +7 -0
- package/sidesheets/farmers-delight-3.3.6.crops.json +42 -42
- package/src/block-shapes.mjs +176 -0
- package/src/calibrate.mjs +1 -0
- package/src/conformance-live.mjs +208 -0
- package/src/conformance.mjs +366 -0
- package/src/cortico-bridge.mjs +397 -325
- package/src/crops.mjs +246 -227
- package/src/import-dump.mjs +139 -0
- package/src/index.mjs +3 -1
- package/src/inspect.mjs +466 -461
- package/src/manifest-io.mjs +1 -1
- package/src/onboard.mjs +21 -3
- package/src/probe.mjs +325 -316
- package/src/rcon.mjs +14 -8
- package/src/server-fingerprint.mjs +5 -5
- package/src/stations.mjs +22 -1
- package/src/validate.mjs +231 -223
- package/src/verify.mjs +18 -10
- package/templates/reference-bot/README.md +22 -0
- package/templates/reference-bot/index.mjs +59 -0
- package/templates/registry-dump/README.md +48 -0
- package/templates/registry-dump/build.gradle +39 -0
- package/templates/registry-dump/gradle.properties +2 -0
- package/templates/registry-dump/modsmith-dump-0.1.2.jar +0 -0
- package/templates/registry-dump/settings.gradle +9 -0
- package/templates/registry-dump/src/main/java/dev/modsmith/dump/RegistryDumpMod.java +98 -0
- package/templates/registry-dump/src/main/resources/fabric.mod.json +18 -0
- package/types/index.d.ts +174 -135
package/src/inspect.mjs
CHANGED
|
@@ -1,461 +1,466 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 从 mod jar 里静态提取语义 manifest。
|
|
3
|
-
*
|
|
4
|
-
* 这一层**没有 LLM**,也不认识任何具体的 mod:命名空间、方块、物品、配方、tag 图、
|
|
5
|
-
* 中文名全部从 jar 里读。唯一的推断是作物年龄上限(见 crops.mjs)。
|
|
6
|
-
*
|
|
7
|
-
* 拿不到的东西(站点用法、玩法说明)留给 `learn` 阶段,拿不到的数字 id 留给
|
|
8
|
-
* `calibrate` 阶段——静态分析有边界,这是明说的。
|
|
9
|
-
*/
|
|
10
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import { Zip } from './zip.mjs';
|
|
13
|
-
import { inferCrops } from './crops.mjs';
|
|
14
|
-
import { resolveMissingTags } from './tag-fallback.mjs';
|
|
15
|
-
import { containerCandidates } from './containers.mjs';
|
|
16
|
-
import { compareVersions, modVersionOf } from './version.mjs';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
for (const v of
|
|
311
|
-
if (v
|
|
312
|
-
else
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// ----
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
// ----
|
|
337
|
-
const {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 从 mod jar 里静态提取语义 manifest。
|
|
3
|
+
*
|
|
4
|
+
* 这一层**没有 LLM**,也不认识任何具体的 mod:命名空间、方块、物品、配方、tag 图、
|
|
5
|
+
* 中文名全部从 jar 里读。唯一的推断是作物年龄上限(见 crops.mjs)。
|
|
6
|
+
*
|
|
7
|
+
* 拿不到的东西(站点用法、玩法说明)留给 `learn` 阶段,拿不到的数字 id 留给
|
|
8
|
+
* `calibrate` 阶段——静态分析有边界,这是明说的。
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { Zip } from './zip.mjs';
|
|
13
|
+
import { inferCrops } from './crops.mjs';
|
|
14
|
+
import { resolveMissingTags } from './tag-fallback.mjs';
|
|
15
|
+
import { containerCandidates } from './containers.mjs';
|
|
16
|
+
import { compareVersions, modVersionOf } from './version.mjs';
|
|
17
|
+
import { boxForRef } from './block-shapes.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 包名:宿主配置里的 `worlds.minecraft.mods.pack` 要逐字对上这个字符串。
|
|
21
|
+
*
|
|
22
|
+
* 默认 `${mod id}-${MC 版本}-${mod 版本}`。**以 id 为准而不是显示名**:id 稳定,
|
|
23
|
+
* 显示名带空格和撇号("Farmer's Delight"),推出来的东西不可预测。
|
|
24
|
+
*
|
|
25
|
+
* 代价是 id 不带连字符的 mod 算出来跟"手改过的部署名"不一定一致(`farmersdelight-...`
|
|
26
|
+
* vs 部署里的 `farmers-delight-...`)。这种情况下用 `--pack` 指过去 —— 显式优先,
|
|
27
|
+
* 而且 `inspect` 发现同目录已有同 mod 的旧清单时会直接告诉你该用哪个名字。
|
|
28
|
+
*/
|
|
29
|
+
export function packNameOf(modId, version, opts = {}) {
|
|
30
|
+
if (opts.pack) return String(opts.pack);
|
|
31
|
+
const id = String(modId).replace(/_/g, '-');
|
|
32
|
+
const mc = String(opts.mcVersion ?? '1.21.1').trim();
|
|
33
|
+
const ver = String(version ?? 'unknown').replace(/[^\w.\-]/g, '_');
|
|
34
|
+
return mc ? `${id}-${mc}-${ver}` : `${id}-${ver}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 目录里有没有同一个 mod 的另一个包名(返回文件名,不含 .json)。 */
|
|
38
|
+
/**
|
|
39
|
+
* `models/item/` 下的**变体模型**不是物品,别写进清单。
|
|
40
|
+
*
|
|
41
|
+
* 1.21 的护甲 trim 是模型变体(`ironwood_chestplate_gold_trim.json`),不是注册物品;
|
|
42
|
+
* 部分 mod 的巨人方块还有 `_base` / `_gui` 这种贴图模型。真实事故:暮色森林 983 个
|
|
43
|
+
* "物品"里 265 个是这类模型文件,`verify` 报"服务端不认这个物品"——清单里多一个
|
|
44
|
+
* 不存在的物品比少一个更糟(机器人照着给物品会一直失败)。
|
|
45
|
+
*/
|
|
46
|
+
export const isItemModelVariant = (name) => /_[a-z]+_trim$|_(base|gui|inventory|overlay|mask)$/.test(name);
|
|
47
|
+
|
|
48
|
+
export function findExistingPack(dir, mod, selfName) {
|
|
49
|
+
try {
|
|
50
|
+
for (const f of readdirSync(dir)) {
|
|
51
|
+
if (!f.endsWith('.json') || f.endsWith('-ids.json') || f === 'manifest.schema.json') continue;
|
|
52
|
+
const name = f.slice(0, -5);
|
|
53
|
+
if (name === selfName) continue;
|
|
54
|
+
try {
|
|
55
|
+
const other = JSON.parse(readFileSync(join(dir, f), 'utf8'));
|
|
56
|
+
if (other?.mod === mod) return name;
|
|
57
|
+
} catch {
|
|
58
|
+
// 坏文件跳过
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// 目录不存在:第一次跑,正常
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 方块模型里能取到碰撞盒;取不到就当实心整块。 */
|
|
68
|
+
function boxFromModel(model, blockstates, name) {
|
|
69
|
+
// 1.21 的 blockstates 只给 model 名,碰撞盒要看 models/block/*.json 的 elements
|
|
70
|
+
const refs = [];
|
|
71
|
+
const variants = blockstates?.variants;
|
|
72
|
+
if (variants) {
|
|
73
|
+
for (const v of Object.values(variants)) {
|
|
74
|
+
const list = Array.isArray(v) ? v : [v];
|
|
75
|
+
for (const item of list) if (item?.model) refs.push(item.model);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (blockstates?.multipart) {
|
|
79
|
+
for (const part of blockstates.multipart) {
|
|
80
|
+
const applies = Array.isArray(part?.apply) ? part.apply : [part?.apply];
|
|
81
|
+
for (const apply of applies) if (apply?.model) refs.push(apply.model);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { refs, model };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 掉落表 → 物品名清单。 */
|
|
88
|
+
function dropsOf(loot) {
|
|
89
|
+
const out = [];
|
|
90
|
+
const walk = (entry) => {
|
|
91
|
+
if (!entry) return;
|
|
92
|
+
if (typeof entry.name === 'string' && entry.type === 'minecraft:item') out.push(entry.name);
|
|
93
|
+
if (Array.isArray(entry.children)) entry.children.forEach(walk);
|
|
94
|
+
if (Array.isArray(entry.pools)) {
|
|
95
|
+
for (const pool of entry.pools) for (const e of pool.entries ?? []) walk(e);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
walk(loot);
|
|
99
|
+
return [...new Set(out)];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 配方产物归一:自定义序列化器有时把 result 写成物品 id 字符串。
|
|
104
|
+
* 清单契约是 `{id,count}`;字符串必须在这里收成对象,否则宿主会整份拒收。
|
|
105
|
+
*/
|
|
106
|
+
export function resultRef(raw) {
|
|
107
|
+
return typeof raw === 'string' ? { id: raw, count: 1 } : raw;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 读一个 Fabric mod jar,产出 manifest 的静态部分。
|
|
112
|
+
*
|
|
113
|
+
* @param {string} jarPath
|
|
114
|
+
* @param {{ namespace?: string }} [opts] 覆盖命名空间(jar 里读不到时用)
|
|
115
|
+
*/
|
|
116
|
+
export function inspectJar(jarPath, opts = {}) {
|
|
117
|
+
const { sidecar, drop } = opts;
|
|
118
|
+
const zip = Zip.open(jarPath);
|
|
119
|
+
const fabric = zip.readJson('fabric.mod.json');
|
|
120
|
+
if (!fabric) {
|
|
121
|
+
throw new Error(`${jarPath} 不是 Fabric 模组:缺 fabric.mod.json`);
|
|
122
|
+
}
|
|
123
|
+
const namespace = opts.namespace || fabric.id;
|
|
124
|
+
if (!namespace) throw new Error('读不到 mod id,也不会自己猜:用 --namespace 指定');
|
|
125
|
+
const version = modVersionOf(fabric.version);
|
|
126
|
+
const packName = packNameOf(fabric.id, version, {
|
|
127
|
+
pack: opts.pack,
|
|
128
|
+
mcVersion: opts.mcVersion,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---- 方块:blockstates 就是"有哪些方块"的唯一来源 ----
|
|
132
|
+
const blockstates = new Map();
|
|
133
|
+
for (const n of zip.names(`assets/${namespace}/blockstates/`)) {
|
|
134
|
+
if (!n.endsWith('.json')) continue;
|
|
135
|
+
const name = n.slice(`assets/${namespace}/blockstates/`.length, -5);
|
|
136
|
+
const json = zip.readJson(n);
|
|
137
|
+
if (json) blockstates.set(name, json);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- 物品:模型目录 + 方块自带物品 ----
|
|
141
|
+
const itemModels = new Set();
|
|
142
|
+
for (const n of zip.names(`assets/${namespace}/models/item/`)) {
|
|
143
|
+
if (!n.endsWith('.json')) continue;
|
|
144
|
+
const name = n.slice(`assets/${namespace}/models/item/`.length, -5);
|
|
145
|
+
if (isItemModelVariant(name)) continue;
|
|
146
|
+
itemModels.add(name);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- 语言文件 ----
|
|
150
|
+
const names = {};
|
|
151
|
+
for (const [lang, key] of [['zh_cn', 'zh'], ['en_us', 'en']]) {
|
|
152
|
+
const data = zip.readJson(`assets/${namespace}/lang/${lang}.json`);
|
|
153
|
+
if (!data) continue;
|
|
154
|
+
for (const [k, v] of Object.entries(data)) {
|
|
155
|
+
if (k.startsWith('block.' + namespace + '.')) {
|
|
156
|
+
const short = k.slice(('block.' + namespace + '.').length);
|
|
157
|
+
(names[`block.${short}`] ??= {})[key] = v;
|
|
158
|
+
} else if (k.startsWith('item.' + namespace + '.')) {
|
|
159
|
+
const short = k.slice(('item.' + namespace + '.').length);
|
|
160
|
+
(names[`item.${short}`] ??= {})[key] = v;
|
|
161
|
+
} else if (k.startsWith('entity.' + namespace + '.')) {
|
|
162
|
+
const short = k.slice(('entity.' + namespace + '.').length);
|
|
163
|
+
(names[`entity.${short}`] ??= {})[key] = v;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- 全局方块 tag(minecraft:mineable/* 之类,用来估挖掘方式) ----
|
|
169
|
+
const blockTags = new Map();
|
|
170
|
+
for (const n of zip.names('data/')) {
|
|
171
|
+
const m = /^data\/([^/]+)\/tags\/(blocks?)\/(.+)\.json$/.exec(n);
|
|
172
|
+
if (!m) continue;
|
|
173
|
+
const json = zip.readJson(n);
|
|
174
|
+
if (json) blockTags.set(`${m[1]}:${m[3]}`, json.values ?? []);
|
|
175
|
+
}
|
|
176
|
+
const tagsOfBlock = (fullName) => {
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const [tag, values] of blockTags) {
|
|
179
|
+
if (values.includes(fullName)) out.push(tag.split(':')[1]);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ---- 组装 blocks ----
|
|
185
|
+
const blocks = {};
|
|
186
|
+
for (const [name, bs] of blockstates) {
|
|
187
|
+
const full = `${namespace}:${name}`;
|
|
188
|
+
const { refs } = boxFromModel(null, bs, name);
|
|
189
|
+
let box = null;
|
|
190
|
+
for (const ref of refs) {
|
|
191
|
+
const b = boxForRef(zip, ref);
|
|
192
|
+
if (b) { box = b; break; }
|
|
193
|
+
}
|
|
194
|
+
const hasVariants = Object.keys(bs.variants ?? {}).length > 0;
|
|
195
|
+
const states = parseStates(bs, namespace);
|
|
196
|
+
// 1.21 用单数(loot_table),旧版本用复数(loot_tables);两种都要认
|
|
197
|
+
const loot = zip.readJson(`data/${namespace}/loot_table/blocks/${name}.json`)
|
|
198
|
+
?? zip.readJson(`data/${namespace}/loot_tables/blocks/${name}.json`);
|
|
199
|
+
blocks[full] = {
|
|
200
|
+
states,
|
|
201
|
+
// 没有模型也没有状态变量的多属"技术方块"(作物/植物),给空碰撞盒
|
|
202
|
+
box,
|
|
203
|
+
solid: box !== null,
|
|
204
|
+
drops: dropsOf(loot),
|
|
205
|
+
tags: tagsOfBlock(full),
|
|
206
|
+
model: refs[0] ?? '',
|
|
207
|
+
};
|
|
208
|
+
if (!hasVariants && !bs.multipart) {
|
|
209
|
+
// 单状态方块(机器、箱子之类)模型缺失时按实心整块算,别让机器人以为能穿过去
|
|
210
|
+
if (box === null) { blocks[full].box = [0, 0, 0, 1, 1, 1]; blocks[full].solid = true; }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- 物品 ----
|
|
215
|
+
const items = {};
|
|
216
|
+
for (const name of itemModels) {
|
|
217
|
+
items[`${namespace}:${name}`] = { stack: 64 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- 配方 ----
|
|
221
|
+
// 资源目录有两代写法:1.21 用 `recipe/`,旧版用 `recipes/`。两种都扫。
|
|
222
|
+
const recipes = [];
|
|
223
|
+
for (const n of zip.names('data/')) {
|
|
224
|
+
if (!n.endsWith('.json')) continue;
|
|
225
|
+
const m = /^data\/([^/]+)\/(recipes?)\/(.+)\.json$/.exec(n);
|
|
226
|
+
if (!m) continue;
|
|
227
|
+
const ns = m[1];
|
|
228
|
+
const rel = m[3];
|
|
229
|
+
const raw = zip.readJson(n);
|
|
230
|
+
if (!raw) continue;
|
|
231
|
+
const type = String(raw.type ?? '').replace(/^minecraft:/, '');
|
|
232
|
+
const rec = { file: rel.split('/').pop(), type, id: `${ns}:${rel}` };
|
|
233
|
+
if (type === 'crafting_shaped') {
|
|
234
|
+
rec.pattern = raw.pattern ?? [];
|
|
235
|
+
rec.key = raw.key ?? {};
|
|
236
|
+
} else if (type === 'crafting_shapeless') {
|
|
237
|
+
rec.ingredients = raw.ingredients ?? [];
|
|
238
|
+
} else if (/cutting|cooking|smelting|smoking|blasting|dough/.test(type)) {
|
|
239
|
+
rec.ingredients = raw.ingredients ?? [];
|
|
240
|
+
rec.tool = raw.tool ?? null;
|
|
241
|
+
rec.cooking_time = raw.cookingtime ?? raw.cooking_time ?? null;
|
|
242
|
+
rec.container = raw.container ?? null;
|
|
243
|
+
rec.experience = raw.experience ?? null;
|
|
244
|
+
} else {
|
|
245
|
+
// 未知自定义类型(avaritia:compressor、fromage:cheese_slicing…):**保留常见字段**。
|
|
246
|
+
// 不猜语义、不补默认值,但 ingredients/result/耗时 是站点验收和规划要用的数据——
|
|
247
|
+
// 以前只留 result,verify 会报"配方缺 ingredients"(Avaritia 实测)。
|
|
248
|
+
if (raw.ingredients !== undefined) rec.ingredients = raw.ingredients;
|
|
249
|
+
if (raw.tool !== undefined) rec.tool = raw.tool;
|
|
250
|
+
if (raw.container !== undefined) rec.container = raw.container;
|
|
251
|
+
const t = raw.cost ?? raw.cookingtime ?? raw.cooking_time ?? raw.time ?? null;
|
|
252
|
+
if (t !== null) rec.cooking_time = t;
|
|
253
|
+
}
|
|
254
|
+
if (raw.result !== undefined && rec.result === undefined) {
|
|
255
|
+
// 有些自定义序列化器的 result 直接写物品 id 字符串(Supplementaries 的
|
|
256
|
+
// add_charges 等)。清单契约要 {id,count},这里统一成对象,别把字符串写进清单——
|
|
257
|
+
// 宿主 validate 会整份拒收。
|
|
258
|
+
rec.result = resultRef(raw.result);
|
|
259
|
+
}
|
|
260
|
+
recipes.push(rec);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---- tag 图 + 候选展开 ----
|
|
264
|
+
const tagmap = new Map();
|
|
265
|
+
for (const n of zip.names('data/')) {
|
|
266
|
+
const m = /^data\/([^/]+)\/tags\/(items?)\/(.+)\.json$/.exec(n);
|
|
267
|
+
if (!m) continue;
|
|
268
|
+
const json = zip.readJson(n);
|
|
269
|
+
if (!json) continue;
|
|
270
|
+
const values = [];
|
|
271
|
+
for (const v of json.values ?? []) {
|
|
272
|
+
if (typeof v === 'string') values.push(v);
|
|
273
|
+
else if (v && typeof v.id === 'string') values.push(v.id);
|
|
274
|
+
}
|
|
275
|
+
tagmap.set(`${m[1]}:${m[3]}`, values);
|
|
276
|
+
}
|
|
277
|
+
const expandTag = (tag, seen = new Set()) => {
|
|
278
|
+
const key = String(tag).replace(/^#/, '');
|
|
279
|
+
if (seen.has(key)) return [];
|
|
280
|
+
seen.add(key);
|
|
281
|
+
const out = [];
|
|
282
|
+
for (const v of tagmap.get(key) ?? []) {
|
|
283
|
+
if (v.startsWith('#')) out.push(...expandTag(v, seen));
|
|
284
|
+
else out.push(v);
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const usedTags = collectTags(recipes);
|
|
290
|
+
const candidates = {};
|
|
291
|
+
const missing = [];
|
|
292
|
+
for (const tag of usedTags) {
|
|
293
|
+
const expanded = [...new Set(expandTag(tag))];
|
|
294
|
+
if (expanded.length > 0) candidates[tag] = expanded;
|
|
295
|
+
else missing.push(tag);
|
|
296
|
+
}
|
|
297
|
+
// jar 里没有的那些:按社区约定落成具体物品(原版 tag 与跨 mod 的 c: tag 都走这条)
|
|
298
|
+
const filled = resolveMissingTags(missing, namespace);
|
|
299
|
+
Object.assign(candidates, filled.resolved);
|
|
300
|
+
const unresolvedTags = filled.stillMissing;
|
|
301
|
+
|
|
302
|
+
// ---- 实体类型 tag(只取 bosses:别的实体语义暂不进清单) ----
|
|
303
|
+
const entityTagmap = new Map();
|
|
304
|
+
for (const n of zip.names('data/')) {
|
|
305
|
+
const m = /^data\/([^/]+)\/tags\/(?:entity_type|entities)\/(.+)\.json$/.exec(n);
|
|
306
|
+
if (!m) continue;
|
|
307
|
+
const json = zip.readJson(n);
|
|
308
|
+
if (!json) continue;
|
|
309
|
+
const values = [];
|
|
310
|
+
for (const v of json.values ?? []) {
|
|
311
|
+
if (typeof v === 'string') values.push(v);
|
|
312
|
+
else if (v && typeof v.id === 'string') values.push(v.id);
|
|
313
|
+
}
|
|
314
|
+
entityTagmap.set(`${m[1]}:${m[2]}`, values);
|
|
315
|
+
}
|
|
316
|
+
const expandEntityTag = (tag, seen = new Set()) => {
|
|
317
|
+
const key = String(tag).replace(/^#/, '');
|
|
318
|
+
if (seen.has(key)) return [];
|
|
319
|
+
seen.add(key);
|
|
320
|
+
const out = [];
|
|
321
|
+
for (const v of entityTagmap.get(key) ?? []) {
|
|
322
|
+
if (v.startsWith('#')) out.push(...expandEntityTag(v, seen));
|
|
323
|
+
else out.push(v);
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
};
|
|
327
|
+
const bossTag = entityTagmap.has(`${namespace}:bosses`) ? `${namespace}:bosses`
|
|
328
|
+
: entityTagmap.has('c:bosses') ? 'c:bosses' : null;
|
|
329
|
+
const bosses = bossTag
|
|
330
|
+
? [...new Set(expandEntityTag(bossTag))].filter((v) => v.includes(':'))
|
|
331
|
+
: [];
|
|
332
|
+
|
|
333
|
+
// ---- 可食用物品 ----
|
|
334
|
+
const foods = collectFoods(tagmap, expandTag, namespace);
|
|
335
|
+
|
|
336
|
+
// ---- 作物 ----
|
|
337
|
+
const { crops, cropNotes } = inferCrops({ blocks, items, namespace, recipes, sidecar, drop });
|
|
338
|
+
|
|
339
|
+
// ---- 容器:只是候选,真假要 probe 阶段实测(crate 名字像却打不开) ----
|
|
340
|
+
const { candidates: containerList, rejected: containerRejected } = containerCandidates(blocks, items);
|
|
341
|
+
|
|
342
|
+
const manifest = {
|
|
343
|
+
mod: fabric.id.replace(/_/g, '-'),
|
|
344
|
+
version,
|
|
345
|
+
namespace,
|
|
346
|
+
source: jarPath.split(/[\\/]/).pop(),
|
|
347
|
+
blocks,
|
|
348
|
+
items,
|
|
349
|
+
foods,
|
|
350
|
+
crops,
|
|
351
|
+
recipes,
|
|
352
|
+
names,
|
|
353
|
+
containers: containerList,
|
|
354
|
+
// 实体 tag 里的 bosses(目前只认这个 tag;没有就不写字段)
|
|
355
|
+
...(bosses.length > 0 ? { bosses } : {}),
|
|
356
|
+
// 配方里的 tag 在**生成时**就展开成具体物品:运行时不再靠启发式猜
|
|
357
|
+
// (漏了这一行的话,重新生成的清单会丢掉所有 tag,配方大批变成"做不了")
|
|
358
|
+
candidates,
|
|
359
|
+
// stations / prompt / unsupported 留给 learn 阶段;空数组让校验器给出 warn 而不是 error
|
|
360
|
+
stations: [],
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
manifest,
|
|
365
|
+
packName,
|
|
366
|
+
unresolvedTags,
|
|
367
|
+
cropNotes,
|
|
368
|
+
containerRejected,
|
|
369
|
+
stats: {
|
|
370
|
+
blocks: Object.keys(blocks).length,
|
|
371
|
+
/** 声明有多少件物品(有 item model 才算) */
|
|
372
|
+
items: Object.keys(items).length,
|
|
373
|
+
/** 其中有多少是真拿得到的(inspect 阶段还没量,等于声明数;calibrate 后由 verify 报实际值) */
|
|
374
|
+
itemsDeclared: Object.keys(items).length,
|
|
375
|
+
recipes: recipes.length,
|
|
376
|
+
states: Object.values(blocks).reduce((n, b) => n + b.states.length, 0),
|
|
377
|
+
foods: foods.length,
|
|
378
|
+
crops: Object.keys(crops).length,
|
|
379
|
+
candidates: Object.keys(candidates).length,
|
|
380
|
+
unresolvedTags: unresolvedTags.length,
|
|
381
|
+
containers: containerList.length,
|
|
382
|
+
containersRejected: containerRejected.length,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* blockstates 的 variants/multipart → 状态组合列表。
|
|
389
|
+
*
|
|
390
|
+
* key 形如 `age=0,facing=north` 或单个 `normal`;值可以是数组(随机模型)。
|
|
391
|
+
*/
|
|
392
|
+
function parseStates(bs, namespace) {
|
|
393
|
+
/** @type {Set<string>} */
|
|
394
|
+
const out = new Set();
|
|
395
|
+
const pushKey = (key) => {
|
|
396
|
+
if (!key || key === 'normal' || key === '') {
|
|
397
|
+
out.add('{}');
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const parts = key.split(',').map((s) => s.trim()).filter(Boolean);
|
|
401
|
+
const props = {};
|
|
402
|
+
let ok = true;
|
|
403
|
+
for (const p of parts) {
|
|
404
|
+
const [k, v] = p.split('=');
|
|
405
|
+
if (!k || v === undefined) { ok = false; break; }
|
|
406
|
+
props[k] = v;
|
|
407
|
+
}
|
|
408
|
+
out.add(ok ? JSON.stringify(props) : '{}');
|
|
409
|
+
};
|
|
410
|
+
for (const key of Object.keys(bs.variants ?? {})) pushKey(key);
|
|
411
|
+
for (const part of bs.multipart ?? []) {
|
|
412
|
+
const when = part.when;
|
|
413
|
+
if (!when) { out.add('{}'); continue; }
|
|
414
|
+
// multipart 的 when 是布尔组合,完整枚举太贵:只取其中最具体的一支
|
|
415
|
+
const flat = flattenWhen(when);
|
|
416
|
+
for (const k of Object.keys(flat)) {
|
|
417
|
+
const values = flat[k];
|
|
418
|
+
for (const v of values) pushKey(`${k}=${v}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return [...out].map((s) => JSON.parse(s));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** multipart 的 when 可能是 {OR:[...]} / {AND:[...]} / {prop:value}。取所有叶子。 */
|
|
425
|
+
function flattenWhen(when, acc = {}) {
|
|
426
|
+
if (!when || typeof when !== 'object') return acc;
|
|
427
|
+
for (const [k, v] of Object.entries(when)) {
|
|
428
|
+
if (k === 'OR' || k === 'AND') {
|
|
429
|
+
for (const sub of Array.isArray(v) ? v : [v]) flattenWhen(sub, acc);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (Array.isArray(v)) { acc[k] = v.map(String); continue; }
|
|
433
|
+
acc[k] = [String(v)];
|
|
434
|
+
}
|
|
435
|
+
return acc;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** 配方里出现过的所有 tag(含 key/ingredients 两种写法)。 */
|
|
439
|
+
function collectTags(recipes) {
|
|
440
|
+
const out = new Set();
|
|
441
|
+
const walk = (x) => {
|
|
442
|
+
if (Array.isArray(x)) { x.forEach(walk); return; }
|
|
443
|
+
if (!x || typeof x !== 'object') return;
|
|
444
|
+
if (typeof x.tag === 'string') out.add(x.tag);
|
|
445
|
+
for (const v of Object.values(x)) walk(v);
|
|
446
|
+
};
|
|
447
|
+
for (const r of recipes) {
|
|
448
|
+
walk(r.key);
|
|
449
|
+
walk(r.ingredients);
|
|
450
|
+
}
|
|
451
|
+
return [...out].sort();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** 从 item tag 图里递归展开出本 mod 的可食用物品。 */
|
|
455
|
+
function collectFoods(tagmap, expandTag, namespace) {
|
|
456
|
+
const out = new Set();
|
|
457
|
+
for (const tag of tagmap.keys()) {
|
|
458
|
+
if (!/foods|drinks/.test(tag)) continue;
|
|
459
|
+
for (const item of expandTag(tag)) {
|
|
460
|
+
if (item.startsWith(`${namespace}:`)) out.add(item);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return [...out].sort();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export { compareVersions };
|