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/cortico-bridge.mjs
CHANGED
|
@@ -1,325 +1,397 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* modsmith 自带的最小注册表注入。
|
|
3
|
-
*
|
|
4
|
-
* 为什么要自带一份而不 import 宿主的:modsmith 是能独立拿出去用的工具,不该反向依赖
|
|
5
|
-
* 某个具体项目。这里只做 probe 需要的两件事:让 `bot.blockAt` 认得出模组方块、让它
|
|
6
|
-
* 知道模组物品的数字 id。
|
|
7
|
-
*
|
|
8
|
-
* 注入时机同样必须在 `inject_allowed` 之前——prismarine-block 的工厂只在那时拷一次
|
|
9
|
-
* boundingBox/shapes。
|
|
10
|
-
*/
|
|
11
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
1
|
+
/**
|
|
2
|
+
* modsmith 自带的最小注册表注入。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要自带一份而不 import 宿主的:modsmith 是能独立拿出去用的工具,不该反向依赖
|
|
5
|
+
* 某个具体项目。这里只做 probe 需要的两件事:让 `bot.blockAt` 认得出模组方块、让它
|
|
6
|
+
* 知道模组物品的数字 id。
|
|
7
|
+
*
|
|
8
|
+
* 注入时机同样必须在 `inject_allowed` 之前——prismarine-block 的工厂只在那时拷一次
|
|
9
|
+
* boundingBox/shapes。
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { harvestToolsFor, isDiggable, shapeTemplateOf, templateShapeId } from './block-shapes.mjs';
|
|
14
|
+
|
|
15
|
+
let modsDir = null;
|
|
16
|
+
|
|
17
|
+
/** 指定 manifest/ids 所在目录(默认 ./mods)。 */
|
|
18
|
+
export function setModsDir(dir) {
|
|
19
|
+
modsDir = dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function dirOf() {
|
|
23
|
+
return modsDir ?? join(process.cwd(), 'mods');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadModPack(name) {
|
|
27
|
+
const dir = dirOf();
|
|
28
|
+
if (!name) return null;
|
|
29
|
+
try {
|
|
30
|
+
const manifest = JSON.parse(readFileSync(join(dir, `${name}.json`), 'utf8'));
|
|
31
|
+
const ids = JSON.parse(readFileSync(join(dir, `${name}-ids.json`), 'utf8'));
|
|
32
|
+
return { manifest, ids };
|
|
33
|
+
} catch {
|
|
34
|
+
// 没有 ids 时才来看一眼有没有 manifest:给个明白的说法
|
|
35
|
+
try {
|
|
36
|
+
JSON.parse(readFileSync(join(dir, `${name}.json`), 'utf8'));
|
|
37
|
+
throw new Error(`${name}-ids.json 不存在:先跑 modsmith calibrate`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (String(err.message).includes('先跑')) throw err;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 已加载的包名(供 probe 逐个注入)。 */
|
|
46
|
+
export function listPacks() {
|
|
47
|
+
try {
|
|
48
|
+
return readdirSync(dirOf())
|
|
49
|
+
.filter((f) => f.endsWith('.json') && !f.endsWith('-ids.json') && f !== 'manifest.schema.json')
|
|
50
|
+
.map((f) => f.slice(0, -5));
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 把 manifest + 实测 id 合并进 bot.registry.
|
|
58
|
+
* 返回注入的方块/状态/物品数,供日志。
|
|
59
|
+
*/
|
|
60
|
+
/** 从方块 tags 推 prismarine material:挖速/工具判定用,推不出就 default。 */
|
|
61
|
+
function materialOf(tags) {
|
|
62
|
+
for (const kind of ['pickaxe', 'axe', 'shovel', 'hoe', 'shears']) {
|
|
63
|
+
if (tags.includes(`mineable/${kind}`)) return `mineable/${kind}`;
|
|
64
|
+
}
|
|
65
|
+
return 'default';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 服务端 SpawnGroup → prismarine 的实体 type。
|
|
70
|
+
*
|
|
71
|
+
* minecraft-data 里 `type` 决定 mineflayer 的 `entity.type`,也是宿主判"会不会打我"
|
|
72
|
+
* 的依据(hostile)。模组实体的原始生成组由 dump 给出,这里只做词表翻译。
|
|
73
|
+
*/
|
|
74
|
+
const SPAWN_GROUP_TYPE = {
|
|
75
|
+
monster: 'hostile',
|
|
76
|
+
creature: 'animal',
|
|
77
|
+
axolotls: 'animal',
|
|
78
|
+
ambient: 'ambient',
|
|
79
|
+
underground_water_creature: 'water_creature',
|
|
80
|
+
water_creature: 'water_creature',
|
|
81
|
+
water_ambient: 'water_creature',
|
|
82
|
+
misc: 'mob',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function installModRegistry(bot, pack) {
|
|
86
|
+
const reg = bot.registry;
|
|
87
|
+
if (!reg?.blocksByStateId || !reg.itemsByName) throw new Error('registry 形状不对,注入中止');
|
|
88
|
+
const { manifest, ids } = pack;
|
|
89
|
+
|
|
90
|
+
let nextBlockId = Math.max(0, ...Object.keys(reg.blocks ?? {}).map(Number)) + 1;
|
|
91
|
+
const shapes = reg.blockCollisionShapes;
|
|
92
|
+
let nextShapeId = shapes ? Math.max(0, ...Object.keys(shapes.shapes ?? {}).map(Number)) + 1 : 0;
|
|
93
|
+
|
|
94
|
+
let blocks = 0;
|
|
95
|
+
let states = 0;
|
|
96
|
+
const observed = [];
|
|
97
|
+
/**
|
|
98
|
+
* 一个 stateId 只能属于一个方块。
|
|
99
|
+
*
|
|
100
|
+
* 老校准会遇到这个:`/setblock` 改不动某些属性时,客户端读回的是上一格的残留值,
|
|
101
|
+
* 于是几十个方块的"第二、三个状态"全都指向同一个 stateId(实测农夫乐事有 100 多个
|
|
102
|
+
* 方块挤在 26979 上)。如果照单全收,后处理的方块会覆盖前者 —— 机器人看见米捆,实际是橱柜。
|
|
103
|
+
*
|
|
104
|
+
* 处理:先到先得,后来的同号条目丢掉。宁可这个状态"未知"(退回空气/不影响),
|
|
105
|
+
* 也别标成错的方块 —— 错名比未知更糟。
|
|
106
|
+
*/
|
|
107
|
+
const claimedBy = new Map();
|
|
108
|
+
|
|
109
|
+
for (const [name, spec] of Object.entries(manifest.blocks ?? {})) {
|
|
110
|
+
const measured = ids.blocks?.[name] ?? [];
|
|
111
|
+
if (measured.length === 0) continue;
|
|
112
|
+
const stateIds = measured.map((m) => m.stateId).filter((v) => v > 0);
|
|
113
|
+
if (stateIds.length === 0) continue;
|
|
114
|
+
|
|
115
|
+
const solid = spec.solid !== false;
|
|
116
|
+
const box = solid ? (spec.box ?? [0, 0, 0, 1, 1, 1]) : null;
|
|
117
|
+
const seq = [...measured].sort((a, b) => a.stateId - b.stateId);
|
|
118
|
+
const propNames = [...new Set(seq.flatMap((m) => Object.keys(m.props ?? {})))];
|
|
119
|
+
const transitions = propNames.map((p) => {
|
|
120
|
+
let n = 0;
|
|
121
|
+
for (let i = 1; i < seq.length; i++) if (seq[i].props?.[p] !== seq[i - 1].props?.[p]) n++;
|
|
122
|
+
const values = new Set(seq.map((m) => m.props?.[p]));
|
|
123
|
+
return { name: p, n, values };
|
|
124
|
+
}).sort((a, b) => a.n - b.n);
|
|
125
|
+
const stateDesc = transitions.map((t) => ({
|
|
126
|
+
name: t.name,
|
|
127
|
+
type: t.values.size === 2 && (t.values.has('true') || t.values.has('false')) ? 'bool' : 'integer',
|
|
128
|
+
num_values: t.values.size,
|
|
129
|
+
}));
|
|
130
|
+
const span = Math.max(...stateIds) - Math.min(...stateIds) + 1;
|
|
131
|
+
const decoded = stateDesc.reduce((n, d) => n * d.num_values, 1);
|
|
132
|
+
if (span > decoded) {
|
|
133
|
+
const known = new Set(stateDesc.map((d) => d.name));
|
|
134
|
+
for (const p of [...new Set((spec.states ?? []).flatMap((s) => Object.keys(s)))]) {
|
|
135
|
+
if (known.has(p)) continue;
|
|
136
|
+
const values = new Set((spec.states ?? []).map((s) => s[p]).filter((v) => v !== undefined));
|
|
137
|
+
if (values.size > 1) {
|
|
138
|
+
stateDesc.push({ name: p, type: 'bool', num_values: values.size });
|
|
139
|
+
known.add(p);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const entry = {
|
|
145
|
+
id: nextBlockId++,
|
|
146
|
+
name,
|
|
147
|
+
displayName: manifest.names?.[`block.${name.split(':')[1]}`]?.zh ?? name.split(':')[1],
|
|
148
|
+
hardness: solid ? 1.5 : 0,
|
|
149
|
+
boundingBox: solid ? 'block' : 'empty',
|
|
150
|
+
// 没有碰撞箱 ≠ 挖不动:作物、花、火把都要能挖。canDigBlock 读的就是这个字段。
|
|
151
|
+
diggable: isDiggable(name),
|
|
152
|
+
transparent: !solid,
|
|
153
|
+
material: materialOf(spec.tags ?? []),
|
|
154
|
+
harvestTools: harvestToolsFor(reg, spec.tags ?? []),
|
|
155
|
+
drops: spec.drops ?? [],
|
|
156
|
+
minStateId: Math.min(...stateIds),
|
|
157
|
+
maxStateId: Math.max(...stateIds),
|
|
158
|
+
defaultState: Math.min(...stateIds),
|
|
159
|
+
stackSize: 64,
|
|
160
|
+
states: stateDesc,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const kept = [];
|
|
164
|
+
for (const id of stateIds) {
|
|
165
|
+
const owner = claimedBy.get(id);
|
|
166
|
+
if (owner !== undefined && owner !== name) continue; // 别的方块已经占了
|
|
167
|
+
if (owner === undefined) claimedBy.set(id, name);
|
|
168
|
+
kept.push(id);
|
|
169
|
+
}
|
|
170
|
+
if (kept.length === 0) continue; // 全部被别人占了:这个方块宁可不注入
|
|
171
|
+
|
|
172
|
+
entry.minStateId = Math.min(...kept);
|
|
173
|
+
entry.maxStateId = Math.max(...kept);
|
|
174
|
+
entry.defaultState = Math.min(...kept);
|
|
175
|
+
|
|
176
|
+
if (shapes) {
|
|
177
|
+
const propsById = new Map(measured.map((m) => [m.stateId, m.props ?? {}]));
|
|
178
|
+
const template = shapeTemplateOf(name);
|
|
179
|
+
const emptyId = nextShapeId++;
|
|
180
|
+
shapes.shapes[emptyId] = [];
|
|
181
|
+
const fallbackId = box ? nextShapeId++ : emptyId;
|
|
182
|
+
if (box) shapes.shapes[fallbackId] = [[box[0], box[1], box[2], box[3], box[4], box[5]]];
|
|
183
|
+
const span = entry.maxStateId - entry.minStateId + 1;
|
|
184
|
+
const stateShapeIds = new Array(span).fill(fallbackId);
|
|
185
|
+
for (const id of kept) {
|
|
186
|
+
const fromTemplate = template ? templateShapeId(reg, template, propsById.get(id)) : null;
|
|
187
|
+
stateShapeIds[id - entry.minStateId] = fromTemplate ?? fallbackId;
|
|
188
|
+
}
|
|
189
|
+
shapes.blocks[name] = stateShapeIds;
|
|
190
|
+
// 模板给出的形状可能让原本 solid:false 的楼梯/台阶有碰撞;以真实形状为准。
|
|
191
|
+
const collides = stateShapeIds.some((id) => (shapes.shapes[id]?.length ?? 0) > 0);
|
|
192
|
+
entry.boundingBox = collides ? 'block' : 'empty';
|
|
193
|
+
entry.transparent = !collides;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
reg.blocks[entry.id] = entry;
|
|
197
|
+
reg.blocksByName[name] = entry;
|
|
198
|
+
reg.blocksArray.push(entry);
|
|
199
|
+
for (const id of kept) reg.blocksByStateId[id] = entry;
|
|
200
|
+
|
|
201
|
+
observed.push({ entry, min: entry.minStateId, span: Math.max(kept.length, spec.states?.length ?? 0) });
|
|
202
|
+
blocks++;
|
|
203
|
+
states += kept.length;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 实测没覆盖到的状态按区间补齐,别留成"未知方块=空气"。
|
|
207
|
+
//
|
|
208
|
+
// span 用 max(实测条数, 声明状态数):Fabric 给每个方块的 stateId 是**按声明状态数
|
|
209
|
+
// 连号**的,没量全时它的区间尾巴伸进隔壁方块的地盘——按实测宽度补会把隔壁标成它
|
|
210
|
+
// (实测:三个挂墙告示牌被填成了 gleaming_salad_block)。
|
|
211
|
+
// 但也**不能抢别的方块实测过的 stateId**:"错名比未知更糟",只填没被任何人量到的号。
|
|
212
|
+
const claimed = new Set();
|
|
213
|
+
for (const list of Object.values(ids.blocks ?? {})) {
|
|
214
|
+
for (const m of list) if (m.stateId > 0) claimed.add(m.stateId);
|
|
215
|
+
}
|
|
216
|
+
for (const { entry, min, span } of observed) {
|
|
217
|
+
for (let id = min; id < min + span; id++) {
|
|
218
|
+
if (reg.blocksByStateId[id]) continue;
|
|
219
|
+
if (claimed.has(id)) continue; // 这一格是别人量到的,别抢
|
|
220
|
+
reg.blocksByStateId[id] = entry;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let items = 0;
|
|
225
|
+
let nextItemId = Math.max(0, ...Object.keys(reg.items ?? {}).map(Number)) + 1;
|
|
226
|
+
for (const name of Object.keys(manifest.items ?? {})) {
|
|
227
|
+
const id = ids.items?.[name];
|
|
228
|
+
if (typeof id !== 'number' || id <= 0) continue;
|
|
229
|
+
const entry = {
|
|
230
|
+
id,
|
|
231
|
+
name,
|
|
232
|
+
displayName: manifest.names?.[`item.${name.split(':')[1]}`]?.zh
|
|
233
|
+
?? manifest.names?.[`block.${name.split(':')[1]}`]?.zh
|
|
234
|
+
?? name.split(':')[1],
|
|
235
|
+
stackSize: manifest.items[name]?.stack ?? 64,
|
|
236
|
+
};
|
|
237
|
+
reg.items[id] = entry;
|
|
238
|
+
reg.itemsByName[name] = entry;
|
|
239
|
+
reg.itemsArray.push(entry);
|
|
240
|
+
items++;
|
|
241
|
+
}
|
|
242
|
+
void nextItemId;
|
|
243
|
+
|
|
244
|
+
// 模组食物:不注入的话 skillEat 会一律拒
|
|
245
|
+
if (reg.foodsByName) {
|
|
246
|
+
for (const name of manifest.foods ?? []) {
|
|
247
|
+
if (reg.foodsByName[name]) continue;
|
|
248
|
+
const id = ids.items?.[name];
|
|
249
|
+
if (typeof id !== 'number') continue;
|
|
250
|
+
reg.foodsByName[name] = { id, name, foodPoints: 4, saturation: 0.6, eatingTime: 32 };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 模组实体:不注入的话,服务端发来的数字 id 在 mineflayer 里找不到名字,
|
|
255
|
+
// boss 在她眼里就是 "unknown"(实测:Naga 打死她,她连对方叫什么都不知道)。
|
|
256
|
+
// 数字 id 来自 registry-dump(0.1.2 起带 spawnGroup/碰撞箱);老 dump 只有 id,
|
|
257
|
+
// 那就退回 mob 体型,名字仍然能认出来。
|
|
258
|
+
let entities = 0;
|
|
259
|
+
const seenEntityId = new Map();
|
|
260
|
+
for (const [name, spec] of Object.entries(ids.entities ?? {})) {
|
|
261
|
+
const id = Number(spec?.id);
|
|
262
|
+
if (!Number.isInteger(id) || id <= 0) continue;
|
|
263
|
+
if (reg.entitiesByName?.[name] || reg.entities?.[id] || seenEntityId.has(id)) continue;
|
|
264
|
+
seenEntityId.set(id, name);
|
|
265
|
+
const short = name.split(':')[1] ?? name;
|
|
266
|
+
const group = typeof spec.spawnGroup === 'string' ? spec.spawnGroup : 'misc';
|
|
267
|
+
// 老 dump 没有 spawnGroup:boss 按 hostile 处理(宁可当她该拔刀,也别当成无害动物)
|
|
268
|
+
const type = spec.spawnGroup ? (SPAWN_GROUP_TYPE[group] ?? 'mob')
|
|
269
|
+
: (spec.boss ? 'hostile' : 'mob');
|
|
270
|
+
const entry = {
|
|
271
|
+
id,
|
|
272
|
+
internalId: id,
|
|
273
|
+
name,
|
|
274
|
+
displayName: manifest.names?.[`entity.${short}`]?.zh
|
|
275
|
+
?? manifest.names?.[`entity.${short}`]?.en
|
|
276
|
+
?? short,
|
|
277
|
+
width: Number.isFinite(spec.width) ? spec.width : 0.6,
|
|
278
|
+
height: Number.isFinite(spec.height) ? spec.height : 1.8,
|
|
279
|
+
type,
|
|
280
|
+
category: group,
|
|
281
|
+
metadataKeys: [],
|
|
282
|
+
...(spec.boss ? { boss: true } : {}),
|
|
283
|
+
};
|
|
284
|
+
reg.entities[id] = entry;
|
|
285
|
+
reg.entitiesByName[name] = entry;
|
|
286
|
+
reg.entitiesArray?.push(entry);
|
|
287
|
+
entities++;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { blocks, states, items, entities };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 包流容错:模组改了静态注册表会让 `declare_recipes` 整包错位,不吞掉客户端连不上。
|
|
295
|
+
*
|
|
296
|
+
* 两个必须照抄的细节(踩过):
|
|
297
|
+
* a) 要补在 **deserializer** 上,不是 proto 上
|
|
298
|
+
* b) 每次状态切换 mineflayer 会换掉 deserializer,所以得挂 `client.on('state')` 反复补
|
|
299
|
+
* c) metadata.size 必须等于本帧长度,否则流会错位
|
|
300
|
+
*/
|
|
301
|
+
/** 测量机器人额外丢弃的包:protodef 在大 mod 上必炸、而测量流程又不依赖它们。 */
|
|
302
|
+
export const MEASURE_DROPS = ['declare_commands', 'entity_metadata'];
|
|
303
|
+
|
|
304
|
+
export function installMineflayerFixes(bot, opts = {}) {
|
|
305
|
+
const client = bot._client;
|
|
306
|
+
if (!client) return;
|
|
307
|
+
// extraDrop:测量类机器人(calibrate/probe/verify)不依赖命令树与实体元数据,
|
|
308
|
+
// 大 mod(暮色森林实测)在这两类包上会把 protodef 打出 PartialReadError,
|
|
309
|
+
// 不丢掉的话 error 事件会把 spawn 等待挂死。宿主不传这个选项,行为不变。
|
|
310
|
+
const drop = new Set(['declare_recipes', ...(opts.extraDrop ?? [])]);
|
|
311
|
+
const seen = new Set();
|
|
312
|
+
|
|
313
|
+
const patch = () => {
|
|
314
|
+
const d = client.deserializer;
|
|
315
|
+
if (!d || d.__modsmithTolerant) return;
|
|
316
|
+
const orig = d.parsePacketBuffer.bind(d);
|
|
317
|
+
d.parsePacketBuffer = (buf) => {
|
|
318
|
+
// 先认包名:服务端真配方只活在这个包里(mineflayer 没有配方插件,prismarine-recipe
|
|
319
|
+
// 用的是静态 registry 数据)。**解析成功也要存** —— mineflayer 哪天修好 1.21.1,
|
|
320
|
+
// 捕获不能跟着静默消失。只存第一份(登录时只有一个 declare_recipes)。
|
|
321
|
+
const name = packetNameOf(bot, buf);
|
|
322
|
+
if (name === 'declare_recipes' && !bot._modsmithDeclareRecipes) {
|
|
323
|
+
bot._modsmithDeclareRecipes = Buffer.from(buf);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return orig(buf);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (!drop.has(name)) throw err;
|
|
329
|
+
if (!seen.has(name)) {
|
|
330
|
+
seen.add(name);
|
|
331
|
+
console.warn(name === 'declare_recipes'
|
|
332
|
+
? `丢弃无法解析的 ${name}(${buf.length} 字节):模组改写了静态注册表;原始包已留存,可用 modsmith recipes 解析`
|
|
333
|
+
: `丢弃无法解析的 ${name}(${buf.length} 字节):测量机器人不依赖它,丢掉保住读包循环`);
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
data: { name: '_modsmith_dropped', params: {} },
|
|
337
|
+
metadata: { state: client.state, name: '_modsmith_dropped', size: buf.length },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
d.__modsmithTolerant = true;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
patch();
|
|
345
|
+
|
|
346
|
+
// 模组自定义窗口:prismarine-windows 不认识就会让 createWindow 返回 null,
|
|
347
|
+
// mineflayer 的 open_window 监听器立刻 `window.id` 崩掉整个进程(Fromage 实测,
|
|
348
|
+
// 探到第 40 个方块时)。窗口类型本身是重要证据——"开了一个未知窗外"要记成
|
|
349
|
+
// 开窗(机制=侧车),不能因为播不出来就当成"不开窗"。
|
|
350
|
+
const emitter = client;
|
|
351
|
+
if (!emitter.__modsmithEmitGuard) {
|
|
352
|
+
const origEmit = emitter.emit.bind(emitter);
|
|
353
|
+
emitter.emit = function guardedEmit(event, ...args) {
|
|
354
|
+
// 只读验收/测量不需要命令树;大 mod 的树会让 minecraft-protocol 直接拒收
|
|
355
|
+
// ("impossible command tree"),在事件层吞掉比让整个 bot 崩掉好。
|
|
356
|
+
if (opts.dropCommands === true && event === 'declare_commands') return false;
|
|
357
|
+
try {
|
|
358
|
+
return origEmit(event, ...args);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
if (event === 'open_window') {
|
|
361
|
+
const type = String(args?.[0]?.inventoryType ?? 'unknown');
|
|
362
|
+
bot._modsmithUnknownWindow = type;
|
|
363
|
+
if (!seen.has(`window:${type}`)) {
|
|
364
|
+
seen.add(`window:${type}`);
|
|
365
|
+
console.warn(`模组自定义窗口类型 ${type}:prismarine-windows 不认识,记为"开窗但界面未知"`);
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
emitter.__modsmithEmitGuard = true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
client.on('state', patch);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 包号 → 名字。协议表用十六进制字符串("0x77")做键。 */
|
|
379
|
+
function packetNameOf(bot, buf) {
|
|
380
|
+
try {
|
|
381
|
+
// varint 解包号
|
|
382
|
+
let i = 0;
|
|
383
|
+
let id = 0;
|
|
384
|
+
let shift = 0;
|
|
385
|
+
while (i < buf.length) {
|
|
386
|
+
const byte = buf[i++];
|
|
387
|
+
id |= (byte & 0x7f) << shift;
|
|
388
|
+
if ((byte & 0x80) === 0) break;
|
|
389
|
+
shift += 7;
|
|
390
|
+
}
|
|
391
|
+
const packet = bot.registry?.protocol?.play?.toClient?.types?.packet;
|
|
392
|
+
const mappings = packet?.[1]?.[0]?.type?.[1]?.mappings ?? {};
|
|
393
|
+
return mappings[`0x${id.toString(16)}`] ?? mappings[String(id)] ?? `packet#${id}`;
|
|
394
|
+
} catch {
|
|
395
|
+
return 'packet#?';
|
|
396
|
+
}
|
|
397
|
+
}
|