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

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.
Files changed (48) hide show
  1. package/dist/bds-manager.d.ts.map +1 -1
  2. package/dist/bds-manager.js +12 -1
  3. package/dist/bds-manager.js.map +2 -2
  4. package/dist/check-update.d.ts.map +1 -1
  5. package/dist/check-update.js +6 -5
  6. package/dist/check-update.js.map +2 -2
  7. package/dist/cli-pack-manager.js +11 -1
  8. package/dist/cli-pack-manager.js.map +2 -2
  9. package/dist/dependency-negotiator.d.ts +73 -0
  10. package/dist/dependency-negotiator.d.ts.map +1 -0
  11. package/dist/dependency-negotiator.js +151 -0
  12. package/dist/dependency-negotiator.js.map +7 -0
  13. package/dist/dependency-negotiator.test.js +120 -0
  14. package/dist/dependency-negotiator.test.js.map +7 -0
  15. package/dist/fsx.d.ts +15 -7
  16. package/dist/fsx.d.ts.map +1 -1
  17. package/dist/fsx.js +67 -8
  18. package/dist/fsx.js.map +2 -2
  19. package/dist/fsx.test.js +60 -0
  20. package/dist/fsx.test.js.map +7 -0
  21. package/dist/level-dat.d.ts +87 -0
  22. package/dist/level-dat.d.ts.map +1 -0
  23. package/dist/level-dat.js +326 -0
  24. package/dist/level-dat.js.map +7 -0
  25. package/dist/level-dat.test.js +125 -0
  26. package/dist/level-dat.test.js.map +7 -0
  27. package/dist/pack-manager.d.ts +18 -6
  28. package/dist/pack-manager.d.ts.map +1 -1
  29. package/dist/pack-manager.js +73 -24
  30. package/dist/pack-manager.js.map +2 -2
  31. package/dist/rollback.d.ts.map +1 -1
  32. package/dist/rollback.js +2 -2
  33. package/dist/rollback.js.map +2 -2
  34. package/dist/server-properties-i18n.d.ts +17 -0
  35. package/dist/server-properties-i18n.d.ts.map +1 -0
  36. package/dist/server-properties-i18n.js +197 -0
  37. package/dist/server-properties-i18n.js.map +7 -0
  38. package/dist/server-properties.d.ts +22 -0
  39. package/dist/server-properties.d.ts.map +1 -1
  40. package/dist/server-properties.js +126 -1
  41. package/dist/server-properties.js.map +2 -2
  42. package/dist/server-properties.test.js +127 -0
  43. package/dist/server-properties.test.js.map +7 -0
  44. package/dist/world-packs.d.ts +57 -1
  45. package/dist/world-packs.d.ts.map +1 -1
  46. package/dist/world-packs.js +159 -1
  47. package/dist/world-packs.js.map +2 -2
  48. package/package.json +6 -3
