@sfmc-bds/bds-tools 0.2.0-beta.12 → 0.2.0-beta.14

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.
@@ -34,11 +34,23 @@ export interface NbtTagMeta {
34
34
  endOffset: number;
35
35
  value?: unknown;
36
36
  }
37
+ export interface KnownExperimentDefinition {
38
+ id: string;
39
+ nbtKey: string;
40
+ nameZh: string;
41
+ nameEn: string;
42
+ aliases?: string[];
43
+ isRootTag?: boolean;
44
+ extraExpTags?: string[];
45
+ }
46
+ export declare const KNOWN_EXPERIMENTS: readonly KnownExperimentDefinition[];
47
+ export declare function resolveExperimentDefinition(query: string): KnownExperimentDefinition | null;
37
48
  export interface LevelDatExperimentsInfo {
38
49
  experimentsEverUsed: boolean;
39
50
  savedWithToggledExperiments: boolean;
40
51
  experiments: Record<string, number | boolean>;
41
52
  hasBetaApis: boolean;
53
+ knownExperiments: Record<string, boolean>;
42
54
  }
43
55
  export interface LevelDatMutateResult {
44
56
  success: boolean;
@@ -46,6 +58,7 @@ export interface LevelDatMutateResult {
46
58
  backupPath?: string;
47
59
  error?: string;
48
60
  previousBetaApisState?: boolean;
61
+ enabledExperiments?: string[];
49
62
  }
50
63
  /**
51
64
  * 遍历并跳过指定类型的 NBT 载荷,返回下一个 tag 的 offset。
@@ -73,15 +86,20 @@ export declare function parseBedrockLevelDat(buf: Buffer): {
73
86
  */
74
87
  export declare function readLevelDatExperiments(levelDatPath: string): LevelDatExperimentsInfo | null;
75
88
  /**
76
- * 为指定 level.dat 开启 Beta APIs(gametest)。
89
+ * 为指定 level.dat 开启指定的实验性开关(若未指定或传 "all" 则开启全部已知实验性开关)。
77
90
  *
78
91
  * 保证:
79
92
  * - 自动备份原文件至 `${levelDatPath}.bak`;
80
- * - 自动更新 experiments 复合标签中的 gametest = 1b;
93
+ * - 自动更新 root 与 experiments 复合标签中的目标 byte tags;
81
94
  * - 自动更新 experiments_ever_used = 1b 与 saved_with_toggled_experiments = 1b;
82
95
  * - 精确校准 8 字节文件头部的 dataLength;
83
96
  * - 原子写操作(.tmp -> rename);
84
- * - 若已处于开启状态,直接返回 changed: false,不破坏文件。
97
+ * - 若所请求开关已全部处于开启状态,直接返回 changed: false,不破坏文件。
98
+ */
99
+ export declare function enableExperimentsInLevelDat(levelDatPath: string, targetExperimentIds?: string[] | "all"): Promise<LevelDatMutateResult>;
100
+ /**
101
+ * 为指定 level.dat 开启 Beta APIs(gametest)。
102
+ * 保持向后兼容,委托给 enableExperimentsInLevelDat。
85
103
  */
86
104
  export declare function enableBetaApisInLevelDat(levelDatPath: string): Promise<LevelDatMutateResult>;
87
105
  //# sourceMappingURL=level-dat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"level-dat.d.ts","sourceRoot":"","sources":["../src/level-dat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,2BAA2B,EAAE,OAAO,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;IAC9C,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA2DhF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,mBAAmB,EAAE,MAAM,GAC1B;IAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CA6ClE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;CACvB,CAwBA;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAuD5F;AAkCD;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA+HlG"}
1
+ {"version":3,"file":"level-dat.d.ts","sourceRoot":"","sources":["../src/level-dat.ts"],"names":[],"mappings":"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"}
package/dist/level-dat.js CHANGED
@@ -12,6 +12,82 @@ const TAG_LIST = 9;
12
12
  const TAG_COMPOUND = 10;
13
13
  const TAG_INT_ARRAY = 11;
14
14
  const TAG_LONG_ARRAY = 12;
15
+ const KNOWN_EXPERIMENTS = [
16
+ {
17
+ id: "gametest",
18
+ nbtKey: "gametest",
19
+ nameZh: "\u6D4B\u8BD5\u7248 API",
20
+ nameEn: "Beta APIs (gametest)",
21
+ aliases: ["beta", "beta_apis", "betapis", "gametest"]
22
+ },
23
+ {
24
+ id: "upcoming_creator_features",
25
+ nbtKey: "upcoming_creator_features",
26
+ nameZh: "\u5373\u5C06\u63A8\u51FA\u7684\u521B\u4F5C\u8005\u529F\u80FD",
27
+ nameEn: "Upcoming Creator Features",
28
+ aliases: ["upcoming", "creator_features", "upcoming_features"]
29
+ },
30
+ {
31
+ id: "experimental_creator_cameras",
32
+ nbtKey: "experimental_creator_cameras",
33
+ nameZh: "\u521B\u5EFA\u8005\u7167\u76F8\u673A\u7684\u5B9E\u9A8C\u6027\u529F\u80FD",
34
+ nameEn: "Experimental Creator Cameras",
35
+ aliases: ["camera", "cameras", "creator_cameras", "creator_camera"]
36
+ },
37
+ {
38
+ id: "voxel_shapes",
39
+ nbtKey: "voxel_shapes",
40
+ nameZh: "\u5B9E\u9A8C\u6027Voxel\u5F62\u72B6\u7279\u5F81",
41
+ nameEn: "Experimental Voxel Shapes",
42
+ aliases: ["voxel", "voxel_shape", "experimental_voxel_shapes", "voxelshapes"]
43
+ },
44
+ {
45
+ id: "villager_trades_rebalance",
46
+ nbtKey: "villager_trades_rebalance",
47
+ nameZh: "\u6751\u6C11\u8D38\u6613\u518D\u5E73\u8861",
48
+ nameEn: "Villager Trade Rebalance",
49
+ aliases: ["villager", "trades", "villager_trades", "trade_rebalance"]
50
+ },
51
+ {
52
+ id: "drop_3_2026",
53
+ nbtKey: "drop_3_2026",
54
+ nameZh: "2026\u5E74\u7B2C3\u6B21\u66F4\u65B0",
55
+ nameEn: "Drop 3 of 2026",
56
+ aliases: [
57
+ "drop3",
58
+ "drop_3",
59
+ "drop_3_2026",
60
+ "wilderness",
61
+ "wilderness_bound",
62
+ "update_2026_drop_3",
63
+ "update_26_3",
64
+ "update_drop_3"
65
+ ],
66
+ extraExpTags: ["update_2026_drop_3"]
67
+ },
68
+ {
69
+ id: "education",
70
+ nbtKey: "educationFeaturesEnabled",
71
+ nameZh: "Minecraft Education \u529F\u80FD",
72
+ nameEn: "Minecraft Education Features",
73
+ isRootTag: true,
74
+ aliases: ["edu", "education", "education_edition", "educationfeatures"],
75
+ extraExpTags: ["education"]
76
+ }
77
+ ];
78
+ function resolveExperimentDefinition(query) {
79
+ const q = query.trim().toLowerCase().replace(/[-_]/g, "");
80
+ for (const def of KNOWN_EXPERIMENTS) {
81
+ if (def.id.replace(/[-_]/g, "") === q) return def;
82
+ if (def.nbtKey.replace(/[-_]/g, "") === q) return def;
83
+ if (def.aliases) {
84
+ for (const a of def.aliases) {
85
+ if (a.replace(/[-_]/g, "") === q) return def;
86
+ }
87
+ }
88
+ }
89
+ return null;
90
+ }
15
91
  function skipNbtPayload(buf, offset, type) {
16
92
  switch (type) {
17
93
  case TAG_BYTE:
@@ -172,11 +248,30 @@ function readLevelDatExperiments(levelDatPath) {
172
248
  }
173
249
  }
174
250
  }
251
+ const knownExperiments = {};
252
+ for (const def of KNOWN_EXPERIMENTS) {
253
+ let enabled = false;
254
+ if (def.isRootTag) {
255
+ const rootTag = parsed.rootChildren.get(def.nbtKey);
256
+ if (rootTag && Number(rootTag.value) === 1) {
257
+ enabled = true;
258
+ }
259
+ }
260
+ const keysToCheck = [def.nbtKey, ...def.aliases ?? []];
261
+ for (const k of keysToCheck) {
262
+ if (experiments[k] === 1) {
263
+ enabled = true;
264
+ break;
265
+ }
266
+ }
267
+ knownExperiments[def.id] = enabled;
268
+ }
175
269
  return {
176
270
  experimentsEverUsed,
177
271
  savedWithToggledExperiments,
178
272
  experiments,
179
- hasBetaApis
273
+ hasBetaApis,
274
+ knownExperiments
180
275
  };
181
276
  } catch {
182
277
  return null;
@@ -204,83 +299,145 @@ function createCompoundTagBuffer(name, byteTags) {
204
299
  nameBuf.copy(header, 3);
205
300
  return Buffer.concat([header, ...childBuffers]);
206
301
  }
207
- async function enableBetaApisInLevelDat(levelDatPath) {
302
+ async function enableExperimentsInLevelDat(levelDatPath, targetExperimentIds) {
208
303
  if (!fs.existsSync(levelDatPath)) {
209
304
  return { success: false, changed: false, error: `File not found: ${levelDatPath}` };
210
305
  }
211
306
  try {
212
307
  const raw = await fs.promises.readFile(levelDatPath);
213
308
  const parsed = parseBedrockLevelDat(raw);
309
+ let targetDefs = [];
310
+ if (!targetExperimentIds || targetExperimentIds === "all") {
311
+ targetDefs = [...KNOWN_EXPERIMENTS];
312
+ } else {
313
+ const seen = /* @__PURE__ */ new Set();
314
+ for (const id of targetExperimentIds) {
315
+ const def = resolveExperimentDefinition(id);
316
+ if (def && !seen.has(def.id)) {
317
+ seen.add(def.id);
318
+ targetDefs.push(def);
319
+ }
320
+ }
321
+ if (targetDefs.length === 0) {
322
+ return { success: true, changed: false, enabledExperiments: [] };
323
+ }
324
+ }
214
325
  const expCompoundTag = parsed.rootChildren.get("experiments");
215
- let currentBetaState = false;
216
- if (expCompoundTag && expCompoundTag.type === TAG_COMPOUND) {
217
- const expChildren = parseCompoundChildren(raw, expCompoundTag.valueOffset).children;
218
- const gametest = expChildren.get("gametest");
219
- if (gametest && Number(gametest.value) === 1) {
220
- currentBetaState = true;
326
+ const existingExpChildren = expCompoundTag && expCompoundTag.type === TAG_COMPOUND ? parseCompoundChildren(raw, expCompoundTag.valueOffset).children : /* @__PURE__ */ new Map();
327
+ let needsChange = false;
328
+ const enabledIds = [];
329
+ if (!expCompoundTag) {
330
+ needsChange = true;
331
+ } else {
332
+ const expEverUsed = existingExpChildren.get("experiments_ever_used");
333
+ if (!expEverUsed || Number(expEverUsed.value) !== 1) needsChange = true;
334
+ const expSaved = existingExpChildren.get("saved_with_toggled_experiments");
335
+ if (!expSaved || Number(expSaved.value) !== 1) needsChange = true;
336
+ }
337
+ const rootEverUsed = parsed.rootChildren.get("experiments_ever_used");
338
+ if (rootEverUsed && Number(rootEverUsed.value) !== 1) needsChange = true;
339
+ const rootSaved = parsed.rootChildren.get("saved_with_toggled_experiments");
340
+ if (rootSaved && Number(rootSaved.value) !== 1) needsChange = true;
341
+ let previousBetaApisState = false;
342
+ const currentGametest = existingExpChildren.get("gametest");
343
+ if (currentGametest && Number(currentGametest.value) === 1) {
344
+ previousBetaApisState = true;
345
+ }
346
+ for (const def of targetDefs) {
347
+ enabledIds.push(def.id);
348
+ if (def.isRootTag) {
349
+ const rootTag = parsed.rootChildren.get(def.nbtKey);
350
+ if (!rootTag || Number(rootTag.value) !== 1) {
351
+ needsChange = true;
352
+ }
353
+ }
354
+ if (!def.isRootTag) {
355
+ const tag = existingExpChildren.get(def.nbtKey);
356
+ if (!tag || Number(tag.value) !== 1) {
357
+ needsChange = true;
358
+ }
359
+ }
360
+ if (def.extraExpTags) {
361
+ for (const extra of def.extraExpTags) {
362
+ const tag = existingExpChildren.get(extra);
363
+ if (!tag || Number(tag.value) !== 1) {
364
+ needsChange = true;
365
+ }
366
+ }
221
367
  }
222
368
  }
223
- if (currentBetaState) {
369
+ if (!needsChange) {
224
370
  return {
225
371
  success: true,
226
372
  changed: false,
227
- previousBetaApisState: true
373
+ previousBetaApisState,
374
+ enabledExperiments: enabledIds
228
375
  };
229
376
  }
230
377
  const backupPath = `${levelDatPath}.bak`;
231
378
  await fs.promises.copyFile(levelDatPath, backupPath);
379
+ let mutatedRaw = Buffer.from(raw);
232
380
  let nbtBody;
233
- if (!expCompoundTag) {
234
- const insertCompoundBuf = createCompoundTagBuffer("experiments", {
235
- experiments_ever_used: 1,
236
- saved_with_toggled_experiments: 1,
237
- gametest: 1
238
- });
239
- const rootEndTagIndex = parsed.rootEndOffset - 1;
240
- const beforeEnd = raw.subarray(8, rootEndTagIndex);
241
- const afterEnd = raw.subarray(rootEndTagIndex);
242
- nbtBody = Buffer.concat([beforeEnd, insertCompoundBuf, afterEnd]);
243
- } else {
244
- const { children: expChildren, compoundEndOffset: expEndOffset } = parseCompoundChildren(
245
- raw,
246
- expCompoundTag.valueOffset
247
- );
248
- const expEndTagIndex = expEndOffset - 1;
249
- const toAppend = [];
250
- let mutatedRaw = Buffer.from(raw);
251
- const gametestTag = expChildren.get("gametest");
252
- if (gametestTag) {
253
- mutatedRaw.writeInt8(1, gametestTag.valueOffset);
254
- } else {
255
- toAppend.push(createByteTagBuffer("gametest", 1));
256
- }
257
- const everUsedTag = expChildren.get("experiments_ever_used");
258
- if (everUsedTag) {
259
- mutatedRaw.writeInt8(1, everUsedTag.valueOffset);
260
- } else {
261
- toAppend.push(createByteTagBuffer("experiments_ever_used", 1));
381
+ const requiredExpTags = {
382
+ experiments_ever_used: 1,
383
+ saved_with_toggled_experiments: 1
384
+ };
385
+ for (const def of targetDefs) {
386
+ if (!def.isRootTag) {
387
+ requiredExpTags[def.nbtKey] = 1;
262
388
  }
263
- const savedTag = expChildren.get("saved_with_toggled_experiments");
264
- if (savedTag) {
265
- mutatedRaw.writeInt8(1, savedTag.valueOffset);
266
- } else {
267
- toAppend.push(createByteTagBuffer("saved_with_toggled_experiments", 1));
389
+ if (def.extraExpTags) {
390
+ for (const extra of def.extraExpTags) {
391
+ requiredExpTags[extra] = 1;
392
+ }
268
393
  }
269
- const rootEverUsed = parsed.rootChildren.get("experiments_ever_used");
270
- if (rootEverUsed) {
271
- mutatedRaw.writeInt8(1, rootEverUsed.valueOffset);
394
+ }
395
+ const rootNeedsEducation = targetDefs.some((d) => d.id === "education");
396
+ if (rootEverUsed) {
397
+ mutatedRaw.writeInt8(1, rootEverUsed.valueOffset);
398
+ }
399
+ if (rootSaved) {
400
+ mutatedRaw.writeInt8(1, rootSaved.valueOffset);
401
+ }
402
+ if (rootNeedsEducation) {
403
+ const eduTag = parsed.rootChildren.get("educationFeaturesEnabled");
404
+ if (eduTag) {
405
+ mutatedRaw.writeInt8(1, eduTag.valueOffset);
272
406
  }
273
- const rootSaved = parsed.rootChildren.get("saved_with_toggled_experiments");
274
- if (rootSaved) {
275
- mutatedRaw.writeInt8(1, rootSaved.valueOffset);
407
+ const optOut = parsed.rootChildren.get("has_opted_out_of_education");
408
+ if (optOut) {
409
+ mutatedRaw.writeInt8(0, optOut.valueOffset);
276
410
  }
277
- if (toAppend.length > 0) {
278
- const beforeExpEnd = mutatedRaw.subarray(8, expEndTagIndex);
279
- const afterExpEnd = mutatedRaw.subarray(expEndTagIndex);
280
- nbtBody = Buffer.concat([beforeExpEnd, ...toAppend, afterExpEnd]);
281
- } else {
282
- nbtBody = mutatedRaw.subarray(8);
411
+ }
412
+ const rootTagsToAppend = [];
413
+ if (rootNeedsEducation && !parsed.rootChildren.has("educationFeaturesEnabled")) {
414
+ rootTagsToAppend.push(createByteTagBuffer("educationFeaturesEnabled", 1));
415
+ }
416
+ if (!expCompoundTag) {
417
+ const newExpCompoundBuf = createCompoundTagBuffer("experiments", requiredExpTags);
418
+ const rootEndTagIndex = parsed.rootEndOffset - 1;
419
+ const beforeEnd = mutatedRaw.subarray(8, rootEndTagIndex);
420
+ const afterEnd = mutatedRaw.subarray(rootEndTagIndex);
421
+ nbtBody = Buffer.concat([beforeEnd, ...rootTagsToAppend, newExpCompoundBuf, afterEnd]);
422
+ } else {
423
+ const { compoundEndOffset: expEndOffset } = parseCompoundChildren(raw, expCompoundTag.valueOffset);
424
+ const expEndTagIndex = expEndOffset - 1;
425
+ const rootEndTagIndex = parsed.rootEndOffset - 1;
426
+ const expTagsToAppend = [];
427
+ for (const [name, val] of Object.entries(requiredExpTags)) {
428
+ const existing = existingExpChildren.get(name);
429
+ if (existing) {
430
+ mutatedRaw.writeInt8(val, existing.valueOffset);
431
+ } else {
432
+ expTagsToAppend.push(createByteTagBuffer(name, val));
433
+ }
283
434
  }
435
+ const part1 = mutatedRaw.subarray(8, expEndTagIndex);
436
+ const part2 = Buffer.concat(expTagsToAppend);
437
+ const part3 = mutatedRaw.subarray(expEndTagIndex, rootEndTagIndex);
438
+ const part4 = Buffer.concat(rootTagsToAppend);
439
+ const part5 = mutatedRaw.subarray(rootEndTagIndex);
440
+ nbtBody = Buffer.concat([part1, part2, part3, part4, part5]);
284
441
  }
285
442
  const newHeader = Buffer.alloc(8);
286
443
  newHeader.writeUInt32LE(parsed.version, 0);
@@ -293,7 +450,8 @@ async function enableBetaApisInLevelDat(levelDatPath) {
293
450
  success: true,
294
451
  changed: true,
295
452
  backupPath,
296
- previousBetaApisState: false
453
+ previousBetaApisState,
454
+ enabledExperiments: enabledIds
297
455
  };
298
456
  } catch (err) {
299
457
  return {
@@ -303,7 +461,11 @@ async function enableBetaApisInLevelDat(levelDatPath) {
303
461
  };
304
462
  }
305
463
  }
464
+ async function enableBetaApisInLevelDat(levelDatPath) {
465
+ return await enableExperimentsInLevelDat(levelDatPath, ["gametest"]);
466
+ }
306
467
  export {
468
+ KNOWN_EXPERIMENTS,
307
469
  TAG_BYTE,
308
470
  TAG_BYTE_ARRAY,
309
471
  TAG_COMPOUND,
@@ -318,9 +480,11 @@ export {
318
480
  TAG_SHORT,
319
481
  TAG_STRING,
320
482
  enableBetaApisInLevelDat,
483
+ enableExperimentsInLevelDat,
321
484
  parseBedrockLevelDat,
322
485
  parseCompoundChildren,
323
486
  readLevelDatExperiments,
487
+ resolveExperimentDefinition,
324
488
  skipNbtPayload
325
489
  };
326
490
  //# sourceMappingURL=level-dat.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/level-dat.ts"],
4
- "sourcesContent": ["/**\n * level-dat.ts \u2014 Minecraft \u57FA\u5CA9\u7248\u4E16\u754C\u5B58\u6863 level.dat \u5C0F\u7AEF\u5E8F NBT \u89E3\u6790\u4E0E\u5B9E\u9A8C\u6027\u73A9\u6CD5\u5B89\u5168\u4FEE\u6539\u5668\n *\n * \u57FA\u5CA9\u7248 level.dat \u89C4\u8303\uFF1A\n * - Header\uFF088 \u5B57\u8282\uFF09\uFF1A\n * - [0..3] uint32 LE\uFF1A\u5B58\u50A8\u683C\u5F0F\u7248\u672C\uFF08\u5F53\u524D\u7248\u672C\u901A\u5E38\u4E3A 10\uFF09\n * - [4..7] uint32 LE\uFF1A\u6709\u6548 NBT \u6570\u636E\u5B57\u8282\u957F\u5EA6\uFF08data_length\uFF09\n * - Payload\uFF1A\u672A\u538B\u7F29\u7684\u5C0F\u7AEF\u5E8F\uFF08Little-Endian\uFF09\u6807\u51C6 NBT \u590D\u5408\u6807\u7B7E\uFF08TAG_Compound\uFF09\n *\n * \u6838\u5FC3\u80FD\u529B\uFF1A\n * - \u539F\u751F\u65E0\u5916\u90E8\u9ED1\u76D2\u4F9D\u8D56\uFF0CBuffer \u7EA7\u89E3\u6790\u4E0E\u7CBE\u786E\u5B57\u8282\u4F4D\u79FB\n * - \u8BFB\u53D6\u4E16\u754C\u5B9E\u9A8C\u6027\u73A9\u6CD5\u7684\u5F00\u542F\u72B6\u6001\uFF08\u7279\u522B\u662F Beta APIs / gametest\uFF09\n * - \u5B89\u5168\u6CE8\u5165\u6216\u5207\u6362\u5B9E\u9A8C\u6027\u5F00\u5173\u5E76\u81EA\u52A8\u5907\u4EFD\u539F\u6587\u4EF6\uFF08.bak\uFF09\u4E0E\u6821\u51C6 Header data_length\n */\n\nimport fs from \"node:fs\";\n\nexport const TAG_END = 0;\nexport const TAG_BYTE = 1;\nexport const TAG_SHORT = 2;\nexport const TAG_INT = 3;\nexport const TAG_LONG = 4;\nexport const TAG_FLOAT = 5;\nexport const TAG_DOUBLE = 6;\nexport const TAG_BYTE_ARRAY = 7;\nexport const TAG_STRING = 8;\nexport const TAG_LIST = 9;\nexport const TAG_COMPOUND = 10;\nexport const TAG_INT_ARRAY = 11;\nexport const TAG_LONG_ARRAY = 12;\n\nexport interface NbtTagMeta {\n type: number;\n name: string;\n startOffset: number; // \u5305\u542B tagType \u7684\u8D77\u59CB\u7EDD\u5BF9 offset\n nameOffset: number; // \u6807\u7B7E\u540D\u79F0\u8D77\u59CB offset\n valueOffset: number; // \u8F7D\u8377\u8D77\u59CB offset\n endOffset: number; // \u6807\u7B7E\u7ED3\u675F\u7EDD\u5BF9 offset\n value?: unknown;\n}\n\nexport interface LevelDatExperimentsInfo {\n experimentsEverUsed: boolean;\n savedWithToggledExperiments: boolean;\n experiments: Record<string, number | boolean>;\n hasBetaApis: boolean;\n}\n\nexport interface LevelDatMutateResult {\n success: boolean;\n changed: boolean;\n backupPath?: string;\n error?: string;\n previousBetaApisState?: boolean;\n}\n\n/**\n * \u904D\u5386\u5E76\u8DF3\u8FC7\u6307\u5B9A\u7C7B\u578B\u7684 NBT \u8F7D\u8377\uFF0C\u8FD4\u56DE\u4E0B\u4E00\u4E2A tag \u7684 offset\u3002\n */\nexport function skipNbtPayload(buf: Buffer, offset: number, type: number): number {\n switch (type) {\n case TAG_BYTE:\n return offset + 1;\n case TAG_SHORT:\n return offset + 2;\n case TAG_INT:\n case TAG_FLOAT:\n return offset + 4;\n case TAG_LONG:\n case TAG_DOUBLE:\n return offset + 8;\n case TAG_BYTE_ARRAY: {\n if (offset + 4 > buf.length) throw new Error(\"Unexpected EOF reading TAG_Byte_Array length\");\n const len = buf.readInt32LE(offset);\n return offset + 4 + Math.max(0, len);\n }\n case TAG_STRING: {\n if (offset + 2 > buf.length) throw new Error(\"Unexpected EOF reading TAG_String length\");\n const len = buf.readUInt16LE(offset);\n return offset + 2 + len;\n }\n case TAG_LIST: {\n if (offset + 5 > buf.length) throw new Error(\"Unexpected EOF reading TAG_List header\");\n const elemType = buf.readUInt8(offset);\n const count = buf.readInt32LE(offset + 1);\n let cur = offset + 5;\n for (let i = 0; i < count; i++) {\n cur = skipNbtPayload(buf, cur, elemType);\n }\n return cur;\n }\n case TAG_COMPOUND: {\n let cur = offset;\n while (cur < buf.length) {\n const t = buf.readUInt8(cur);\n if (t === TAG_END) {\n return cur + 1;\n }\n if (cur + 3 > buf.length) throw new Error(\"Unexpected EOF reading compound child header\");\n const nameLen = buf.readUInt16LE(cur + 1);\n const childValOffset = cur + 3 + nameLen;\n cur = skipNbtPayload(buf, childValOffset, t);\n }\n return cur;\n }\n case TAG_INT_ARRAY: {\n if (offset + 4 > buf.length) throw new Error(\"Unexpected EOF reading TAG_Int_Array length\");\n const len = buf.readInt32LE(offset);\n return offset + 4 + Math.max(0, len) * 4;\n }\n case TAG_LONG_ARRAY: {\n if (offset + 4 > buf.length) throw new Error(\"Unexpected EOF reading TAG_Long_Array length\");\n const len = buf.readInt32LE(offset);\n return offset + 4 + Math.max(0, len) * 8;\n }\n default:\n throw new Error(`Unsupported or unknown NBT tag type: ${type} at offset ${offset}`);\n }\n}\n\n/**\n * \u89E3\u6790\u590D\u5408\u6807\u7B7E\u5185\u90E8\u6240\u6709\u76F4\u63A5\u5B50\u6807\u7B7E\u7684\u5143\u6570\u636E\u6620\u5C04\u3002\n */\nexport function parseCompoundChildren(\n buf: Buffer,\n compoundValueOffset: number\n): { children: Map<string, NbtTagMeta>; compoundEndOffset: number } {\n const children = new Map<string, NbtTagMeta>();\n let cur = compoundValueOffset;\n\n while (cur < buf.length) {\n const tagType = buf.readUInt8(cur);\n if (tagType === TAG_END) {\n return { children, compoundEndOffset: cur + 1 };\n }\n if (cur + 3 > buf.length) {\n throw new Error(`Unexpected EOF reading named tag header at offset ${cur}`);\n }\n const nameLen = buf.readUInt16LE(cur + 1);\n const nameOffset = cur + 3;\n if (nameOffset + nameLen > buf.length) {\n throw new Error(`Unexpected EOF reading tag name at offset ${cur}`);\n }\n const name = buf.toString(\"utf8\", nameOffset, nameOffset + nameLen);\n const valOffset = nameOffset + nameLen;\n const endOffset = skipNbtPayload(buf, valOffset, tagType);\n\n let val: unknown = undefined;\n if (tagType === TAG_BYTE) {\n val = buf.readInt8(valOffset);\n } else if (tagType === TAG_INT) {\n val = buf.readInt32LE(valOffset);\n } else if (tagType === TAG_STRING) {\n const sLen = buf.readUInt16LE(valOffset);\n val = buf.toString(\"utf8\", valOffset + 2, valOffset + 2 + sLen);\n }\n\n children.set(name, {\n type: tagType,\n name,\n startOffset: cur,\n nameOffset,\n valueOffset: valOffset,\n endOffset,\n value: val,\n });\n\n cur = endOffset;\n }\n\n return { children, compoundEndOffset: cur };\n}\n\n/**\n * \u89E3\u6790\u57FA\u5CA9\u7248 level.dat \u7684\u5B8C\u6574\u6587\u4EF6\u7ED3\u6784\u5E76\u5B9A\u4F4D\u6839 Compound\u3002\n */\nexport function parseBedrockLevelDat(buf: Buffer): {\n version: number;\n dataLength: number;\n rootCompoundName: string;\n rootChildren: Map<string, NbtTagMeta>;\n rootEndOffset: number;\n} {\n if (buf.length < 8) {\n throw new Error(\"Invalid level.dat: buffer smaller than 8-byte header\");\n }\n const version = buf.readUInt32LE(0);\n const dataLength = buf.readUInt32LE(4);\n\n // \u6839\u6807\u7B7E\u5FC5\u987B\u662F TAG_Compound\n if (buf.length < 9 || buf.readUInt8(8) !== TAG_COMPOUND) {\n throw new Error(\"Invalid level.dat: root tag is not TAG_Compound\");\n }\n const rootNameLen = buf.readUInt16LE(9);\n const rootCompoundName = buf.toString(\"utf8\", 11, 11 + rootNameLen);\n const rootValueOffset = 11 + rootNameLen;\n\n const { children: rootChildren, compoundEndOffset: rootEndOffset } = parseCompoundChildren(buf, rootValueOffset);\n\n return {\n version,\n dataLength,\n rootCompoundName,\n rootChildren,\n rootEndOffset,\n };\n}\n\n/**\n * \u8BFB\u53D6\u4E16\u754C\u5B58\u6863 level.dat \u7684\u5B9E\u9A8C\u6027\u914D\u7F6E\u3002\u82E5\u89E3\u6790\u5931\u8D25\u6216\u6587\u4EF6\u4E0D\u5B58\u5728\u8FD4\u56DE null\u3002\n */\nexport function readLevelDatExperiments(levelDatPath: string): LevelDatExperimentsInfo | null {\n if (!fs.existsSync(levelDatPath)) return null;\n try {\n const buf = fs.readFileSync(levelDatPath);\n const parsed = parseBedrockLevelDat(buf);\n\n let experimentsEverUsed = false;\n const everUsedTag = parsed.rootChildren.get(\"experiments_ever_used\");\n if (everUsedTag && everUsedTag.type === TAG_BYTE && Number(everUsedTag.value) === 1) {\n experimentsEverUsed = true;\n }\n\n let savedWithToggledExperiments = false;\n const savedTag = parsed.rootChildren.get(\"saved_with_toggled_experiments\");\n if (savedTag && savedTag.type === TAG_BYTE && Number(savedTag.value) === 1) {\n savedWithToggledExperiments = true;\n }\n\n const experiments: Record<string, number | boolean> = {};\n let hasBetaApis = false;\n\n const expCompoundTag = parsed.rootChildren.get(\"experiments\");\n if (expCompoundTag && expCompoundTag.type === TAG_COMPOUND) {\n const expChildren = parseCompoundChildren(buf, expCompoundTag.valueOffset).children;\n\n // \u90E8\u5206\u7248\u672C experiments_ever_used \u4E5F\u53EF\u80FD\u4F4D\u4E8E experiments \u5185\u90E8\n const innerEverUsed = expChildren.get(\"experiments_ever_used\");\n if (innerEverUsed && Number(innerEverUsed.value) === 1) {\n experimentsEverUsed = true;\n }\n const innerSaved = expChildren.get(\"saved_with_toggled_experiments\");\n if (innerSaved && Number(innerSaved.value) === 1) {\n savedWithToggledExperiments = true;\n }\n\n for (const [name, meta] of expChildren.entries()) {\n if (meta.type === TAG_BYTE) {\n const num = Number(meta.value);\n experiments[name] = num;\n if (name === \"gametest\" && num === 1) {\n hasBetaApis = true;\n }\n }\n }\n }\n\n return {\n experimentsEverUsed,\n savedWithToggledExperiments,\n experiments,\n hasBetaApis,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * \u6784\u9020\u4E00\u4E2A TAG_Byte \u7684\u4E8C\u8FDB\u5236 Buffer\u3002\n */\nfunction createByteTagBuffer(name: string, value: number): Buffer {\n const nameBuf = Buffer.from(name, \"utf8\");\n const buf = Buffer.alloc(1 + 2 + nameBuf.length + 1);\n buf.writeUInt8(TAG_BYTE, 0);\n buf.writeUInt16LE(nameBuf.length, 1);\n nameBuf.copy(buf, 3);\n buf.writeInt8(value, 3 + nameBuf.length);\n return buf;\n}\n\n/**\n * \u6784\u9020\u4E00\u4E2A\u5305\u542B\u6307\u5B9A\u952E\u503C\u5BF9\u7684 TAG_Compound Buffer\u3002\n */\nfunction createCompoundTagBuffer(name: string, byteTags: Record<string, number>): Buffer {\n const nameBuf = Buffer.from(name, \"utf8\");\n const childBuffers: Buffer[] = [];\n for (const [k, v] of Object.entries(byteTags)) {\n childBuffers.push(createByteTagBuffer(k, v));\n }\n childBuffers.push(Buffer.from([TAG_END]));\n\n const header = Buffer.alloc(1 + 2 + nameBuf.length);\n header.writeUInt8(TAG_COMPOUND, 0);\n header.writeUInt16LE(nameBuf.length, 1);\n nameBuf.copy(header, 3);\n\n return Buffer.concat([header, ...childBuffers]);\n}\n\n/**\n * \u4E3A\u6307\u5B9A level.dat \u5F00\u542F Beta APIs\uFF08gametest\uFF09\u3002\n *\n * \u4FDD\u8BC1\uFF1A\n * - \u81EA\u52A8\u5907\u4EFD\u539F\u6587\u4EF6\u81F3 `${levelDatPath}.bak`\uFF1B\n * - \u81EA\u52A8\u66F4\u65B0 experiments \u590D\u5408\u6807\u7B7E\u4E2D\u7684 gametest = 1b\uFF1B\n * - \u81EA\u52A8\u66F4\u65B0 experiments_ever_used = 1b \u4E0E saved_with_toggled_experiments = 1b\uFF1B\n * - \u7CBE\u786E\u6821\u51C6 8 \u5B57\u8282\u6587\u4EF6\u5934\u90E8\u7684 dataLength\uFF1B\n * - \u539F\u5B50\u5199\u64CD\u4F5C\uFF08.tmp -> rename\uFF09\uFF1B\n * - \u82E5\u5DF2\u5904\u4E8E\u5F00\u542F\u72B6\u6001\uFF0C\u76F4\u63A5\u8FD4\u56DE changed: false\uFF0C\u4E0D\u7834\u574F\u6587\u4EF6\u3002\n */\nexport async function enableBetaApisInLevelDat(levelDatPath: string): Promise<LevelDatMutateResult> {\n if (!fs.existsSync(levelDatPath)) {\n return { success: false, changed: false, error: `File not found: ${levelDatPath}` };\n }\n\n try {\n const raw = await fs.promises.readFile(levelDatPath);\n const parsed = parseBedrockLevelDat(raw);\n\n const expCompoundTag = parsed.rootChildren.get(\"experiments\");\n let currentBetaState = false;\n\n if (expCompoundTag && expCompoundTag.type === TAG_COMPOUND) {\n const expChildren = parseCompoundChildren(raw, expCompoundTag.valueOffset).children;\n const gametest = expChildren.get(\"gametest\");\n if (gametest && Number(gametest.value) === 1) {\n currentBetaState = true;\n }\n }\n\n if (currentBetaState) {\n return {\n success: true,\n changed: false,\n previousBetaApisState: true,\n };\n }\n\n // \u9700\u8981\u8FDB\u884C\u5907\u4EFD\n const backupPath = `${levelDatPath}.bak`;\n await fs.promises.copyFile(levelDatPath, backupPath);\n\n let nbtBody: Buffer;\n\n if (!expCompoundTag) {\n // \u573A\u666F 1\uFF1A\u5B8C\u5168\u7F3A\u5931 experiments compound\uFF0C\u5728\u6839 compound \u7ED3\u675F\u6807\u7B7E\u524D\u8FFD\u52A0\u5B8C\u6574 experiments compound\n const insertCompoundBuf = createCompoundTagBuffer(\"experiments\", {\n experiments_ever_used: 1,\n saved_with_toggled_experiments: 1,\n gametest: 1,\n });\n\n // \u6839\u6807\u7B7E\u7ED3\u675F\u70B9\u5728 parsed.rootEndOffset - 1\uFF08TAG_END \u5B57\u8282\u6240\u5728\u4F4D\u7F6E\uFF09\n const rootEndTagIndex = parsed.rootEndOffset - 1;\n const beforeEnd = raw.subarray(8, rootEndTagIndex);\n const afterEnd = raw.subarray(rootEndTagIndex); // \u5305\u542B TAG_END \u53CA\u540E\u7EED\uFF08\u82E5\u6709\uFF09\n nbtBody = Buffer.concat([beforeEnd, insertCompoundBuf, afterEnd]);\n } else {\n // \u573A\u666F 2\uFF1A\u5DF2\u6709 experiments compound\uFF0C\u5728 experiments compound \u5185\u90E8\u66F4\u65B0\u6216\u63D2\u5165\u7F3A\u5931\u5B57\u6BB5\n const { children: expChildren, compoundEndOffset: expEndOffset } = parseCompoundChildren(\n raw,\n expCompoundTag.valueOffset\n );\n const expEndTagIndex = expEndOffset - 1; // \u5185\u90E8 TAG_END \u6240\u5728\u7EDD\u5BF9\u7D22\u5F15\n\n // \u7EC4\u88C5\u8981\u5728 experiments \u5185\u90E8\u8FFD\u52A0\u7684 tag\uFF08\u82E5\u4E0D\u5B58\u5728\uFF09\n const toAppend: Buffer[] = [];\n let mutatedRaw = Buffer.from(raw);\n\n // \u5904\u7406 gametest\n const gametestTag = expChildren.get(\"gametest\");\n if (gametestTag) {\n mutatedRaw.writeInt8(1, gametestTag.valueOffset);\n } else {\n toAppend.push(createByteTagBuffer(\"gametest\", 1));\n }\n\n // \u5904\u7406 experiments_ever_used\n const everUsedTag = expChildren.get(\"experiments_ever_used\");\n if (everUsedTag) {\n mutatedRaw.writeInt8(1, everUsedTag.valueOffset);\n } else {\n toAppend.push(createByteTagBuffer(\"experiments_ever_used\", 1));\n }\n\n // \u5904\u7406 saved_with_toggled_experiments\n const savedTag = expChildren.get(\"saved_with_toggled_experiments\");\n if (savedTag) {\n mutatedRaw.writeInt8(1, savedTag.valueOffset);\n } else {\n toAppend.push(createByteTagBuffer(\"saved_with_toggled_experiments\", 1));\n }\n\n // \u4E5F\u628A root \u5C42\u7684 experiments_ever_used \u8BBE\u4E3A 1\uFF08\u82E5\u5B58\u5728\uFF09\n const rootEverUsed = parsed.rootChildren.get(\"experiments_ever_used\");\n if (rootEverUsed) {\n mutatedRaw.writeInt8(1, rootEverUsed.valueOffset);\n }\n const rootSaved = parsed.rootChildren.get(\"saved_with_toggled_experiments\");\n if (rootSaved) {\n mutatedRaw.writeInt8(1, rootSaved.valueOffset);\n }\n\n if (toAppend.length > 0) {\n const beforeExpEnd = mutatedRaw.subarray(8, expEndTagIndex);\n const afterExpEnd = mutatedRaw.subarray(expEndTagIndex);\n nbtBody = Buffer.concat([beforeExpEnd, ...toAppend, afterExpEnd]);\n } else {\n nbtBody = mutatedRaw.subarray(8);\n }\n }\n\n // \u91CD\u65B0\u6784\u9020 8 \u5B57\u8282 header\n const newHeader = Buffer.alloc(8);\n newHeader.writeUInt32LE(parsed.version, 0);\n newHeader.writeUInt32LE(nbtBody.length, 4);\n\n const finalFileBuffer = Buffer.concat([newHeader, nbtBody]);\n\n // \u539F\u5B50\u66FF\u6362\n const tmpFile = `${levelDatPath}.${process.pid}.tmp`;\n await fs.promises.writeFile(tmpFile, finalFileBuffer);\n await fs.promises.rename(tmpFile, levelDatPath);\n\n return {\n success: true,\n changed: true,\n backupPath,\n previousBetaApisState: false,\n };\n } catch (err) {\n return {\n success: false,\n changed: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * level-dat.ts \u2014 Minecraft \u57FA\u5CA9\u7248\u4E16\u754C\u5B58\u6863 level.dat \u5C0F\u7AEF\u5E8F NBT \u89E3\u6790\u4E0E\u5B9E\u9A8C\u6027\u73A9\u6CD5\u5B89\u5168\u4FEE\u6539\u5668\n *\n * \u57FA\u5CA9\u7248 level.dat \u89C4\u8303\uFF1A\n * - Header\uFF088 \u5B57\u8282\uFF09\uFF1A\n * - [0..3] uint32 LE\uFF1A\u5B58\u50A8\u683C\u5F0F\u7248\u672C\uFF08\u5F53\u524D\u7248\u672C\u901A\u5E38\u4E3A 10\uFF09\n * - [4..7] uint32 LE\uFF1A\u6709\u6548 NBT \u6570\u636E\u5B57\u8282\u957F\u5EA6\uFF08data_length\uFF09\n * - Payload\uFF1A\u672A\u538B\u7F29\u7684\u5C0F\u7AEF\u5E8F\uFF08Little-Endian\uFF09\u6807\u51C6 NBT \u590D\u5408\u6807\u7B7E\uFF08TAG_Compound\uFF09\n *\n * \u6838\u5FC3\u80FD\u529B\uFF1A\n * - \u539F\u751F\u65E0\u5916\u90E8\u9ED1\u76D2\u4F9D\u8D56\uFF0CBuffer \u7EA7\u89E3\u6790\u4E0E\u7CBE\u786E\u5B57\u8282\u4F4D\u79FB\n * - \u8BFB\u53D6\u4E16\u754C\u5B9E\u9A8C\u6027\u73A9\u6CD5\u7684\u5F00\u542F\u72B6\u6001\uFF08\u7279\u522B\u662F Beta APIs / gametest\uFF09\n * - \u5B89\u5168\u6CE8\u5165\u6216\u5207\u6362\u5B9E\u9A8C\u6027\u5F00\u5173\u5E76\u81EA\u52A8\u5907\u4EFD\u539F\u6587\u4EF6\uFF08.bak\uFF09\u4E0E\u6821\u51C6 Header data_length\n */\n\nimport fs from \"node:fs\";\n\nexport const TAG_END = 0;\nexport const TAG_BYTE = 1;\nexport const TAG_SHORT = 2;\nexport const TAG_INT = 3;\nexport const TAG_LONG = 4;\nexport const TAG_FLOAT = 5;\nexport const TAG_DOUBLE = 6;\nexport const TAG_BYTE_ARRAY = 7;\nexport const TAG_STRING = 8;\nexport const TAG_LIST = 9;\nexport const TAG_COMPOUND = 10;\nexport const TAG_INT_ARRAY = 11;\nexport const TAG_LONG_ARRAY = 12;\n\nexport interface NbtTagMeta {\n type: number;\n name: string;\n startOffset: number; // \u5305\u542B tagType \u7684\u8D77\u59CB\u7EDD\u5BF9 offset\n nameOffset: number; // \u6807\u7B7E\u540D\u79F0\u8D77\u59CB offset\n valueOffset: number; // \u8F7D\u8377\u8D77\u59CB offset\n endOffset: number; // \u6807\u7B7E\u7ED3\u675F\u7EDD\u5BF9 offset\n value?: unknown;\n}\n\nexport interface KnownExperimentDefinition {\n id: string;\n nbtKey: string;\n nameZh: string;\n nameEn: string;\n aliases?: string[];\n isRootTag?: boolean;\n extraExpTags?: string[];\n}\n\nexport const KNOWN_EXPERIMENTS: readonly KnownExperimentDefinition[] = [\n {\n id: \"gametest\",\n nbtKey: \"gametest\",\n nameZh: \"\u6D4B\u8BD5\u7248 API\",\n nameEn: \"Beta APIs (gametest)\",\n aliases: [\"beta\", \"beta_apis\", \"betapis\", \"gametest\"],\n },\n {\n id: \"upcoming_creator_features\",\n nbtKey: \"upcoming_creator_features\",\n nameZh: \"\u5373\u5C06\u63A8\u51FA\u7684\u521B\u4F5C\u8005\u529F\u80FD\",\n nameEn: \"Upcoming Creator Features\",\n aliases: [\"upcoming\", \"creator_features\", \"upcoming_features\"],\n },\n {\n id: \"experimental_creator_cameras\",\n nbtKey: \"experimental_creator_cameras\",\n nameZh: \"\u521B\u5EFA\u8005\u7167\u76F8\u673A\u7684\u5B9E\u9A8C\u6027\u529F\u80FD\",\n nameEn: \"Experimental Creator Cameras\",\n aliases: [\"camera\", \"cameras\", \"creator_cameras\", \"creator_camera\"],\n },\n {\n id: \"voxel_shapes\",\n nbtKey: \"voxel_shapes\",\n nameZh: \"\u5B9E\u9A8C\u6027Voxel\u5F62\u72B6\u7279\u5F81\",\n nameEn: \"Experimental Voxel Shapes\",\n aliases: [\"voxel\", \"voxel_shape\", \"experimental_voxel_shapes\", \"voxelshapes\"],\n },\n {\n id: \"villager_trades_rebalance\",\n nbtKey: \"villager_trades_rebalance\",\n nameZh: \"\u6751\u6C11\u8D38\u6613\u518D\u5E73\u8861\",\n nameEn: \"Villager Trade Rebalance\",\n aliases: [\"villager\", \"trades\", \"villager_trades\", \"trade_rebalance\"],\n },\n {\n id: \"drop_3_2026\",\n nbtKey: \"drop_3_2026\",\n nameZh: \"2026\u5E74\u7B2C3\u6B21\u66F4\u65B0\",\n nameEn: \"Drop 3 of 2026\",\n aliases: [\n \"drop3\",\n \"drop_3\",\n \"drop_3_2026\",\n \"wilderness\",\n \"wilderness_bound\",\n \"update_2026_drop_3\",\n \"update_26_3\",\n \"update_drop_3\",\n ],\n extraExpTags: [\"update_2026_drop_3\"],\n },\n {\n id: \"education\",\n nbtKey: \"educationFeaturesEnabled\",\n nameZh: \"Minecraft Education \u529F\u80FD\",\n nameEn: \"Minecraft Education Features\",\n isRootTag: true,\n aliases: [\"edu\", \"education\", \"education_edition\", \"educationfeatures\"],\n extraExpTags: [\"education\"],\n },\n] as const;\n\nexport function resolveExperimentDefinition(query: string): KnownExperimentDefinition | null {\n const q = query.trim().toLowerCase().replace(/[-_]/g, \"\");\n for (const def of KNOWN_EXPERIMENTS) {\n if (def.id.replace(/[-_]/g, \"\") === q) return def;\n if (def.nbtKey.replace(/[-_]/g, \"\") === q) return def;\n if (def.aliases) {\n for (const a of def.aliases) {\n if (a.replace(/[-_]/g, \"\") === q) return def;\n }\n }\n }\n return null;\n}\n\nexport interface LevelDatExperimentsInfo {\n experimentsEverUsed: boolean;\n savedWithToggledExperiments: boolean;\n experiments: Record<string, number | boolean>;\n hasBetaApis: boolean;\n knownExperiments: Record<string, boolean>;\n}\n\nexport interface LevelDatMutateResult {\n success: boolean;\n changed: boolean;\n backupPath?: string;\n error?: string;\n previousBetaApisState?: boolean;\n enabledExperiments?: string[];\n}\n\n/**\n * \u904D\u5386\u5E76\u8DF3\u8FC7\u6307\u5B9A\u7C7B\u578B\u7684 NBT \u8F7D\u8377\uFF0C\u8FD4\u56DE\u4E0B\u4E00\u4E2A tag \u7684 offset\u3002\n */\nexport function skipNbtPayload(buf: Buffer, offset: number, type: number): number {\n switch (type) {\n case TAG_BYTE:\n return offset + 1;\n case TAG_SHORT:\n return offset + 2;\n case TAG_INT:\n case TAG_FLOAT:\n return offset + 4;\n case TAG_LONG:\n case TAG_DOUBLE:\n return offset + 8;\n case TAG_BYTE_ARRAY: {\n if (offset + 4 > buf.length) throw new Error(\"Unexpected EOF reading TAG_Byte_Array length\");\n const len = buf.readInt32LE(offset);\n return offset + 4 + Math.max(0, len);\n }\n case TAG_STRING: {\n if (offset + 2 > buf.length) throw new Error(\"Unexpected EOF reading TAG_String length\");\n const len = buf.readUInt16LE(offset);\n return offset + 2 + len;\n }\n case TAG_LIST: {\n if (offset + 5 > buf.length) throw new Error(\"Unexpected EOF reading TAG_List header\");\n const elemType = buf.readUInt8(offset);\n const count = buf.readInt32LE(offset + 1);\n let cur = offset + 5;\n for (let i = 0; i < count; i++) {\n cur = skipNbtPayload(buf, cur, elemType);\n }\n return cur;\n }\n case TAG_COMPOUND: {\n let cur = offset;\n while (cur < buf.length) {\n const t = buf.readUInt8(cur);\n if (t === TAG_END) {\n return cur + 1;\n }\n if (cur + 3 > buf.length) throw new Error(\"Unexpected EOF reading compound child header\");\n const nameLen = buf.readUInt16LE(cur + 1);\n const childValOffset = cur + 3 + nameLen;\n cur = skipNbtPayload(buf, childValOffset, t);\n }\n return cur;\n }\n case TAG_INT_ARRAY: {\n if (offset + 4 > buf.length) throw new Error(\"Unexpected EOF reading TAG_Int_Array length\");\n const len = buf.readInt32LE(offset);\n return offset + 4 + Math.max(0, len) * 4;\n }\n case TAG_LONG_ARRAY: {\n if (offset + 4 > buf.length) throw new Error(\"Unexpected EOF reading TAG_Long_Array length\");\n const len = buf.readInt32LE(offset);\n return offset + 4 + Math.max(0, len) * 8;\n }\n default:\n throw new Error(`Unsupported or unknown NBT tag type: ${type} at offset ${offset}`);\n }\n}\n\n/**\n * \u89E3\u6790\u590D\u5408\u6807\u7B7E\u5185\u90E8\u6240\u6709\u76F4\u63A5\u5B50\u6807\u7B7E\u7684\u5143\u6570\u636E\u6620\u5C04\u3002\n */\nexport function parseCompoundChildren(\n buf: Buffer,\n compoundValueOffset: number\n): { children: Map<string, NbtTagMeta>; compoundEndOffset: number } {\n const children = new Map<string, NbtTagMeta>();\n let cur = compoundValueOffset;\n\n while (cur < buf.length) {\n const tagType = buf.readUInt8(cur);\n if (tagType === TAG_END) {\n return { children, compoundEndOffset: cur + 1 };\n }\n if (cur + 3 > buf.length) {\n throw new Error(`Unexpected EOF reading named tag header at offset ${cur}`);\n }\n const nameLen = buf.readUInt16LE(cur + 1);\n const nameOffset = cur + 3;\n if (nameOffset + nameLen > buf.length) {\n throw new Error(`Unexpected EOF reading tag name at offset ${cur}`);\n }\n const name = buf.toString(\"utf8\", nameOffset, nameOffset + nameLen);\n const valOffset = nameOffset + nameLen;\n const endOffset = skipNbtPayload(buf, valOffset, tagType);\n\n let val: unknown = undefined;\n if (tagType === TAG_BYTE) {\n val = buf.readInt8(valOffset);\n } else if (tagType === TAG_INT) {\n val = buf.readInt32LE(valOffset);\n } else if (tagType === TAG_STRING) {\n const sLen = buf.readUInt16LE(valOffset);\n val = buf.toString(\"utf8\", valOffset + 2, valOffset + 2 + sLen);\n }\n\n children.set(name, {\n type: tagType,\n name,\n startOffset: cur,\n nameOffset,\n valueOffset: valOffset,\n endOffset,\n value: val,\n });\n\n cur = endOffset;\n }\n\n return { children, compoundEndOffset: cur };\n}\n\n/**\n * \u89E3\u6790\u57FA\u5CA9\u7248 level.dat \u7684\u5B8C\u6574\u6587\u4EF6\u7ED3\u6784\u5E76\u5B9A\u4F4D\u6839 Compound\u3002\n */\nexport function parseBedrockLevelDat(buf: Buffer): {\n version: number;\n dataLength: number;\n rootCompoundName: string;\n rootChildren: Map<string, NbtTagMeta>;\n rootEndOffset: number;\n} {\n if (buf.length < 8) {\n throw new Error(\"Invalid level.dat: buffer smaller than 8-byte header\");\n }\n const version = buf.readUInt32LE(0);\n const dataLength = buf.readUInt32LE(4);\n\n // \u6839\u6807\u7B7E\u5FC5\u987B\u662F TAG_Compound\n if (buf.length < 9 || buf.readUInt8(8) !== TAG_COMPOUND) {\n throw new Error(\"Invalid level.dat: root tag is not TAG_Compound\");\n }\n const rootNameLen = buf.readUInt16LE(9);\n const rootCompoundName = buf.toString(\"utf8\", 11, 11 + rootNameLen);\n const rootValueOffset = 11 + rootNameLen;\n\n const { children: rootChildren, compoundEndOffset: rootEndOffset } = parseCompoundChildren(buf, rootValueOffset);\n\n return {\n version,\n dataLength,\n rootCompoundName,\n rootChildren,\n rootEndOffset,\n };\n}\n\n/**\n * \u8BFB\u53D6\u4E16\u754C\u5B58\u6863 level.dat \u7684\u5B9E\u9A8C\u6027\u914D\u7F6E\u3002\u82E5\u89E3\u6790\u5931\u8D25\u6216\u6587\u4EF6\u4E0D\u5B58\u5728\u8FD4\u56DE null\u3002\n */\nexport function readLevelDatExperiments(levelDatPath: string): LevelDatExperimentsInfo | null {\n if (!fs.existsSync(levelDatPath)) return null;\n try {\n const buf = fs.readFileSync(levelDatPath);\n const parsed = parseBedrockLevelDat(buf);\n\n let experimentsEverUsed = false;\n const everUsedTag = parsed.rootChildren.get(\"experiments_ever_used\");\n if (everUsedTag && everUsedTag.type === TAG_BYTE && Number(everUsedTag.value) === 1) {\n experimentsEverUsed = true;\n }\n\n let savedWithToggledExperiments = false;\n const savedTag = parsed.rootChildren.get(\"saved_with_toggled_experiments\");\n if (savedTag && savedTag.type === TAG_BYTE && Number(savedTag.value) === 1) {\n savedWithToggledExperiments = true;\n }\n\n const experiments: Record<string, number | boolean> = {};\n let hasBetaApis = false;\n\n const expCompoundTag = parsed.rootChildren.get(\"experiments\");\n if (expCompoundTag && expCompoundTag.type === TAG_COMPOUND) {\n const expChildren = parseCompoundChildren(buf, expCompoundTag.valueOffset).children;\n\n // \u90E8\u5206\u7248\u672C experiments_ever_used \u4E5F\u53EF\u80FD\u4F4D\u4E8E experiments \u5185\u90E8\n const innerEverUsed = expChildren.get(\"experiments_ever_used\");\n if (innerEverUsed && Number(innerEverUsed.value) === 1) {\n experimentsEverUsed = true;\n }\n const innerSaved = expChildren.get(\"saved_with_toggled_experiments\");\n if (innerSaved && Number(innerSaved.value) === 1) {\n savedWithToggledExperiments = true;\n }\n\n for (const [name, meta] of expChildren.entries()) {\n if (meta.type === TAG_BYTE) {\n const num = Number(meta.value);\n experiments[name] = num;\n if (name === \"gametest\" && num === 1) {\n hasBetaApis = true;\n }\n }\n }\n }\n\n const knownExperiments: Record<string, boolean> = {};\n for (const def of KNOWN_EXPERIMENTS) {\n let enabled = false;\n if (def.isRootTag) {\n const rootTag = parsed.rootChildren.get(def.nbtKey);\n if (rootTag && Number(rootTag.value) === 1) {\n enabled = true;\n }\n }\n const keysToCheck = [def.nbtKey, ...(def.aliases ?? [])];\n for (const k of keysToCheck) {\n if (experiments[k] === 1) {\n enabled = true;\n break;\n }\n }\n knownExperiments[def.id] = enabled;\n }\n\n return {\n experimentsEverUsed,\n savedWithToggledExperiments,\n experiments,\n hasBetaApis,\n knownExperiments,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * \u6784\u9020\u4E00\u4E2A TAG_Byte \u7684\u4E8C\u8FDB\u5236 Buffer\u3002\n */\nfunction createByteTagBuffer(name: string, value: number): Buffer {\n const nameBuf = Buffer.from(name, \"utf8\");\n const buf = Buffer.alloc(1 + 2 + nameBuf.length + 1);\n buf.writeUInt8(TAG_BYTE, 0);\n buf.writeUInt16LE(nameBuf.length, 1);\n nameBuf.copy(buf, 3);\n buf.writeInt8(value, 3 + nameBuf.length);\n return buf;\n}\n\n/**\n * \u6784\u9020\u4E00\u4E2A\u5305\u542B\u6307\u5B9A\u952E\u503C\u5BF9\u7684 TAG_Compound Buffer\u3002\n */\nfunction createCompoundTagBuffer(name: string, byteTags: Record<string, number>): Buffer {\n const nameBuf = Buffer.from(name, \"utf8\");\n const childBuffers: Buffer[] = [];\n for (const [k, v] of Object.entries(byteTags)) {\n childBuffers.push(createByteTagBuffer(k, v));\n }\n childBuffers.push(Buffer.from([TAG_END]));\n\n const header = Buffer.alloc(1 + 2 + nameBuf.length);\n header.writeUInt8(TAG_COMPOUND, 0);\n header.writeUInt16LE(nameBuf.length, 1);\n nameBuf.copy(header, 3);\n\n return Buffer.concat([header, ...childBuffers]);\n}\n\n/**\n * \u4E3A\u6307\u5B9A level.dat \u5F00\u542F\u6307\u5B9A\u7684\u5B9E\u9A8C\u6027\u5F00\u5173\uFF08\u82E5\u672A\u6307\u5B9A\u6216\u4F20 \"all\" \u5219\u5F00\u542F\u5168\u90E8\u5DF2\u77E5\u5B9E\u9A8C\u6027\u5F00\u5173\uFF09\u3002\n *\n * \u4FDD\u8BC1\uFF1A\n * - \u81EA\u52A8\u5907\u4EFD\u539F\u6587\u4EF6\u81F3 `${levelDatPath}.bak`\uFF1B\n * - \u81EA\u52A8\u66F4\u65B0 root \u4E0E experiments \u590D\u5408\u6807\u7B7E\u4E2D\u7684\u76EE\u6807 byte tags\uFF1B\n * - \u81EA\u52A8\u66F4\u65B0 experiments_ever_used = 1b \u4E0E saved_with_toggled_experiments = 1b\uFF1B\n * - \u7CBE\u786E\u6821\u51C6 8 \u5B57\u8282\u6587\u4EF6\u5934\u90E8\u7684 dataLength\uFF1B\n * - \u539F\u5B50\u5199\u64CD\u4F5C\uFF08.tmp -> rename\uFF09\uFF1B\n * - \u82E5\u6240\u8BF7\u6C42\u5F00\u5173\u5DF2\u5168\u90E8\u5904\u4E8E\u5F00\u542F\u72B6\u6001\uFF0C\u76F4\u63A5\u8FD4\u56DE changed: false\uFF0C\u4E0D\u7834\u574F\u6587\u4EF6\u3002\n */\nexport async function enableExperimentsInLevelDat(\n levelDatPath: string,\n targetExperimentIds?: string[] | \"all\"\n): Promise<LevelDatMutateResult> {\n if (!fs.existsSync(levelDatPath)) {\n return { success: false, changed: false, error: `File not found: ${levelDatPath}` };\n }\n\n try {\n const raw = await fs.promises.readFile(levelDatPath);\n const parsed = parseBedrockLevelDat(raw);\n\n // \u89E3\u6790\u76EE\u6807\u5B9E\u9A8C\u9879\n let targetDefs: KnownExperimentDefinition[] = [];\n if (!targetExperimentIds || targetExperimentIds === \"all\") {\n targetDefs = [...KNOWN_EXPERIMENTS];\n } else {\n const seen = new Set<string>();\n for (const id of targetExperimentIds) {\n const def = resolveExperimentDefinition(id);\n if (def && !seen.has(def.id)) {\n seen.add(def.id);\n targetDefs.push(def);\n }\n }\n if (targetDefs.length === 0) {\n return { success: true, changed: false, enabledExperiments: [] };\n }\n }\n\n const expCompoundTag = parsed.rootChildren.get(\"experiments\");\n const existingExpChildren =\n expCompoundTag && expCompoundTag.type === TAG_COMPOUND\n ? parseCompoundChildren(raw, expCompoundTag.valueOffset).children\n : new Map<string, NbtTagMeta>();\n\n // \u68C0\u67E5\u5F53\u524D\u72B6\u6001\n let needsChange = false;\n const enabledIds: string[] = [];\n\n // 1. \u68C0\u67E5 experiments compound \u53CA\u5185\u90E8\u5FC5\u8981\u7CFB\u7EDF\u6807\u7B7E\n if (!expCompoundTag) {\n needsChange = true;\n } else {\n const expEverUsed = existingExpChildren.get(\"experiments_ever_used\");\n if (!expEverUsed || Number(expEverUsed.value) !== 1) needsChange = true;\n\n const expSaved = existingExpChildren.get(\"saved_with_toggled_experiments\");\n if (!expSaved || Number(expSaved.value) !== 1) needsChange = true;\n }\n\n // \u82E5 root \u5C42\u5B58\u5728\u7CFB\u7EDF\u6807\u7B7E\u4F46\u672A\u8BBE\u4E3A 1\uFF0C\u4E5F\u9700\u4FEE\u6B63\n const rootEverUsed = parsed.rootChildren.get(\"experiments_ever_used\");\n if (rootEverUsed && Number(rootEverUsed.value) !== 1) needsChange = true;\n\n const rootSaved = parsed.rootChildren.get(\"saved_with_toggled_experiments\");\n if (rootSaved && Number(rootSaved.value) !== 1) needsChange = true;\n\n let previousBetaApisState = false;\n const currentGametest = existingExpChildren.get(\"gametest\");\n if (currentGametest && Number(currentGametest.value) === 1) {\n previousBetaApisState = true;\n }\n\n for (const def of targetDefs) {\n enabledIds.push(def.id);\n if (def.isRootTag) {\n const rootTag = parsed.rootChildren.get(def.nbtKey);\n if (!rootTag || Number(rootTag.value) !== 1) {\n needsChange = true;\n }\n }\n if (!def.isRootTag) {\n const tag = existingExpChildren.get(def.nbtKey);\n if (!tag || Number(tag.value) !== 1) {\n needsChange = true;\n }\n }\n if (def.extraExpTags) {\n for (const extra of def.extraExpTags) {\n const tag = existingExpChildren.get(extra);\n if (!tag || Number(tag.value) !== 1) {\n needsChange = true;\n }\n }\n }\n }\n\n if (!needsChange) {\n return {\n success: true,\n changed: false,\n previousBetaApisState,\n enabledExperiments: enabledIds,\n };\n }\n\n // \u5907\u4EFD\u539F\u6587\u4EF6\n const backupPath = `${levelDatPath}.bak`;\n await fs.promises.copyFile(levelDatPath, backupPath);\n\n let mutatedRaw = Buffer.from(raw);\n let nbtBody: Buffer;\n\n // \u6536\u96C6\u9700\u8981\u5728 experiments \u590D\u5408\u6807\u7B7E\u4E2D\u8BBE\u7F6E\u7684 tags\n const requiredExpTags: Record<string, number> = {\n experiments_ever_used: 1,\n saved_with_toggled_experiments: 1,\n };\n for (const def of targetDefs) {\n if (!def.isRootTag) {\n requiredExpTags[def.nbtKey] = 1;\n }\n if (def.extraExpTags) {\n for (const extra of def.extraExpTags) {\n requiredExpTags[extra] = 1;\n }\n }\n }\n\n // \u6536\u96C6 root \u5C42\u9700\u8981\u66F4\u65B0\u6216\u63D2\u5165\u7684 tags\n const rootNeedsEducation = targetDefs.some((d) => d.id === \"education\");\n\n // \u66F4\u65B0 root \u5C42\u5DF2\u5B58\u5728\u7684\u76F8\u5173 tags\n if (rootEverUsed) {\n mutatedRaw.writeInt8(1, rootEverUsed.valueOffset);\n }\n if (rootSaved) {\n mutatedRaw.writeInt8(1, rootSaved.valueOffset);\n }\n if (rootNeedsEducation) {\n const eduTag = parsed.rootChildren.get(\"educationFeaturesEnabled\");\n if (eduTag) {\n mutatedRaw.writeInt8(1, eduTag.valueOffset);\n }\n const optOut = parsed.rootChildren.get(\"has_opted_out_of_education\");\n if (optOut) {\n mutatedRaw.writeInt8(0, optOut.valueOffset);\n }\n }\n\n const rootTagsToAppend: Buffer[] = [];\n if (rootNeedsEducation && !parsed.rootChildren.has(\"educationFeaturesEnabled\")) {\n rootTagsToAppend.push(createByteTagBuffer(\"educationFeaturesEnabled\", 1));\n }\n\n if (!expCompoundTag) {\n // \u573A\u666F 1\uFF1A\u5B8C\u5168\u7F3A\u5931 experiments compound\n const newExpCompoundBuf = createCompoundTagBuffer(\"experiments\", requiredExpTags);\n const rootEndTagIndex = parsed.rootEndOffset - 1;\n const beforeEnd = mutatedRaw.subarray(8, rootEndTagIndex);\n const afterEnd = mutatedRaw.subarray(rootEndTagIndex);\n nbtBody = Buffer.concat([beforeEnd, ...rootTagsToAppend, newExpCompoundBuf, afterEnd]);\n } else {\n // \u573A\u666F 2\uFF1A\u5DF2\u6709 experiments compound\n const { compoundEndOffset: expEndOffset } = parseCompoundChildren(raw, expCompoundTag.valueOffset);\n const expEndTagIndex = expEndOffset - 1;\n const rootEndTagIndex = parsed.rootEndOffset - 1;\n\n const expTagsToAppend: Buffer[] = [];\n for (const [name, val] of Object.entries(requiredExpTags)) {\n const existing = existingExpChildren.get(name);\n if (existing) {\n mutatedRaw.writeInt8(val, existing.valueOffset);\n } else {\n expTagsToAppend.push(createByteTagBuffer(name, val));\n }\n }\n\n // \u7EC4\u88C5\u65B0 NBT body\n // Part 1: root \u5F00\u59CB\u5230 experiments TAG_END \u524D\n const part1 = mutatedRaw.subarray(8, expEndTagIndex);\n // Part 2: \u8FFD\u52A0\u5230 experiments \u5185\u90E8\u7684 tags\n const part2 = Buffer.concat(expTagsToAppend);\n // Part 3: experiments TAG_END \u5230 root TAG_END \u524D\n const part3 = mutatedRaw.subarray(expEndTagIndex, rootEndTagIndex);\n // Part 4: \u8FFD\u52A0\u5230 root \u5C42\u7684 tags (\u5982 educationFeaturesEnabled)\n const part4 = Buffer.concat(rootTagsToAppend);\n // Part 5: root TAG_END \u53CA\u540E\u7EED\n const part5 = mutatedRaw.subarray(rootEndTagIndex);\n\n nbtBody = Buffer.concat([part1, part2, part3, part4, part5]);\n }\n\n // \u91CD\u65B0\u6784\u9020 8 \u5B57\u8282 header\n const newHeader = Buffer.alloc(8);\n newHeader.writeUInt32LE(parsed.version, 0);\n newHeader.writeUInt32LE(nbtBody.length, 4);\n\n const finalFileBuffer = Buffer.concat([newHeader, nbtBody]);\n\n // \u539F\u5B50\u66FF\u6362\n const tmpFile = `${levelDatPath}.${process.pid}.tmp`;\n await fs.promises.writeFile(tmpFile, finalFileBuffer);\n await fs.promises.rename(tmpFile, levelDatPath);\n\n return {\n success: true,\n changed: true,\n backupPath,\n previousBetaApisState,\n enabledExperiments: enabledIds,\n };\n } catch (err) {\n return {\n success: false,\n changed: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * \u4E3A\u6307\u5B9A level.dat \u5F00\u542F Beta APIs\uFF08gametest\uFF09\u3002\n * \u4FDD\u6301\u5411\u540E\u517C\u5BB9\uFF0C\u59D4\u6258\u7ED9 enableExperimentsInLevelDat\u3002\n */\nexport async function enableBetaApisInLevelDat(levelDatPath: string): Promise<LevelDatMutateResult> {\n return await enableExperimentsInLevelDat(levelDatPath, [\"gametest\"]);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5,8 +5,11 @@ import path from "node:path";
5
5
  import test from "node:test";
6
6
  import {
7
7
  enableBetaApisInLevelDat,
8
+ enableExperimentsInLevelDat,
9
+ KNOWN_EXPERIMENTS,
8
10
  parseBedrockLevelDat,
9
11
  readLevelDatExperiments,
12
+ resolveExperimentDefinition,
10
13
  TAG_BYTE,
11
14
  TAG_COMPOUND,
12
15
  TAG_END
@@ -70,14 +73,27 @@ test("readLevelDatExperiments: correctly reads experiments status", () => {
70
73
  assert.ok(info1);
71
74
  assert.equal(info1.hasBetaApis, false);
72
75
  assert.equal(info1.experimentsEverUsed, false);
76
+ assert.equal(info1.knownExperiments.gametest, false);
77
+ assert.equal(info1.knownExperiments.upcoming_creator_features, false);
73
78
  fs.writeFileSync(file, buildMockLevelDat(true, 1));
74
79
  const info2 = readLevelDatExperiments(file);
75
80
  assert.ok(info2);
76
81
  assert.equal(info2.hasBetaApis, true);
82
+ assert.equal(info2.knownExperiments.gametest, true);
77
83
  } finally {
78
84
  fs.rmSync(tmpDir, { recursive: true, force: true });
79
85
  }
80
86
  });
87
+ test("resolveExperimentDefinition: resolves aliases and canonical ids", () => {
88
+ assert.equal(resolveExperimentDefinition("upcoming")?.id, "upcoming_creator_features");
89
+ assert.equal(resolveExperimentDefinition("cameras")?.id, "experimental_creator_cameras");
90
+ assert.equal(resolveExperimentDefinition("voxel")?.id, "voxel_shapes");
91
+ assert.equal(resolveExperimentDefinition("trades")?.id, "villager_trades_rebalance");
92
+ assert.equal(resolveExperimentDefinition("drop3")?.id, "drop_3_2026");
93
+ assert.equal(resolveExperimentDefinition("wilderness_bound")?.id, "drop_3_2026");
94
+ assert.equal(resolveExperimentDefinition("edu")?.id, "education");
95
+ assert.equal(resolveExperimentDefinition("unknown_feature"), null);
96
+ });
81
97
  test("enableBetaApisInLevelDat: enables gametest and updates header length", async () => {
82
98
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-lvl-test-"));
83
99
  try {
@@ -102,12 +118,12 @@ test("enableBetaApisInLevelDat: enables gametest and updates header length", asy
102
118
  fs.rmSync(tmpDir, { recursive: true, force: true });
103
119
  }
104
120
  });
105
- test("enableBetaApisInLevelDat: creates experiments compound if completely absent", async () => {
121
+ test("enableExperimentsInLevelDat: enables all known experiments and handles education root tag", async () => {
106
122
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-lvl-test-"));
107
123
  try {
108
124
  const file = path.join(tmpDir, "level.dat");
109
- fs.writeFileSync(file, buildMockLevelDat(false));
110
- const res = await enableBetaApisInLevelDat(file);
125
+ fs.writeFileSync(file, buildMockLevelDat(true));
126
+ const res = await enableExperimentsInLevelDat(file, "all");
111
127
  assert.equal(res.success, true);
112
128
  assert.equal(res.changed, true);
113
129
  const info = readLevelDatExperiments(file);
@@ -115,6 +131,37 @@ test("enableBetaApisInLevelDat: creates experiments compound if completely absen
115
131
  assert.equal(info.hasBetaApis, true);
116
132
  assert.equal(info.experimentsEverUsed, true);
117
133
  assert.equal(info.savedWithToggledExperiments, true);
134
+ for (const def of KNOWN_EXPERIMENTS) {
135
+ assert.equal(info.knownExperiments[def.id], true, `Experiment ${def.id} should be enabled`);
136
+ }
137
+ const finalBuf = fs.readFileSync(file);
138
+ const parsed = parseBedrockLevelDat(finalBuf);
139
+ assert.equal(parsed.dataLength, finalBuf.length - 8);
140
+ const eduRoot = parsed.rootChildren.get("educationFeaturesEnabled");
141
+ assert.ok(eduRoot);
142
+ assert.equal(eduRoot.value, 1);
143
+ const res2 = await enableExperimentsInLevelDat(file, "all");
144
+ assert.equal(res2.success, true);
145
+ assert.equal(res2.changed, false);
146
+ } finally {
147
+ fs.rmSync(tmpDir, { recursive: true, force: true });
148
+ }
149
+ });
150
+ test("enableExperimentsInLevelDat: enables specific subset from scratch (no experiments compound)", async () => {
151
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-lvl-test-"));
152
+ try {
153
+ const file = path.join(tmpDir, "level.dat");
154
+ fs.writeFileSync(file, buildMockLevelDat(false));
155
+ const res = await enableExperimentsInLevelDat(file, ["upcoming", "voxel", "edu"]);
156
+ assert.equal(res.success, true);
157
+ assert.equal(res.changed, true);
158
+ const info = readLevelDatExperiments(file);
159
+ assert.ok(info);
160
+ assert.equal(info.knownExperiments.upcoming_creator_features, true);
161
+ assert.equal(info.knownExperiments.voxel_shapes, true);
162
+ assert.equal(info.knownExperiments.education, true);
163
+ assert.equal(info.knownExperiments.gametest, false);
164
+ assert.equal(info.knownExperiments.villager_trades_rebalance, false);
118
165
  const finalBuf = fs.readFileSync(file);
119
166
  const parsed = parseBedrockLevelDat(finalBuf);
120
167
  assert.equal(parsed.dataLength, finalBuf.length - 8);