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
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 接包验收:manifest / ids / 注入后的 registry 三层离线检查。
|
|
3
|
+
*
|
|
4
|
+
* 这不是"再跑一遍 validate":validate 只看字段,这里看**行为**——声明的每个方块
|
|
5
|
+
* 有没有形状、每个 state 能不能落到 shape、作物能不能挖、楼梯上下半是不是不同、
|
|
6
|
+
* boss 有没有真的进注册表。conformance 是 modsmith 的验收线,不是宿主的游戏逻辑。
|
|
7
|
+
*/
|
|
8
|
+
import { checkServerMods } from './server-fingerprint.mjs';
|
|
9
|
+
import { installModRegistry } from './cortico-bridge.mjs';
|
|
10
|
+
import { shapeTemplateOf } from './block-shapes.mjs';
|
|
11
|
+
import { craftRecipeViews } from './recipes-view.mjs';
|
|
12
|
+
import { validateManifest, hasErrors, describeIssues } from './validate.mjs';
|
|
13
|
+
import { vanillaBlockStateCeiling } from './recipes-join.mjs';
|
|
14
|
+
|
|
15
|
+
const V_1_21_1 = { blockState: 26683, item: 1332, entity: 129 };
|
|
16
|
+
const CROPISH = /wheat|carrot|potato|beetroot|onion|tomato|cabbage|rice|melon|pumpkin|sugarcane|berry|flax|hemp|budding_/i;
|
|
17
|
+
|
|
18
|
+
function check(checks, id, level, subject, message, details) {
|
|
19
|
+
checks.push({ id, level, subject, message, ...(details === undefined ? {} : { details }) });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ageValues(block) {
|
|
23
|
+
const values = new Set();
|
|
24
|
+
for (const state of block?.states ?? []) {
|
|
25
|
+
const raw = state?.age;
|
|
26
|
+
if (raw !== undefined && Number.isFinite(Number(raw))) values.add(Number(raw));
|
|
27
|
+
}
|
|
28
|
+
return [...values].sort((a, b) => a - b);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** M1–M8:只吃 manifest,不连服务器。 */
|
|
32
|
+
export function checkManifest(manifest) {
|
|
33
|
+
const checks = [];
|
|
34
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
35
|
+
check(checks, 'M1', 'fail', 'manifest', '不是对象');
|
|
36
|
+
return checks;
|
|
37
|
+
}
|
|
38
|
+
const issues = validateManifest(manifest);
|
|
39
|
+
const errors = issues.filter((i) => i.level === 'error');
|
|
40
|
+
const warns = issues.filter((i) => i.level !== 'error');
|
|
41
|
+
check(checks, 'M1', errors.length > 0 ? 'fail' : 'pass', 'manifest',
|
|
42
|
+
errors.length > 0 ? describeIssues(errors).join(';') : 'schema/validate 通过',
|
|
43
|
+
errors.length > 0 ? errors : undefined);
|
|
44
|
+
for (const w of warns) check(checks, 'M1.warn', 'warn', w.path ?? 'manifest', w.message);
|
|
45
|
+
|
|
46
|
+
for (const [name, block] of Object.entries(manifest.blocks ?? {})) {
|
|
47
|
+
if (!name.includes(':')) {
|
|
48
|
+
check(checks, 'M2', 'fail', name, '方块名没有 namespace');
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const solid = block?.solid !== false;
|
|
52
|
+
const box = block?.box;
|
|
53
|
+
if (solid && !Array.isArray(box) && !shapeTemplateOf(name)) {
|
|
54
|
+
check(checks, 'M3', 'fail', name, 'solid:true 但既没有 box 也没有可用的形状模板');
|
|
55
|
+
} else if (!solid && Array.isArray(box)) {
|
|
56
|
+
check(checks, 'M3', 'warn', name, 'solid:false 却给了 box(会让客户端以为有碰撞)');
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(block?.states) || block.states.length === 0) {
|
|
59
|
+
check(checks, 'M4', 'fail', name, 'states 为空,无法映射 state');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [crop, spec] of Object.entries(manifest.crops ?? {})) {
|
|
64
|
+
const seed = String(spec?.seed ?? '');
|
|
65
|
+
if (!seed) {
|
|
66
|
+
check(checks, 'M5', 'fail', crop, 'crop 没有 seed');
|
|
67
|
+
} else if (!manifest.items?.[seed] && !manifest.items?.[`${manifest.namespace}:${seed}`]) {
|
|
68
|
+
// 原版种子(wheat_seeds 等)不在模组 items 里,属于正常;只对带 namespace 的种子要求
|
|
69
|
+
if (seed.includes(':') && !seed.startsWith('minecraft:')) {
|
|
70
|
+
check(checks, 'M5', 'warn', crop, `seed ${seed} 不在 items 里`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const maxAge = Number(spec?.max_age);
|
|
74
|
+
for (const aliasShort of spec?.aliases ?? []) {
|
|
75
|
+
const alias = aliasShort.includes(':') ? aliasShort : `${manifest.namespace}:${aliasShort}`;
|
|
76
|
+
const block = manifest.blocks?.[alias];
|
|
77
|
+
if (!block) {
|
|
78
|
+
check(checks, 'M5', 'fail', crop, `alias ${alias} 不在 blocks 里`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const ages = ageValues(block);
|
|
82
|
+
if (ages.length > 0 && Number.isFinite(maxAge) && Math.max(...ages) !== maxAge) {
|
|
83
|
+
const drops = block.drops ?? [];
|
|
84
|
+
const seedOnly = drops.length > 0 && drops.every((d) => d === seed || String(d).endsWith('_seeds'));
|
|
85
|
+
check(checks, 'M5', seedOnly ? 'fail' : 'warn', alias,
|
|
86
|
+
`alias 的 age 上限 ${Math.max(...ages)} 与作物 max_age ${maxAge} 不一致` +
|
|
87
|
+
(seedOnly ? ',且只掉种子(长苗阶段不该当成熟作物)' : ''));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (maxAge <= 0) check(checks, 'M5', 'fail', crop, `max_age 非法:${spec?.max_age}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const recipes = manifest.recipes ?? [];
|
|
94
|
+
const resultRequired = new Set([
|
|
95
|
+
'crafting_shaped', 'crafting_shapeless',
|
|
96
|
+
'smelting', 'blasting', 'smoking', 'campfire_cooking',
|
|
97
|
+
'stonecutting', 'smithing_transform',
|
|
98
|
+
]);
|
|
99
|
+
const customMissing = new Map();
|
|
100
|
+
for (const rec of recipes) {
|
|
101
|
+
if (rec?.unsupported) continue;
|
|
102
|
+
const type = String(rec?.type ?? '').replace(/^minecraft:/, '');
|
|
103
|
+
if (!rec?.result || typeof rec.result.id !== 'string' || !rec.result.id) {
|
|
104
|
+
// 自定义序列化器(twilightforest:emperors_cloth_recipe 之类)本来就可能没有
|
|
105
|
+
// 统一的 result;只对原版有明确产物形状的类型判死,其余按类型汇总提示。
|
|
106
|
+
if (resultRequired.has(type)) {
|
|
107
|
+
check(checks, 'M6', 'fail', rec?.id ?? rec?.file ?? 'recipe', '配方没有可解析的 result.id');
|
|
108
|
+
} else {
|
|
109
|
+
const key = type || '?';
|
|
110
|
+
const hit = customMissing.get(key);
|
|
111
|
+
if (hit) hit.count += 1;
|
|
112
|
+
else customMissing.set(key, { count: 1, first: rec?.id ?? rec?.file ?? 'recipe' });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const [type, hit] of customMissing) {
|
|
117
|
+
check(checks, 'M6', 'warn', type,
|
|
118
|
+
`${hit.count} 条自定义配方没有统一 result,配方视图不会消费(首条 ${hit.first})`);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const views = craftRecipeViews(manifest);
|
|
122
|
+
if (recipes.length > 0 && views.length === 0) {
|
|
123
|
+
check(checks, 'M6', 'fail', 'recipes', '清单有配方,但 craftRecipeViews 一条都产不出');
|
|
124
|
+
} else {
|
|
125
|
+
check(checks, 'M6', 'pass', 'recipes', `craftRecipeViews ${views.length} 条`);
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
check(checks, 'M6', 'fail', 'recipes', `craftRecipeViews 抛错:${err.message}`);
|
|
129
|
+
}
|
|
130
|
+
for (const [tag, list] of Object.entries(manifest.candidates ?? {})) {
|
|
131
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
132
|
+
check(checks, 'M6', 'fail', tag, 'candidate 为空');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const boss of manifest.bosses ?? []) {
|
|
137
|
+
if (!String(boss).includes(':')) check(checks, 'M7', 'fail', boss, 'boss 不是完整实体 id');
|
|
138
|
+
}
|
|
139
|
+
return checks;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** I1–I6:manifest + ids 的交叉检查;不需要 registry。 */
|
|
143
|
+
export function checkIds(manifest, ids, opts = {}) {
|
|
144
|
+
const checks = [];
|
|
145
|
+
if (!ids || typeof ids !== 'object') {
|
|
146
|
+
check(checks, 'I1', 'fail', 'ids', '没有 ids');
|
|
147
|
+
return checks;
|
|
148
|
+
}
|
|
149
|
+
const blockCeiling = Number(opts.blockStateCeiling ?? V_1_21_1.blockState);
|
|
150
|
+
const itemCeiling = Number(opts.itemCeiling ?? V_1_21_1.item);
|
|
151
|
+
const entityCeiling = Number(opts.entityCeiling ?? V_1_21_1.entity);
|
|
152
|
+
|
|
153
|
+
const declaredBlocks = new Set(Object.keys(manifest.blocks ?? {}));
|
|
154
|
+
const gotBlocks = new Set(Object.keys(ids.blocks ?? {}));
|
|
155
|
+
for (const name of declaredBlocks) if (!gotBlocks.has(name)) check(checks, 'I1', 'warn', name, 'manifest 声明了但服务器没有注册(兼容/技术方块,通常按实测裁剪)');
|
|
156
|
+
for (const name of gotBlocks) if (!declaredBlocks.has(name)) check(checks, 'I1', 'warn', name, 'ids 有但 manifest 没声明');
|
|
157
|
+
|
|
158
|
+
const declaredItems = new Set(Object.keys(manifest.items ?? {}));
|
|
159
|
+
const gotItems = new Set(Object.keys(ids.items ?? {}));
|
|
160
|
+
for (const name of declaredItems) if (!gotItems.has(name)) check(checks, 'I1', 'warn', name, 'manifest 声明了但服务器没有注册这个物品(兼容/技术物品,通常按实测裁剪)');
|
|
161
|
+
|
|
162
|
+
const stateOwner = new Map();
|
|
163
|
+
const ranges = [];
|
|
164
|
+
for (const [name, states] of Object.entries(ids.blocks ?? {})) {
|
|
165
|
+
if (!Array.isArray(states) || states.length === 0) {
|
|
166
|
+
check(checks, 'I2', 'fail', name, 'ids.blocks 为空');
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
let min = Infinity;
|
|
170
|
+
let max = -1;
|
|
171
|
+
for (const state of states) {
|
|
172
|
+
const id = Number(state?.stateId);
|
|
173
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
174
|
+
check(checks, 'I2', 'fail', name, `stateId 非法:${state?.stateId}`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (id <= blockCeiling) {
|
|
178
|
+
check(checks, 'I2', 'fail', name, `stateId ${id} 落在 vanilla 区间(<= ${blockCeiling})`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const owner = stateOwner.get(id);
|
|
182
|
+
if (owner && owner !== name) check(checks, 'I2', 'fail', name, `stateId ${id} 已被 ${owner} 占用`);
|
|
183
|
+
else stateOwner.set(id, name);
|
|
184
|
+
min = Math.min(min, id);
|
|
185
|
+
max = Math.max(max, id);
|
|
186
|
+
}
|
|
187
|
+
if (Number.isFinite(min)) ranges.push({ name, min, max });
|
|
188
|
+
}
|
|
189
|
+
ranges.sort((a, b) => a.min - b.min);
|
|
190
|
+
for (let i = 1; i < ranges.length; i += 1) {
|
|
191
|
+
if (ranges[i].min <= ranges[i - 1].max) {
|
|
192
|
+
check(checks, 'I2', 'fail', ranges[i].name,
|
|
193
|
+
`stateId 区间与 ${ranges[i - 1].name} 重叠:[${ranges[i].min}, ${ranges[i].max}] vs [${ranges[i - 1].min}, ${ranges[i - 1].max}]`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const itemOwner = new Map();
|
|
198
|
+
for (const [name, raw] of Object.entries(ids.items ?? {})) {
|
|
199
|
+
const id = Number(raw);
|
|
200
|
+
if (!Number.isInteger(id) || id <= 0) check(checks, 'I3', 'fail', name, `item id 非法:${raw}`);
|
|
201
|
+
else if (id <= itemCeiling) check(checks, 'I3', 'fail', name, `item id ${id} 落在 vanilla 区间(<= ${itemCeiling})`);
|
|
202
|
+
else {
|
|
203
|
+
const owner = itemOwner.get(id);
|
|
204
|
+
if (owner && owner !== name) check(checks, 'I3', 'fail', name, `item id ${id} 已被 ${owner} 占用`);
|
|
205
|
+
else itemOwner.set(id, name);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const entities = ids.entities ?? {};
|
|
210
|
+
const entityOwner = new Map();
|
|
211
|
+
for (const [name, spec] of Object.entries(entities)) {
|
|
212
|
+
const id = Number(spec?.id);
|
|
213
|
+
if (!Number.isInteger(id) || id <= entityCeiling) {
|
|
214
|
+
check(checks, 'I4', 'fail', name, `entity id ${spec?.id} 落在原版区间(<= ${entityCeiling})或非法`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const owner = entityOwner.get(id);
|
|
218
|
+
if (owner && owner !== name) check(checks, 'I4', 'fail', name, `entity id ${id} 已被 ${owner} 占用`);
|
|
219
|
+
else entityOwner.set(id, name);
|
|
220
|
+
}
|
|
221
|
+
for (const boss of manifest.bosses ?? []) {
|
|
222
|
+
if (!entities[boss]) check(checks, 'I4', 'fail', boss, 'boss 不在 ids.entities 里');
|
|
223
|
+
else if (!entities[boss].boss) check(checks, 'I4', 'fail', boss, 'boss 条目没有 boss:true');
|
|
224
|
+
}
|
|
225
|
+
if ((manifest.bosses ?? []).length > 0 && Object.keys(entities).length === 0) {
|
|
226
|
+
check(checks, 'I4', 'fail', 'entities', '清单有 boss,但 ids 没有 entities(dump mod 太旧?)');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (opts.serverMods) {
|
|
230
|
+
const v = checkServerMods(ids.serverMods, opts.serverMods);
|
|
231
|
+
check(checks, 'I5', v.fatal ? 'fail' : 'pass', 'server-mods', v.lines.join(';') || '指纹检查完成');
|
|
232
|
+
} else if (!ids.serverMods?.fingerprint) {
|
|
233
|
+
check(checks, 'I5', 'warn', 'ids', '没有 serverMods 指纹,换 jar 组合时无法拒绝旧 ids');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!ids.method) check(checks, 'I6', 'warn', 'ids', '缺少 method');
|
|
237
|
+
if (!ids.measuredAt) check(checks, 'I6', 'warn', 'ids', '缺少 measuredAt');
|
|
238
|
+
if (Array.isArray(ids.failed) && ids.failed.length > 0) {
|
|
239
|
+
check(checks, 'I6', 'warn', 'ids', `${ids.failed.length} 项在导入时被剔除(见 ids.failed)`);
|
|
240
|
+
}
|
|
241
|
+
return checks;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function shapeIdsFor(registry, name) {
|
|
245
|
+
return registry.blockCollisionShapes?.blocks?.[name];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function shapeOf(registry, name, stateId) {
|
|
249
|
+
const arr = shapeIdsFor(registry, name);
|
|
250
|
+
const entry = registry.blocksByName?.[name];
|
|
251
|
+
if (!Array.isArray(arr) || !entry) return undefined;
|
|
252
|
+
return arr[stateId - entry.minStateId];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** R1–R7:注入真实 prismarine registry 后检查行为。 */
|
|
256
|
+
export function checkRegistry(manifest, ids, registry, opts = {}) {
|
|
257
|
+
const checks = [];
|
|
258
|
+
if (!registry?.blocksByName || !registry?.blockCollisionShapes) {
|
|
259
|
+
check(checks, 'R1', 'fail', 'registry', '不是可注入的 prismarine registry');
|
|
260
|
+
return checks;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
if (!opts.installed) installModRegistry({ registry }, { manifest, ids });
|
|
264
|
+
} catch (err) {
|
|
265
|
+
check(checks, 'R1', 'fail', 'registry', `注入失败:${err.message}`);
|
|
266
|
+
return checks;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const [name, spec] of Object.entries(manifest.blocks ?? {})) {
|
|
270
|
+
const entry = registry.blocksByName[name];
|
|
271
|
+
if (!entry) {
|
|
272
|
+
check(checks, 'R1', 'fail', name, '注入后 registry 里没有这个方块');
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const shapeArr = shapeIdsFor(registry, name);
|
|
276
|
+
if (!Array.isArray(shapeArr)) {
|
|
277
|
+
check(checks, 'R2', 'fail', name, '没有 blockCollisionShapes 条目');
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const span = entry.maxStateId - entry.minStateId + 1;
|
|
281
|
+
if (shapeArr.length < span) check(checks, 'R2', 'fail', name, `shape 数组 ${shapeArr.length} < state 区间 ${span}`);
|
|
282
|
+
const missing = shapeArr.filter((id) => id === undefined || id === null);
|
|
283
|
+
if (missing.length > 0) check(checks, 'R2', 'fail', name, `${missing.length} 个 state 没有 shape`);
|
|
284
|
+
const empty = shapeArr.filter((id) => (registry.blockCollisionShapes.shapes[id]?.length ?? 0) === 0).length;
|
|
285
|
+
if (spec.solid !== false && empty === shapeArr.length && shapeArr.length > 0) {
|
|
286
|
+
check(checks, 'R3', 'fail', name, 'solid 方块的所有 state 都是空 shape(会被当空气)');
|
|
287
|
+
} else if (empty > 0 && spec.solid !== false && !/_fence_gate/.test(name)) {
|
|
288
|
+
check(checks, 'R3', 'warn', name, `${empty}/${shapeArr.length} 个 state 是空 shape`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const tags = spec.tags ?? [];
|
|
292
|
+
const cropLike = tags.includes('crops') || CROPISH.test(name);
|
|
293
|
+
if (cropLike) {
|
|
294
|
+
if (entry.diggable !== true) check(checks, 'R4', 'fail', name, '作物不可挖(diggable:false)');
|
|
295
|
+
else if (entry.harvestTools && Object.keys(entry.harvestTools).length === 0) {
|
|
296
|
+
check(checks, 'R4', 'fail', name, 'harvestTools 是空对象,canHarvest 会永远 false');
|
|
297
|
+
} else {
|
|
298
|
+
check(checks, 'R4', 'pass', name, '作物可挖');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const short = name.split(':')[1] ?? name;
|
|
303
|
+
if (/_stairs/.test(short)) {
|
|
304
|
+
const byHalf = new Map();
|
|
305
|
+
for (const state of ids.blocks?.[name] ?? []) {
|
|
306
|
+
const half = state?.props?.half;
|
|
307
|
+
if (!half) continue;
|
|
308
|
+
const sid = shapeOf(registry, name, Number(state.stateId));
|
|
309
|
+
const shape = registry.blockCollisionShapes.shapes[sid];
|
|
310
|
+
const key = JSON.stringify(shape ?? []);
|
|
311
|
+
if (!byHalf.has(half)) byHalf.set(half, new Set());
|
|
312
|
+
byHalf.get(half).add(key);
|
|
313
|
+
}
|
|
314
|
+
const top = byHalf.get('top');
|
|
315
|
+
const bottom = byHalf.get('bottom');
|
|
316
|
+
if (top && bottom && JSON.stringify([...top].sort()) === JSON.stringify([...bottom].sort())) {
|
|
317
|
+
check(checks, 'R5', 'fail', name, '楼梯 top/bottom 的 shape 完全相同,形状映射没生效');
|
|
318
|
+
} else if (!top || !bottom) {
|
|
319
|
+
check(checks, 'R5', 'warn', name, '楼梯缺少 top 或 bottom state,无法核对');
|
|
320
|
+
} else {
|
|
321
|
+
check(checks, 'R5', 'pass', name, '楼梯 top/bottom shape 不同');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const views = craftRecipeViews(manifest);
|
|
328
|
+
if ((manifest.recipes ?? []).length > 0 && views.length === 0) {
|
|
329
|
+
check(checks, 'R6', 'fail', 'recipes', '注入后仍没有可消费配方');
|
|
330
|
+
} else {
|
|
331
|
+
check(checks, 'R6', 'pass', 'recipes', `${views.length} 条可消费配方`);
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
check(checks, 'R6', 'fail', 'recipes', `配方视图失败:${err.message}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
for (const [name, spec] of Object.entries(ids.entities ?? {})) {
|
|
338
|
+
const e = registry.entitiesByName?.[name];
|
|
339
|
+
if (!e) check(checks, 'R7', 'fail', name, '实体没有注入 registry');
|
|
340
|
+
else if (spec.boss && !e.boss) check(checks, 'R7', 'fail', name, 'boss 条目没有 boss 标记');
|
|
341
|
+
}
|
|
342
|
+
if (Object.keys(ids.entities ?? {}).length > 0) {
|
|
343
|
+
check(checks, 'R7', 'pass', 'entities', `${Object.keys(ids.entities).length} 个实体已注入`);
|
|
344
|
+
}
|
|
345
|
+
return checks;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function summarize(checks) {
|
|
349
|
+
const summary = { pass: 0, warn: 0, fail: 0, total: checks.length };
|
|
350
|
+
for (const c of checks) {
|
|
351
|
+
if (c.level === 'fail') summary.fail += 1;
|
|
352
|
+
else if (c.level === 'warn') summary.warn += 1;
|
|
353
|
+
else summary.pass += 1;
|
|
354
|
+
}
|
|
355
|
+
return summary;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function runConformance({ manifest, ids, registry, serverMods, mcv = '1.21.1' }) {
|
|
359
|
+
const checks = checkManifest(manifest);
|
|
360
|
+
if (ids) {
|
|
361
|
+
const ceiling = vanillaBlockStateCeiling(mcv) || V_1_21_1.blockState;
|
|
362
|
+
checks.push(...checkIds(manifest, ids, { blockStateCeiling: ceiling, serverMods }));
|
|
363
|
+
if (registry) checks.push(...checkRegistry(manifest, ids, registry));
|
|
364
|
+
}
|
|
365
|
+
return { pack: manifest?.mod ?? manifest?.source ?? 'unknown', summary: summarize(checks), checks };
|
|
366
|
+
}
|