@@ -0,0 +1,87 @@
1
+ /**
2
+ * level-dat.ts — Minecraft 基岩版世界存档 level.dat 小端序 NBT 解析与实验性玩法安全修改器
3
+ *
4
+ * 基岩版 level.dat 规范:
5
+ * - Header(8 字节):
6
+ * - [0..3] uint32 LE:存储格式版本(当前版本通常为 10)
7
+ * - [4..7] uint32 LE:有效 NBT 数据字节长度(data_length)
8
+ * - Payload:未压缩的小端序(Little-Endian)标准 NBT 复合标签(TAG_Compound)
9
+ *
10
+ * 核心能力:
11
+ * - 原生无外部黑盒依赖,Buffer 级解析与精确字节位移
12
+ * - 读取世界实验性玩法的开启状态(特别是 Beta APIs / gametest)
13
+ * - 安全注入或切换实验性开关并自动备份原文件(.bak)与校准 Header data_length
14
+ */
15
+ export declare const TAG_END = 0;
16
+ export declare const TAG_BYTE = 1;
17
+ export declare const TAG_SHORT = 2;
18
+ export declare const TAG_INT = 3;
19
+ export declare const TAG_LONG = 4;
20
+ export declare const TAG_FLOAT = 5;
21
+ export declare const TAG_DOUBLE = 6;
22
+ export declare const TAG_BYTE_ARRAY = 7;
23
+ export declare const TAG_STRING = 8;
24
+ export declare const TAG_LIST = 9;
25
+ export declare const TAG_COMPOUND = 10;
26
+ export declare const TAG_INT_ARRAY = 11;
27
+ export declare const TAG_LONG_ARRAY = 12;
28
+ export interface NbtTagMeta {
29
+ type: number;
30
+ name: string;
31
+ startOffset: number;
32
+ nameOffset: number;
33
+ valueOffset: number;
34
+ endOffset: number;
35
+ value?: unknown;
36
+ }
37
+ export interface LevelDatExperimentsInfo {
38
+ experimentsEverUsed: boolean;
39
+ savedWithToggledExperiments: boolean;
40
+ experiments: Record<string, number | boolean>;
41
+ hasBetaApis: boolean;
42
+ }
43
+ export interface LevelDatMutateResult {
44
+ success: boolean;
45
+ changed: boolean;
46
+ backupPath?: string;
47
+ error?: string;
48
+ previousBetaApisState?: boolean;
49
+ }
50
+ /**
51
+ * 遍历并跳过指定类型的 NBT 载荷,返回下一个 tag 的 offset。
52
+ */
53
+ export declare function skipNbtPayload(buf: Buffer, offset: number, type: number): number;
54
+ /**
55
+ * 解析复合标签内部所有直接子标签的元数据映射。
56
+ */
57
+ export declare function parseCompoundChildren(buf: Buffer, compoundValueOffset: number): {
58
+ children: Map<string, NbtTagMeta>;
59
+ compoundEndOffset: number;
60
+ };
61
+ /**
62
+ * 解析基岩版 level.dat 的完整文件结构并定位根 Compound。
63
+ */
64
+ export declare function parseBedrockLevelDat(buf: Buffer): {
65
+ version: number;
66
+ dataLength: number;
67
+ rootCompoundName: string;
68
+ rootChildren: Map<string, NbtTagMeta>;
69
+ rootEndOffset: number;
70
+ };
71
+ /**
72
+ * 读取世界存档 level.dat 的实验性配置。若解析失败或文件不存在返回 null。
73
+ */
74
+ export declare function readLevelDatExperiments(levelDatPath: string): LevelDatExperimentsInfo | null;
75
+ /**
76
+ * 为指定 level.dat 开启 Beta APIs(gametest)。
77
+ *
78
+ * 保证:
79
+ * - 自动备份原文件至 `${levelDatPath}.bak`;
80
+ * - 自动更新 experiments 复合标签中的 gametest = 1b;
81
+ * - 自动更新 experiments_ever_used = 1b 与 saved_with_toggled_experiments = 1b;
82
+ * - 精确校准 8 字节文件头部的 dataLength;
83
+ * - 原子写操作(.tmp -> rename);
84
+ * - 若已处于开启状态,直接返回 changed: false,不破坏文件。
85
+ */
86
+ export declare function enableBetaApisInLevelDat(levelDatPath: string): Promise<LevelDatMutateResult>;
87
+ //# sourceMappingURL=level-dat.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,326 @@
1
+ import fs from "node:fs";
2
+ const TAG_END = 0;
3
+ const TAG_BYTE = 1;
4
+ const TAG_SHORT = 2;
5
+ const TAG_INT = 3;
6
+ const TAG_LONG = 4;
7
+ const TAG_FLOAT = 5;
8
+ const TAG_DOUBLE = 6;
9
+ const TAG_BYTE_ARRAY = 7;
10
+ const TAG_STRING = 8;
11
+ const TAG_LIST = 9;
12
+ const TAG_COMPOUND = 10;
13
+ const TAG_INT_ARRAY = 11;
14
+ const TAG_LONG_ARRAY = 12;
15
+ function skipNbtPayload(buf, offset, type) {
16
+ switch (type) {
17
+ case TAG_BYTE:
18
+ return offset + 1;
19
+ case TAG_SHORT:
20
+ return offset + 2;
21
+ case TAG_INT:
22
+ case TAG_FLOAT:
23
+ return offset + 4;
24
+ case TAG_LONG:
25
+ case TAG_DOUBLE:
26
+ return offset + 8;
27
+ case TAG_BYTE_ARRAY: {
28
+ if (offset + 4 > buf.length) throw new Error("Unexpected EOF reading TAG_Byte_Array length");
29
+ const len = buf.readInt32LE(offset);
30
+ return offset + 4 + Math.max(0, len);
31
+ }
32
+ case TAG_STRING: {
33
+ if (offset + 2 > buf.length) throw new Error("Unexpected EOF reading TAG_String length");
34
+ const len = buf.readUInt16LE(offset);
35
+ return offset + 2 + len;
36
+ }
37
+ case TAG_LIST: {
38
+ if (offset + 5 > buf.length) throw new Error("Unexpected EOF reading TAG_List header");
39
+ const elemType = buf.readUInt8(offset);
40
+ const count = buf.readInt32LE(offset + 1);
41
+ let cur = offset + 5;
42
+ for (let i = 0; i < count; i++) {
43
+ cur = skipNbtPayload(buf, cur, elemType);
44
+ }
45
+ return cur;
46
+ }
47
+ case TAG_COMPOUND: {
48
+ let cur = offset;
49
+ while (cur < buf.length) {
50
+ const t = buf.readUInt8(cur);
51
+ if (t === TAG_END) {
52
+ return cur + 1;
53
+ }
54
+ if (cur + 3 > buf.length) throw new Error("Unexpected EOF reading compound child header");
55
+ const nameLen = buf.readUInt16LE(cur + 1);
56
+ const childValOffset = cur + 3 + nameLen;
57
+ cur = skipNbtPayload(buf, childValOffset, t);
58
+ }
59
+ return cur;
60
+ }
61
+ case TAG_INT_ARRAY: {
62
+ if (offset + 4 > buf.length) throw new Error("Unexpected EOF reading TAG_Int_Array length");
63
+ const len = buf.readInt32LE(offset);
64
+ return offset + 4 + Math.max(0, len) * 4;
65
+ }
66
+ case TAG_LONG_ARRAY: {
67
+ if (offset + 4 > buf.length) throw new Error("Unexpected EOF reading TAG_Long_Array length");
68
+ const len = buf.readInt32LE(offset);
69
+ return offset + 4 + Math.max(0, len) * 8;
70
+ }
71
+ default:
72
+ throw new Error(`Unsupported or unknown NBT tag type: ${type} at offset ${offset}`);
73
+ }
74
+ }
75
+ function parseCompoundChildren(buf, compoundValueOffset) {
76
+ const children = /* @__PURE__ */ new Map();
77
+ let cur = compoundValueOffset;
78
+ while (cur < buf.length) {
79
+ const tagType = buf.readUInt8(cur);
80
+ if (tagType === TAG_END) {
81
+ return { children, compoundEndOffset: cur + 1 };
82
+ }
83
+ if (cur + 3 > buf.length) {
84
+ throw new Error(`Unexpected EOF reading named tag header at offset ${cur}`);
85
+ }
86
+ const nameLen = buf.readUInt16LE(cur + 1);
87
+ const nameOffset = cur + 3;
88
+ if (nameOffset + nameLen > buf.length) {
89
+ throw new Error(`Unexpected EOF reading tag name at offset ${cur}`);
90
+ }
91
+ const name = buf.toString("utf8", nameOffset, nameOffset + nameLen);
92
+ const valOffset = nameOffset + nameLen;
93
+ const endOffset = skipNbtPayload(buf, valOffset, tagType);
94
+ let val = void 0;
95
+ if (tagType === TAG_BYTE) {
96
+ val = buf.readInt8(valOffset);
97
+ } else if (tagType === TAG_INT) {
98
+ val = buf.readInt32LE(valOffset);
99
+ } else if (tagType === TAG_STRING) {
100
+ const sLen = buf.readUInt16LE(valOffset);
101
+ val = buf.toString("utf8", valOffset + 2, valOffset + 2 + sLen);
102
+ }
103
+ children.set(name, {
104
+ type: tagType,
105
+ name,
106
+ startOffset: cur,
107
+ nameOffset,
108
+ valueOffset: valOffset,
109
+ endOffset,
110
+ value: val
111
+ });
112
+ cur = endOffset;
113
+ }
114
+ return { children, compoundEndOffset: cur };
115
+ }
116
+ function parseBedrockLevelDat(buf) {
117
+ if (buf.length < 8) {
118
+ throw new Error("Invalid level.dat: buffer smaller than 8-byte header");
119
+ }
120
+ const version = buf.readUInt32LE(0);
121
+ const dataLength = buf.readUInt32LE(4);
122
+ if (buf.length < 9 || buf.readUInt8(8) !== TAG_COMPOUND) {
123
+ throw new Error("Invalid level.dat: root tag is not TAG_Compound");
124
+ }
125
+ const rootNameLen = buf.readUInt16LE(9);
126
+ const rootCompoundName = buf.toString("utf8", 11, 11 + rootNameLen);
127
+ const rootValueOffset = 11 + rootNameLen;
128
+ const { children: rootChildren, compoundEndOffset: rootEndOffset } = parseCompoundChildren(buf, rootValueOffset);
129
+ return {
130
+ version,
131
+ dataLength,
132
+ rootCompoundName,
133
+ rootChildren,
134
+ rootEndOffset
135
+ };
136
+ }
137
+ function readLevelDatExperiments(levelDatPath) {
138
+ if (!fs.existsSync(levelDatPath)) return null;
139
+ try {
140
+ const buf = fs.readFileSync(levelDatPath);
141
+ const parsed = parseBedrockLevelDat(buf);
142
+ let experimentsEverUsed = false;
143
+ const everUsedTag = parsed.rootChildren.get("experiments_ever_used");
144
+ if (everUsedTag && everUsedTag.type === TAG_BYTE && Number(everUsedTag.value) === 1) {
145
+ experimentsEverUsed = true;
146
+ }
147
+ let savedWithToggledExperiments = false;
148
+ const savedTag = parsed.rootChildren.get("saved_with_toggled_experiments");
149
+ if (savedTag && savedTag.type === TAG_BYTE && Number(savedTag.value) === 1) {
150
+ savedWithToggledExperiments = true;
151
+ }
152
+ const experiments = {};
153
+ let hasBetaApis = false;
154
+ const expCompoundTag = parsed.rootChildren.get("experiments");
155
+ if (expCompoundTag && expCompoundTag.type === TAG_COMPOUND) {
156
+ const expChildren = parseCompoundChildren(buf, expCompoundTag.valueOffset).children;
157
+ const innerEverUsed = expChildren.get("experiments_ever_used");
158
+ if (innerEverUsed && Number(innerEverUsed.value) === 1) {
159
+ experimentsEverUsed = true;
160
+ }
161
+ const innerSaved = expChildren.get("saved_with_toggled_experiments");
162
+ if (innerSaved && Number(innerSaved.value) === 1) {
163
+ savedWithToggledExperiments = true;
164
+ }
165
+ for (const [name, meta] of expChildren.entries()) {
166
+ if (meta.type === TAG_BYTE) {
167
+ const num = Number(meta.value);
168
+ experiments[name] = num;
169
+ if (name === "gametest" && num === 1) {
170
+ hasBetaApis = true;
171
+ }
172
+ }
173
+ }
174
+ }
175
+ return {
176
+ experimentsEverUsed,
177
+ savedWithToggledExperiments,
178
+ experiments,
179
+ hasBetaApis
180
+ };
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+ function createByteTagBuffer(name, value) {
186
+ const nameBuf = Buffer.from(name, "utf8");
187
+ const buf = Buffer.alloc(1 + 2 + nameBuf.length + 1);
188
+ buf.writeUInt8(TAG_BYTE, 0);
189
+ buf.writeUInt16LE(nameBuf.length, 1);
190
+ nameBuf.copy(buf, 3);
191
+ buf.writeInt8(value, 3 + nameBuf.length);
192
+ return buf;
193
+ }
194
+ function createCompoundTagBuffer(name, byteTags) {
195
+ const nameBuf = Buffer.from(name, "utf8");
196
+ const childBuffers = [];
197
+ for (const [k, v] of Object.entries(byteTags)) {
198
+ childBuffers.push(createByteTagBuffer(k, v));
199
+ }
200
+ childBuffers.push(Buffer.from([TAG_END]));
201
+ const header = Buffer.alloc(1 + 2 + nameBuf.length);
202
+ header.writeUInt8(TAG_COMPOUND, 0);
203
+ header.writeUInt16LE(nameBuf.length, 1);
204
+ nameBuf.copy(header, 3);
205
+ return Buffer.concat([header, ...childBuffers]);
206
+ }
207
+ async function enableBetaApisInLevelDat(levelDatPath) {
208
+ if (!fs.existsSync(levelDatPath)) {
209
+ return { success: false, changed: false, error: `File not found: ${levelDatPath}` };
210
+ }
211
+ try {
212
+ const raw = await fs.promises.readFile(levelDatPath);
213
+ const parsed = parseBedrockLevelDat(raw);
214
+ 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;
221
+ }
222
+ }
223
+ if (currentBetaState) {
224
+ return {
225
+ success: true,
226
+ changed: false,
227
+ previousBetaApisState: true
228
+ };
229
+ }
230
+ const backupPath = `${levelDatPath}.bak`;
231
+ await fs.promises.copyFile(levelDatPath, backupPath);
232
+ 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));
262
+ }
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));
268
+ }
269
+ const rootEverUsed = parsed.rootChildren.get("experiments_ever_used");
270
+ if (rootEverUsed) {
271
+ mutatedRaw.writeInt8(1, rootEverUsed.valueOffset);
272
+ }
273
+ const rootSaved = parsed.rootChildren.get("saved_with_toggled_experiments");
274
+ if (rootSaved) {
275
+ mutatedRaw.writeInt8(1, rootSaved.valueOffset);
276
+ }
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);
283
+ }
284
+ }
285
+ const newHeader = Buffer.alloc(8);
286
+ newHeader.writeUInt32LE(parsed.version, 0);
287
+ newHeader.writeUInt32LE(nbtBody.length, 4);
288
+ const finalFileBuffer = Buffer.concat([newHeader, nbtBody]);
289
+ const tmpFile = `${levelDatPath}.${process.pid}.tmp`;
290
+ await fs.promises.writeFile(tmpFile, finalFileBuffer);
291
+ await fs.promises.rename(tmpFile, levelDatPath);
292
+ return {
293
+ success: true,
294
+ changed: true,
295
+ backupPath,
296
+ previousBetaApisState: false
297
+ };
298
+ } catch (err) {
299
+ return {
300
+ success: false,
301
+ changed: false,
302
+ error: err instanceof Error ? err.message : String(err)
303
+ };
304
+ }
305
+ }
306
+ export {
307
+ TAG_BYTE,
308
+ TAG_BYTE_ARRAY,
309
+ TAG_COMPOUND,
310
+ TAG_DOUBLE,
311
+ TAG_END,
312
+ TAG_FLOAT,
313
+ TAG_INT,
314
+ TAG_INT_ARRAY,
315
+ TAG_LIST,
316
+ TAG_LONG,
317
+ TAG_LONG_ARRAY,
318
+ TAG_SHORT,
319
+ TAG_STRING,
320
+ enableBetaApisInLevelDat,
321
+ parseBedrockLevelDat,
322
+ parseCompoundChildren,
323
+ readLevelDatExperiments,
324
+ skipNbtPayload
325
+ };
326
+ //# sourceMappingURL=level-dat.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,125 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import {
7
+ enableBetaApisInLevelDat,
8
+ parseBedrockLevelDat,
9
+ readLevelDatExperiments,
10
+ TAG_BYTE,
11
+ TAG_COMPOUND,
12
+ TAG_END
13
+ } from "./level-dat.js";
14
+ function buildMockLevelDat(includeExperiments, gametestVal) {
15
+ const rootName = "";
16
+ const rootNameBuf = Buffer.from(rootName, "utf8");
17
+ const childBuffers = [];
18
+ const sampleName = Buffer.from("LevelName", "utf8");
19
+ const sampleVal = Buffer.from("TestWorld", "utf8");
20
+ const strTag = Buffer.alloc(1 + 2 + sampleName.length + 2 + sampleVal.length);
21
+ strTag.writeUInt8(8, 0);
22
+ strTag.writeUInt16LE(sampleName.length, 1);
23
+ sampleName.copy(strTag, 3);
24
+ strTag.writeUInt16LE(sampleVal.length, 3 + sampleName.length);
25
+ sampleVal.copy(strTag, 5 + sampleName.length);
26
+ childBuffers.push(strTag);
27
+ if (includeExperiments) {
28
+ const expName = Buffer.from("experiments", "utf8");
29
+ const expChildren = [];
30
+ const everUsedName = Buffer.from("experiments_ever_used", "utf8");
31
+ const b1 = Buffer.alloc(1 + 2 + everUsedName.length + 1);
32
+ b1.writeUInt8(TAG_BYTE, 0);
33
+ b1.writeUInt16LE(everUsedName.length, 1);
34
+ everUsedName.copy(b1, 3);
35
+ b1.writeInt8(0, 3 + everUsedName.length);
36
+ expChildren.push(b1);
37
+ if (gametestVal !== void 0) {
38
+ const gtName = Buffer.from("gametest", "utf8");
39
+ const b2 = Buffer.alloc(1 + 2 + gtName.length + 1);
40
+ b2.writeUInt8(TAG_BYTE, 0);
41
+ b2.writeUInt16LE(gtName.length, 1);
42
+ gtName.copy(b2, 3);
43
+ b2.writeInt8(gametestVal, 3 + gtName.length);
44
+ expChildren.push(b2);
45
+ }
46
+ expChildren.push(Buffer.from([TAG_END]));
47
+ const expHeader = Buffer.alloc(1 + 2 + expName.length);
48
+ expHeader.writeUInt8(TAG_COMPOUND, 0);
49
+ expHeader.writeUInt16LE(expName.length, 1);
50
+ expName.copy(expHeader, 3);
51
+ childBuffers.push(Buffer.concat([expHeader, ...expChildren]));
52
+ }
53
+ childBuffers.push(Buffer.from([TAG_END]));
54
+ const rootHeader = Buffer.alloc(1 + 2 + rootNameBuf.length);
55
+ rootHeader.writeUInt8(TAG_COMPOUND, 0);
56
+ rootHeader.writeUInt16LE(rootNameBuf.length, 1);
57
+ rootNameBuf.copy(rootHeader, 3);
58
+ const nbtBody = Buffer.concat([rootHeader, ...childBuffers]);
59
+ const fileHeader = Buffer.alloc(8);
60
+ fileHeader.writeUInt32LE(10, 0);
61
+ fileHeader.writeUInt32LE(nbtBody.length, 4);
62
+ return Buffer.concat([fileHeader, nbtBody]);
63
+ }
64
+ test("readLevelDatExperiments: correctly reads experiments status", () => {
65
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-lvl-test-"));
66
+ try {
67
+ const file = path.join(tmpDir, "level.dat");
68
+ fs.writeFileSync(file, buildMockLevelDat(true));
69
+ const info1 = readLevelDatExperiments(file);
70
+ assert.ok(info1);
71
+ assert.equal(info1.hasBetaApis, false);
72
+ assert.equal(info1.experimentsEverUsed, false);
73
+ fs.writeFileSync(file, buildMockLevelDat(true, 1));
74
+ const info2 = readLevelDatExperiments(file);
75
+ assert.ok(info2);
76
+ assert.equal(info2.hasBetaApis, true);
77
+ } finally {
78
+ fs.rmSync(tmpDir, { recursive: true, force: true });
79
+ }
80
+ });
81
+ test("enableBetaApisInLevelDat: enables gametest and updates header length", async () => {
82
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-lvl-test-"));
83
+ try {
84
+ const file = path.join(tmpDir, "level.dat");
85
+ fs.writeFileSync(file, buildMockLevelDat(true));
86
+ const res1 = await enableBetaApisInLevelDat(file);
87
+ assert.equal(res1.success, true);
88
+ assert.equal(res1.changed, true);
89
+ assert.ok(res1.backupPath && fs.existsSync(res1.backupPath));
90
+ const info = readLevelDatExperiments(file);
91
+ assert.ok(info);
92
+ assert.equal(info.hasBetaApis, true);
93
+ assert.equal(info.experimentsEverUsed, true);
94
+ assert.equal(info.savedWithToggledExperiments, true);
95
+ const finalBuf = fs.readFileSync(file);
96
+ const parsed = parseBedrockLevelDat(finalBuf);
97
+ assert.equal(parsed.dataLength, finalBuf.length - 8);
98
+ const res2 = await enableBetaApisInLevelDat(file);
99
+ assert.equal(res2.success, true);
100
+ assert.equal(res2.changed, false);
101
+ } finally {
102
+ fs.rmSync(tmpDir, { recursive: true, force: true });
103
+ }
104
+ });
105
+ test("enableBetaApisInLevelDat: creates experiments compound if completely absent", async () => {
106
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-lvl-test-"));
107
+ try {
108
+ const file = path.join(tmpDir, "level.dat");
109
+ fs.writeFileSync(file, buildMockLevelDat(false));
110
+ const res = await enableBetaApisInLevelDat(file);
111
+ assert.equal(res.success, true);
112
+ assert.equal(res.changed, true);
113
+ const info = readLevelDatExperiments(file);
114
+ assert.ok(info);
115
+ assert.equal(info.hasBetaApis, true);
116
+ assert.equal(info.experimentsEverUsed, true);
117
+ assert.equal(info.savedWithToggledExperiments, true);
118
+ const finalBuf = fs.readFileSync(file);
119
+ const parsed = parseBedrockLevelDat(finalBuf);
120
+ assert.equal(parsed.dataLength, finalBuf.length - 8);
121
+ } finally {
122
+ fs.rmSync(tmpDir, { recursive: true, force: true });
123
+ }
124
+ });
125
+ //# sourceMappingURL=level-dat.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/level-dat.test.ts"],
4
+ "sourcesContent": ["import assert from \"node:assert/strict\";\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport test from \"node:test\";\nimport {\n enableBetaApisInLevelDat,\n parseBedrockLevelDat,\n readLevelDatExperiments,\n TAG_BYTE,\n TAG_COMPOUND,\n TAG_END,\n} from \"./level-dat.js\";\n\nfunction buildMockLevelDat(includeExperiments: boolean, gametestVal?: number): Buffer {\n const rootName = \"\";\n const rootNameBuf = Buffer.from(rootName, \"utf8\");\n\n const childBuffers: Buffer[] = [];\n\n // Random sample tag\n const sampleName = Buffer.from(\"LevelName\", \"utf8\");\n const sampleVal = Buffer.from(\"TestWorld\", \"utf8\");\n const strTag = Buffer.alloc(1 + 2 + sampleName.length + 2 + sampleVal.length);\n strTag.writeUInt8(8, 0); // TAG_String\n strTag.writeUInt16LE(sampleName.length, 1);\n sampleName.copy(strTag, 3);\n strTag.writeUInt16LE(sampleVal.length, 3 + sampleName.length);\n sampleVal.copy(strTag, 5 + sampleName.length);\n childBuffers.push(strTag);\n\n if (includeExperiments) {\n const expName = Buffer.from(\"experiments\", \"utf8\");\n const expChildren: Buffer[] = [];\n\n // experiments_ever_used = 0\n const everUsedName = Buffer.from(\"experiments_ever_used\", \"utf8\");\n const b1 = Buffer.alloc(1 + 2 + everUsedName.length + 1);\n b1.writeUInt8(TAG_BYTE, 0);\n b1.writeUInt16LE(everUsedName.length, 1);\n everUsedName.copy(b1, 3);\n b1.writeInt8(0, 3 + everUsedName.length);\n expChildren.push(b1);\n\n if (gametestVal !== undefined) {\n const gtName = Buffer.from(\"gametest\", \"utf8\");\n const b2 = Buffer.alloc(1 + 2 + gtName.length + 1);\n b2.writeUInt8(TAG_BYTE, 0);\n b2.writeUInt16LE(gtName.length, 1);\n gtName.copy(b2, 3);\n b2.writeInt8(gametestVal, 3 + gtName.length);\n expChildren.push(b2);\n }\n\n expChildren.push(Buffer.from([TAG_END]));\n\n const expHeader = Buffer.alloc(1 + 2 + expName.length);\n expHeader.writeUInt8(TAG_COMPOUND, 0);\n expHeader.writeUInt16LE(expName.length, 1);\n expName.copy(expHeader, 3);\n\n childBuffers.push(Buffer.concat([expHeader, ...expChildren]));\n }\n\n childBuffers.push(Buffer.from([TAG_END]));\n\n const rootHeader = Buffer.alloc(1 + 2 + rootNameBuf.length);\n rootHeader.writeUInt8(TAG_COMPOUND, 0);\n rootHeader.writeUInt16LE(rootNameBuf.length, 1);\n rootNameBuf.copy(rootHeader, 3);\n\n const nbtBody = Buffer.concat([rootHeader, ...childBuffers]);\n\n const fileHeader = Buffer.alloc(8);\n fileHeader.writeUInt32LE(10, 0); // version 10\n fileHeader.writeUInt32LE(nbtBody.length, 4);\n\n return Buffer.concat([fileHeader, nbtBody]);\n}\n\ntest(\"readLevelDatExperiments: correctly reads experiments status\", () => {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"sfmc-lvl-test-\"));\n try {\n const file = path.join(tmpDir, \"level.dat\");\n\n // 1. Without gametest\n fs.writeFileSync(file, buildMockLevelDat(true));\n const info1 = readLevelDatExperiments(file);\n assert.ok(info1);\n assert.equal(info1.hasBetaApis, false);\n assert.equal(info1.experimentsEverUsed, false);\n\n // 2. With gametest = 1\n fs.writeFileSync(file, buildMockLevelDat(true, 1));\n const info2 = readLevelDatExperiments(file);\n assert.ok(info2);\n assert.equal(info2.hasBetaApis, true);\n } finally {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n }\n});\n\ntest(\"enableBetaApisInLevelDat: enables gametest and updates header length\", async () => {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"sfmc-lvl-test-\"));\n try {\n const file = path.join(tmpDir, \"level.dat\");\n fs.writeFileSync(file, buildMockLevelDat(true));\n\n const res1 = await enableBetaApisInLevelDat(file);\n assert.equal(res1.success, true);\n assert.equal(res1.changed, true);\n assert.ok(res1.backupPath && fs.existsSync(res1.backupPath));\n\n // Check after change\n const info = readLevelDatExperiments(file);\n assert.ok(info);\n assert.equal(info.hasBetaApis, true);\n assert.equal(info.experimentsEverUsed, true);\n assert.equal(info.savedWithToggledExperiments, true);\n\n // Verify header length matches buffer length\n const finalBuf = fs.readFileSync(file);\n const parsed = parseBedrockLevelDat(finalBuf);\n assert.equal(parsed.dataLength, finalBuf.length - 8);\n\n // Idempotency test: second run shouldn't change\n const res2 = await enableBetaApisInLevelDat(file);\n assert.equal(res2.success, true);\n assert.equal(res2.changed, false);\n } finally {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n }\n});\n\ntest(\"enableBetaApisInLevelDat: creates experiments compound if completely absent\", async () => {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"sfmc-lvl-test-\"));\n try {\n const file = path.join(tmpDir, \"level.dat\");\n fs.writeFileSync(file, buildMockLevelDat(false));\n\n const res = await enableBetaApisInLevelDat(file);\n assert.equal(res.success, true);\n assert.equal(res.changed, true);\n\n const info = readLevelDatExperiments(file);\n assert.ok(info);\n assert.equal(info.hasBetaApis, true);\n assert.equal(info.experimentsEverUsed, true);\n assert.equal(info.savedWithToggledExperiments, true);\n\n const finalBuf = fs.readFileSync(file);\n const parsed = parseBedrockLevelDat(finalBuf);\n assert.equal(parsed.dataLength, finalBuf.length - 8);\n } finally {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n }\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }