mcbe-leveldb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DBUtils.js +114 -0
- package/DBUtils.js.map +1 -0
- package/DBUtils.ts +169 -0
- package/LevelUtils.js +1168 -0
- package/LevelUtils.js.map +1 -0
- package/LevelUtils.ts +1965 -0
- package/SNBTUtils.js +1374 -0
- package/SNBTUtils.js.map +1 -0
- package/SNBTUtils.ts +1725 -0
- package/__biome_data__.js +278 -0
- package/__biome_data__.js.map +1 -0
- package/__biome_data__.ts +277 -0
- package/index.js +6 -0
- package/index.js.map +1 -0
- package/index.ts +5 -0
- package/nbtSchemas.js +9211 -0
- package/nbtSchemas.js.map +1 -0
- package/nbtSchemas.ts +17213 -0
- package/package.json +60 -0
- package/tsconfig.json +24 -0
- package/tsconfig.production.json +25 -0
- package/types.d.ts +501 -0
- package/utils/JSONB.js +348 -0
- package/utils/JSONB.js.map +1 -0
- package/utils/JSONB.ts +566 -0
package/LevelUtils.ts
ADDED
|
@@ -0,0 +1,1965 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
import NBT from "prismarine-nbt";
|
|
3
|
+
import BiomeData from "./__biome_data__.ts";
|
|
4
|
+
import type { LevelDB } from "leveldb-zlib";
|
|
5
|
+
import type { NBTSchemas } from "./nbtSchemas.ts";
|
|
6
|
+
|
|
7
|
+
//#region Local Constants
|
|
8
|
+
|
|
9
|
+
const DEBUG = false;
|
|
10
|
+
const fileNameEncodeCharacterRegExp = /[/:>?\\]/g;
|
|
11
|
+
const fileNameCharacterFilterRegExp = /[^a-zA-Z0-9-_+,-.;=@~/:>?\\]/g;
|
|
12
|
+
|
|
13
|
+
//#endregion Local Constants
|
|
14
|
+
|
|
15
|
+
//#region Local Functions
|
|
16
|
+
|
|
17
|
+
type TupleOfLength16<T> = [T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T];
|
|
18
|
+
|
|
19
|
+
function readInt16LE(buf: Uint8Array, offset: number): number {
|
|
20
|
+
const val = buf[offset]! | (buf[offset + 1]! << 8);
|
|
21
|
+
return val >= 0x8000 ? val - 0x10000 : val;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeInt16LE(value: number): Buffer {
|
|
25
|
+
const buf = Buffer.alloc(2);
|
|
26
|
+
buf.writeInt16LE(value, 0);
|
|
27
|
+
return buf;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeInt32LE(value: number): Buffer {
|
|
31
|
+
const buf = Buffer.alloc(4);
|
|
32
|
+
buf.writeInt32LE(value, 0);
|
|
33
|
+
return buf;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// export function readData3dValue(rawvalue: Uint8Array | null): {
|
|
37
|
+
// /**
|
|
38
|
+
// * A height map in the form [x][z].
|
|
39
|
+
// */
|
|
40
|
+
// heightMap: TupleOfLength16<TupleOfLength16<number>>;
|
|
41
|
+
// /**
|
|
42
|
+
// * A biome map in the form [x][y][z].
|
|
43
|
+
// */
|
|
44
|
+
// biomeMap: TupleOfLength16<TupleOfLength16<TupleOfLength16<number>>>;
|
|
45
|
+
// } | null {
|
|
46
|
+
// if (!rawvalue) {
|
|
47
|
+
// return null;
|
|
48
|
+
// }
|
|
49
|
+
|
|
50
|
+
// // --- Height map (first 512 bytes -> 256 signed 16-bit ints) ---
|
|
51
|
+
// const heightMap: TupleOfLength16<TupleOfLength16<number>> = Array.from(
|
|
52
|
+
// { length: 16 },
|
|
53
|
+
// (): TupleOfLength16<number> => Array(16).fill(0) as TupleOfLength16<number>
|
|
54
|
+
// ) as TupleOfLength16<TupleOfLength16<number>>;
|
|
55
|
+
// for (let i: number = 0; i < 256; i++) {
|
|
56
|
+
// const val: number = readInt16LE(rawvalue, i * 2);
|
|
57
|
+
// const x: number = i % 16;
|
|
58
|
+
// const z: number = Math.floor(i / 16);
|
|
59
|
+
// heightMap[x]![z] = val;
|
|
60
|
+
// }
|
|
61
|
+
|
|
62
|
+
// // --- Biome subchunks (remaining bytes) ---
|
|
63
|
+
// const biomeBytes: Uint8Array<ArrayBuffer> = rawvalue.slice(512);
|
|
64
|
+
// let b: BiomePalette[] = readChunkBiomes(biomeBytes);
|
|
65
|
+
|
|
66
|
+
// // Validate Biome Data
|
|
67
|
+
// if (b.length === 0 || b[0]!.values === null) {
|
|
68
|
+
// throw new Error("Value does not contain at least one subchunk of biome data.");
|
|
69
|
+
// }
|
|
70
|
+
|
|
71
|
+
// // Enlarge list to length 24 if necessary
|
|
72
|
+
// if (b.length < 24) {
|
|
73
|
+
// while (b.length < 24) b.push({ values: null, palette: [] });
|
|
74
|
+
// }
|
|
75
|
+
|
|
76
|
+
// // Trim biome data
|
|
77
|
+
// if (b.length > 24) {
|
|
78
|
+
// if (b.slice(24).some((sub: BiomePalette): boolean => sub.values !== null)) {
|
|
79
|
+
// console.warn(`Trimming biome data from ${b.length} to 24 subchunks.`);
|
|
80
|
+
// }
|
|
81
|
+
// b = b.slice(0, 24);
|
|
82
|
+
// }
|
|
83
|
+
|
|
84
|
+
// // --- Fill biome_map [16][24*16][16] (x,y,z order) ---
|
|
85
|
+
// const biomeMap: TupleOfLength16<TupleOfLength16<TupleOfLength16<number>>> = Array.from(
|
|
86
|
+
// { length: 16 },
|
|
87
|
+
// (): TupleOfLength16<TupleOfLength16<number>> =>
|
|
88
|
+
// Array.from({ length: 24 * 16 }, (): TupleOfLength16<number> => Array(16).fill(NaN) as TupleOfLength16<number>) as TupleOfLength16<
|
|
89
|
+
// TupleOfLength16<number>
|
|
90
|
+
// >
|
|
91
|
+
// ) as TupleOfLength16<TupleOfLength16<TupleOfLength16<number>>>;
|
|
92
|
+
|
|
93
|
+
// const hasData: boolean[] = b.map((sub: BiomePalette): boolean => sub.values !== null);
|
|
94
|
+
// const ii: number[] = hasData
|
|
95
|
+
// .map((has: boolean, idx: number): number | null => (has ? idx + 1 : null))
|
|
96
|
+
// .filter((i: number | null): i is number => i !== null);
|
|
97
|
+
|
|
98
|
+
// for (const i of ii) {
|
|
99
|
+
// const bb: BiomePalette = b[i - 1]!;
|
|
100
|
+
// if (!bb.values) continue;
|
|
101
|
+
|
|
102
|
+
// for (let u: number = 0; u < 4096; u++) {
|
|
103
|
+
// const val: number = bb.palette[bb.values[u]! - 1]!; // R is 1-based
|
|
104
|
+
// const x: number = u % 16;
|
|
105
|
+
// const z: number = Math.floor(u / 16) % 16;
|
|
106
|
+
// const y: number = Math.floor(u / 256);
|
|
107
|
+
// biomeMap[x]![16 * (i - 1) + y]![z] = val;
|
|
108
|
+
// }
|
|
109
|
+
// }
|
|
110
|
+
|
|
111
|
+
// // Fill missing subchunks by copying top of last data subchunk
|
|
112
|
+
// const iMax: number = Math.max(...ii);
|
|
113
|
+
// if (iMax < 24) {
|
|
114
|
+
// const y: number = 16 * iMax - 1;
|
|
115
|
+
// for (let yy: number = y + 1; yy < 24 * 16; yy++) {
|
|
116
|
+
// for (let x: number = 0; x < 16; x++) {
|
|
117
|
+
// for (let z: number = 0; z < 16; z++) {
|
|
118
|
+
// biomeMap[x]![yy]![z] = biomeMap[x]![y]![z]!;
|
|
119
|
+
// }
|
|
120
|
+
// }
|
|
121
|
+
// }
|
|
122
|
+
// }
|
|
123
|
+
|
|
124
|
+
// return { heightMap, biomeMap };
|
|
125
|
+
// }
|
|
126
|
+
export function readData3dValue(rawvalue: Uint8Array | null): {
|
|
127
|
+
/**
|
|
128
|
+
* A height map in the form [x][z].
|
|
129
|
+
*/
|
|
130
|
+
heightMap: TupleOfLength16<TupleOfLength16<number>>;
|
|
131
|
+
/**
|
|
132
|
+
* A biome map as an array of 24 or more BiomePalette objects.
|
|
133
|
+
*/
|
|
134
|
+
biomes: BiomePalette[];
|
|
135
|
+
} | null {
|
|
136
|
+
if (!rawvalue) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Height map (first 512 bytes -> 256 signed 16-bit ints) ---
|
|
141
|
+
const heightMap: TupleOfLength16<TupleOfLength16<number>> = Array.from(
|
|
142
|
+
{ length: 16 },
|
|
143
|
+
(): TupleOfLength16<number> => Array(16).fill(0) as TupleOfLength16<number>
|
|
144
|
+
) as TupleOfLength16<TupleOfLength16<number>>;
|
|
145
|
+
|
|
146
|
+
for (let i: number = 0; i < 256; i++) {
|
|
147
|
+
const val: number = readInt16LE(rawvalue, i * 2);
|
|
148
|
+
const x: number = i % 16;
|
|
149
|
+
const z: number = Math.floor(i / 16);
|
|
150
|
+
heightMap[x]![z] = val;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Biome subchunks (remaining bytes) ---
|
|
154
|
+
const biomeBytes: Uint8Array = rawvalue.slice(512);
|
|
155
|
+
let biomes: BiomePalette[] = readChunkBiomes(biomeBytes);
|
|
156
|
+
|
|
157
|
+
// Validate Biome Data
|
|
158
|
+
if (biomes.length === 0 || biomes[0]!.values === null) {
|
|
159
|
+
throw new Error("Value does not contain at least one subchunk of biome data.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Enlarge list to length 24 if necessary
|
|
163
|
+
if (biomes.length < 24) {
|
|
164
|
+
while (biomes.length < 24) {
|
|
165
|
+
biomes.push({ values: null, palette: [] });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Trim biome data (DISABLED (so that when increasing the height limits, it still works properly))
|
|
170
|
+
// if (biomeMap.length > 24) {
|
|
171
|
+
// if (biomeMap.slice(24).some((sub: BiomePalette): boolean => sub.values !== null)) {
|
|
172
|
+
// console.warn(`Trimming biome data from ${biomeMap.length} to 24 subchunks.`);
|
|
173
|
+
// }
|
|
174
|
+
// biomeMap = biomeMap.slice(0, 24);
|
|
175
|
+
// }
|
|
176
|
+
|
|
177
|
+
return { heightMap, biomes };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function writeData3DValue(heightMap: TupleOfLength16<TupleOfLength16<number>>, biomes: BiomePalette[]): Buffer {
|
|
181
|
+
// heightMap is 16x16, flatten to 256 shorts
|
|
182
|
+
const flatHeight: number[] = heightMap.flatMap((v: TupleOfLength16<number>, i: number): number[] =>
|
|
183
|
+
v.map((_v2: number, i2: number): number => heightMap[i2]![i]!)
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Write height map (512 bytes)
|
|
187
|
+
const heightBufs: Buffer[] = flatHeight.map((v: number): Buffer => writeInt16LE(v));
|
|
188
|
+
const heightBuf: Buffer<ArrayBuffer> = Buffer.concat(heightBufs);
|
|
189
|
+
|
|
190
|
+
// Write biome data
|
|
191
|
+
const biomeBuf: Buffer = writeChunkBiomes(biomes);
|
|
192
|
+
|
|
193
|
+
// Combine
|
|
194
|
+
return Buffer.concat([heightBuf, biomeBuf]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readChunkBiomes(rawvalue: Uint8Array): BiomePalette[] {
|
|
198
|
+
let p: number = 0;
|
|
199
|
+
const end: number = rawvalue.length;
|
|
200
|
+
const result: BiomePalette[] = [];
|
|
201
|
+
|
|
202
|
+
while (p < end) {
|
|
203
|
+
const { values, isPersistent, paletteSize, newOffset } = readSubchunkPaletteIds(rawvalue, p, end);
|
|
204
|
+
p = newOffset;
|
|
205
|
+
|
|
206
|
+
if (values === null) {
|
|
207
|
+
result.push({ values: null, palette: [] });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (isPersistent) {
|
|
212
|
+
throw new Error("Biome palette does not have runtime ids.", { cause: result.length });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (end - p < paletteSize * 4) {
|
|
216
|
+
throw new Error("Subchunk biome palette is truncated.");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const palette: number[] = [];
|
|
220
|
+
for (let i: number = 0; i < paletteSize; i++) {
|
|
221
|
+
const val: number = rawvalue[p]! | (rawvalue[p + 1]! << 8) | (rawvalue[p + 2]! << 16) | (rawvalue[p + 3]! << 24);
|
|
222
|
+
palette.push(val);
|
|
223
|
+
p += 4;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
result.push({ values, palette });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function writeChunkBiomes(biomes: BiomePalette[]): Buffer {
|
|
233
|
+
const buffers: Buffer[] = [];
|
|
234
|
+
|
|
235
|
+
for (const bb of biomes) {
|
|
236
|
+
if (!bb || bb.values === null || bb.values.length === 0) continue; // NULL case in R
|
|
237
|
+
const { values, palette } = bb;
|
|
238
|
+
|
|
239
|
+
// --- Write subchunk palette ids (bitpacked) ---
|
|
240
|
+
const idsBuf: Buffer = writeSubchunkPaletteIds(values!, palette.length);
|
|
241
|
+
|
|
242
|
+
// --- Write palette size ---
|
|
243
|
+
const paletteSizeBuf: Buffer = writeInt32LE(palette.length);
|
|
244
|
+
|
|
245
|
+
// --- Write palette values ---
|
|
246
|
+
const paletteBufs: Buffer[] = palette.map((v: number): Buffer => writeInt32LE(v));
|
|
247
|
+
|
|
248
|
+
buffers.push(idsBuf, paletteSizeBuf, ...paletteBufs);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return Buffer.concat(buffers);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function readSubchunkPaletteIds(
|
|
255
|
+
buffer: Uint8Array,
|
|
256
|
+
offset: number,
|
|
257
|
+
end: number
|
|
258
|
+
): {
|
|
259
|
+
values: number[] | null;
|
|
260
|
+
isPersistent: boolean;
|
|
261
|
+
paletteSize: number;
|
|
262
|
+
newOffset: number;
|
|
263
|
+
} {
|
|
264
|
+
let p = offset;
|
|
265
|
+
|
|
266
|
+
if (end - p < 1) {
|
|
267
|
+
throw new Error("Subchunk biome error: not enough data for flags.");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const flags = buffer[p++]!;
|
|
271
|
+
const isPersistent = (flags & 1) === 0;
|
|
272
|
+
const bitsPerBlock = flags >> 1;
|
|
273
|
+
|
|
274
|
+
// Special case: palette copy
|
|
275
|
+
if (bitsPerBlock === 127) {
|
|
276
|
+
return { values: null, isPersistent, paletteSize: 0, newOffset: p };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const values = new Array(4096);
|
|
280
|
+
|
|
281
|
+
if (bitsPerBlock > 0) {
|
|
282
|
+
const blocksPerWord = Math.floor(32 / bitsPerBlock);
|
|
283
|
+
const wordCount = Math.floor(4095 / blocksPerWord) + 1;
|
|
284
|
+
const mask = (1 << bitsPerBlock) - 1;
|
|
285
|
+
|
|
286
|
+
if (end - p < 4 * wordCount) {
|
|
287
|
+
throw new Error("Subchunk biome error: not enough data for block words.");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let u = 0;
|
|
291
|
+
for (let j = 0; j < wordCount; j++) {
|
|
292
|
+
let temp = buffer[p]! | (buffer[p + 1]! << 8) | (buffer[p + 2]! << 16) | (buffer[p + 3]! << 24);
|
|
293
|
+
p += 4;
|
|
294
|
+
|
|
295
|
+
for (let k = 0; k < blocksPerWord && u < 4096; k++) {
|
|
296
|
+
// const x = (u >> 8) & 0xf;
|
|
297
|
+
// const z = (u >> 4) & 0xf;
|
|
298
|
+
// const y = u & 0xf;
|
|
299
|
+
|
|
300
|
+
values[u] = (temp & mask) + 1; // +1 because R stored 1-based
|
|
301
|
+
temp >>= bitsPerBlock;
|
|
302
|
+
u++;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (end - p < 4) {
|
|
307
|
+
throw new Error("Subchunk biome error: missing palette size.");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const paletteSize = buffer[p]! | (buffer[p + 1]! << 8) | (buffer[p + 2]! << 16) | (buffer[p + 3]! << 24);
|
|
311
|
+
p += 4;
|
|
312
|
+
|
|
313
|
+
return { values, isPersistent, paletteSize, newOffset: p };
|
|
314
|
+
} else {
|
|
315
|
+
// bitsPerBlock == 0 -> everything is ID 1
|
|
316
|
+
for (let u = 0; u < 4096; u++) {
|
|
317
|
+
values[u] = 1;
|
|
318
|
+
}
|
|
319
|
+
return { values, isPersistent, paletteSize: 1, newOffset: p };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function writeSubchunkPaletteIds(values: number[], paletteSize: number): Buffer {
|
|
324
|
+
const blockCount: number = values.length; // usually 16*16*16 = 4096
|
|
325
|
+
const bitsPerBlock: number = Math.max(1, Math.ceil(Math.log2(paletteSize)));
|
|
326
|
+
const wordsPerBlock: number = Math.ceil((blockCount * bitsPerBlock) / 32);
|
|
327
|
+
const words = new Uint32Array(wordsPerBlock);
|
|
328
|
+
|
|
329
|
+
// let bitIndex: number = 0;
|
|
330
|
+
// for (const v of values) {
|
|
331
|
+
// let idx: number = v >>> 0; // ensure unsigned
|
|
332
|
+
// for (let i: number = 0; i < bitsPerBlock; i++) {
|
|
333
|
+
// if (idx & (1 << i)) {
|
|
334
|
+
// const wordIndex: number = Math.floor(bitIndex / 32);
|
|
335
|
+
// const bitOffset: number = bitIndex % 32;
|
|
336
|
+
// words[wordIndex]! |= 1 << bitOffset;
|
|
337
|
+
// }
|
|
338
|
+
// bitIndex++;
|
|
339
|
+
// }
|
|
340
|
+
// }
|
|
341
|
+
let bitIndex = 0;
|
|
342
|
+
for (let i = 0; i < values.length; i++) {
|
|
343
|
+
let val = values[i]! & ((1 << bitsPerBlock) - 1); // mask to bpe bits
|
|
344
|
+
const wordIndex = Math.floor(bitIndex / 32);
|
|
345
|
+
const bitOffset = bitIndex % 32;
|
|
346
|
+
words[wordIndex]! |= val << bitOffset;
|
|
347
|
+
if (bitOffset + bitsPerBlock > 32) {
|
|
348
|
+
// spill over into next word
|
|
349
|
+
words[wordIndex + 1]! |= val >>> (32 - bitOffset);
|
|
350
|
+
}
|
|
351
|
+
bitIndex += bitsPerBlock;
|
|
352
|
+
}
|
|
353
|
+
words.forEach((val, i) => {
|
|
354
|
+
words[i] = (-val - 1) & 0xffffffff;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// --- Write header byte ---
|
|
358
|
+
const header: Buffer<ArrayBuffer> = Buffer.alloc(1);
|
|
359
|
+
header.writeUInt8((bitsPerBlock << 1) | 1, 0);
|
|
360
|
+
|
|
361
|
+
// --- Write packed data ---
|
|
362
|
+
const packed: Buffer<ArrayBuffer> = Buffer.alloc(words.length * 4);
|
|
363
|
+
for (let i: number = 0; i < words.length; i++) {
|
|
364
|
+
packed.writeUInt32LE(words[i]!, i * 4);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return Buffer.concat([header, packed]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
//#endregion Local Functions
|
|
371
|
+
|
|
372
|
+
// --------------------------------------------------------------------------------
|
|
373
|
+
// Constants
|
|
374
|
+
// --------------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
//#region Constants
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The list of Minecraft dimensions.
|
|
380
|
+
*/
|
|
381
|
+
export const dimensions = ["overworld", "nether", "the_end"] as const;
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The list of Minecraft game modes, with their indices corresponding to their numeric gamemode IDs.
|
|
385
|
+
*/
|
|
386
|
+
export const gameModes = ["survival", "creative", "adventure", , , "default", "spectator"] as const;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The content types for LevelDB entries.
|
|
390
|
+
*/
|
|
391
|
+
export const DBEntryContentTypes = [
|
|
392
|
+
// Biome Linked
|
|
393
|
+
"Data3D",
|
|
394
|
+
"Version",
|
|
395
|
+
"Data2D",
|
|
396
|
+
"Data2DLegacy",
|
|
397
|
+
"SubChunkPrefix",
|
|
398
|
+
"LegacyTerrain",
|
|
399
|
+
"BlockEntity",
|
|
400
|
+
"Entity",
|
|
401
|
+
"PendingTicks",
|
|
402
|
+
"LegacyBlockExtraData",
|
|
403
|
+
"BiomeState",
|
|
404
|
+
"FinalizedState",
|
|
405
|
+
"ConversionData",
|
|
406
|
+
"BorderBlocks",
|
|
407
|
+
"HardcodedSpawners",
|
|
408
|
+
"RandomTicks",
|
|
409
|
+
"Checksums",
|
|
410
|
+
"MetaDataHash",
|
|
411
|
+
"GeneratedPreCavesAndCliffsBlending",
|
|
412
|
+
"BlendingBiomeHeight",
|
|
413
|
+
"BlendingData",
|
|
414
|
+
"ActorDigestVersion",
|
|
415
|
+
"LegacyVersion",
|
|
416
|
+
"AABBVolumes", // TO-DO: Figure out how to parse this, it seems to have data for trial chambers and trail ruins.
|
|
417
|
+
// Village
|
|
418
|
+
"VillageDwellers",
|
|
419
|
+
"VillageInfo",
|
|
420
|
+
"VillagePOI",
|
|
421
|
+
"VillagePlayers",
|
|
422
|
+
// Standalone
|
|
423
|
+
"Player",
|
|
424
|
+
"PlayerClient",
|
|
425
|
+
"ActorPrefix",
|
|
426
|
+
"Digest",
|
|
427
|
+
"Map",
|
|
428
|
+
"Portals",
|
|
429
|
+
"SchedulerWT",
|
|
430
|
+
"StructureTemplate",
|
|
431
|
+
"TickingArea",
|
|
432
|
+
"FlatWorldLayers",
|
|
433
|
+
"Scoreboard",
|
|
434
|
+
"Dimension",
|
|
435
|
+
"AutonomousEntities",
|
|
436
|
+
"BiomeData",
|
|
437
|
+
"MobEvents",
|
|
438
|
+
"DynamicProperties",
|
|
439
|
+
"LevelChunkMetaDataDictionary",
|
|
440
|
+
"RealmsStoriesData",
|
|
441
|
+
"LevelDat",
|
|
442
|
+
// Dev Version
|
|
443
|
+
"ForcedWorldCorruption",
|
|
444
|
+
// Misc.
|
|
445
|
+
"Unknown",
|
|
446
|
+
] as const;
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The content type to format mapping for LevelDB entries.
|
|
450
|
+
*/
|
|
451
|
+
export const entryContentTypeToFormatMap = {
|
|
452
|
+
/**
|
|
453
|
+
* @todo Make a parser for this.
|
|
454
|
+
*/
|
|
455
|
+
Data3D: {
|
|
456
|
+
type: "custom",
|
|
457
|
+
resultType: "JSONNBT",
|
|
458
|
+
parse(data: Buffer): NBTSchemas.NBTSchemaTypes.Data3D {
|
|
459
|
+
const result = readData3dValue(data);
|
|
460
|
+
return {
|
|
461
|
+
type: "compound",
|
|
462
|
+
value: {
|
|
463
|
+
heightMap: {
|
|
464
|
+
type: "list",
|
|
465
|
+
value: {
|
|
466
|
+
type: "list",
|
|
467
|
+
value: result!.heightMap.map((row) => ({
|
|
468
|
+
type: "short",
|
|
469
|
+
value: row,
|
|
470
|
+
})),
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
biomes: {
|
|
474
|
+
type: "list",
|
|
475
|
+
value: {
|
|
476
|
+
type: "compound",
|
|
477
|
+
value: result!.biomes.map((subchunk: BiomePalette): NBTSchemas.NBTSchemaTypes.Data3D["value"]["biomes"]["value"]["value"][number] => ({
|
|
478
|
+
values: {
|
|
479
|
+
type: "list",
|
|
480
|
+
value: subchunk.values
|
|
481
|
+
? {
|
|
482
|
+
type: "int",
|
|
483
|
+
value: subchunk.values,
|
|
484
|
+
}
|
|
485
|
+
: ({
|
|
486
|
+
type: "end",
|
|
487
|
+
value: [],
|
|
488
|
+
} as any),
|
|
489
|
+
},
|
|
490
|
+
palette: {
|
|
491
|
+
type: "list",
|
|
492
|
+
value: {
|
|
493
|
+
type: "int",
|
|
494
|
+
value: subchunk.palette,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
})),
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
serialize(data: NBTSchemas.NBTSchemaTypes.Data3D): Buffer {
|
|
504
|
+
return writeData3DValue(
|
|
505
|
+
data.value.heightMap.value.value.map((row: { type: "short"; value: number[] }): number[] => row.value) as any,
|
|
506
|
+
data.value.biomes.value.value.map(
|
|
507
|
+
(subchunk: NBTSchemas.NBTSchemaTypes.Data3D["value"]["biomes"]["value"]["value"][number]): BiomePalette => ({
|
|
508
|
+
palette: subchunk.palette.value.value,
|
|
509
|
+
values: !subchunk.values.value.value?.length ? null : subchunk.values.value.value,
|
|
510
|
+
})
|
|
511
|
+
)
|
|
512
|
+
);
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
Version: { type: "int", bytes: 1, format: "LE", signed: false },
|
|
516
|
+
/**
|
|
517
|
+
* @deprecated Only used in versions < 1.18.0.
|
|
518
|
+
*
|
|
519
|
+
* @todo Make a parser for this so that versions < 1.18.0 can be supported.
|
|
520
|
+
*/
|
|
521
|
+
Data2D: { type: "unknown" },
|
|
522
|
+
/**
|
|
523
|
+
* @deprecated Only used in versions < 1.0.0.
|
|
524
|
+
*/
|
|
525
|
+
Data2DLegacy: { type: "unknown" },
|
|
526
|
+
SubChunkPrefix: {
|
|
527
|
+
type: "custom",
|
|
528
|
+
resultType: "JSONNBT",
|
|
529
|
+
async parse(data: Buffer): Promise<DataTypes_SubChunkPrefix> {
|
|
530
|
+
let currentOffset: number = 0;
|
|
531
|
+
const layers: DataTypes_SubChunkPrefixLayer[] = [];
|
|
532
|
+
/**
|
|
533
|
+
* The version of the SubChunkPrefix.
|
|
534
|
+
*
|
|
535
|
+
* Should be `0x08` (1.2.13 <= x < 1.18.0) or `0x09` (1.18.0 <= x).
|
|
536
|
+
*/
|
|
537
|
+
const version: 0x08 | 0x09 = data[currentOffset++]! as any;
|
|
538
|
+
if (![0x08, 0x09].includes(version)) throw new Error(`Unknown SubChunkPrefix version: ${version}`);
|
|
539
|
+
/**
|
|
540
|
+
* How many blocks are in each location (ex. 2 might mean here is a waterlog layer).
|
|
541
|
+
*/
|
|
542
|
+
let numStorageBlocks: number = data[currentOffset++]!;
|
|
543
|
+
const subChunkIndex: number | undefined = version >= 0x09 ? data[currentOffset++] : undefined;
|
|
544
|
+
for (let blockIndex: number = 0; blockIndex < numStorageBlocks; blockIndex++) {
|
|
545
|
+
const storageVersion: number = data[currentOffset]!;
|
|
546
|
+
currentOffset++;
|
|
547
|
+
const bitsPerBlock: number = storageVersion >> 1;
|
|
548
|
+
const blocksPerWord: number = Math.floor(32 / bitsPerBlock);
|
|
549
|
+
const numints: number = Math.ceil(4096 / blocksPerWord);
|
|
550
|
+
const blockDataOffset: number = currentOffset;
|
|
551
|
+
let paletteOffset: number = blockDataOffset + 4 * numints;
|
|
552
|
+
let psize: number = bitsPerBlock === 0 ? 0 : getInt32Val(data, paletteOffset);
|
|
553
|
+
paletteOffset += Math.sign(bitsPerBlock) * 4;
|
|
554
|
+
// const debugInfo = {
|
|
555
|
+
// version,
|
|
556
|
+
// blockIndex,
|
|
557
|
+
// storageVersion,
|
|
558
|
+
// bitsPerBlock,
|
|
559
|
+
// blocksPerWord,
|
|
560
|
+
// numints,
|
|
561
|
+
// blockDataOffset,
|
|
562
|
+
// paletteOffset,
|
|
563
|
+
// psize,
|
|
564
|
+
// // blockData,
|
|
565
|
+
// // palette,
|
|
566
|
+
// };
|
|
567
|
+
const palette: {
|
|
568
|
+
[paletteIndex: `${bigint}`]: NBTSchemas.NBTSchemaTypes.Block;
|
|
569
|
+
} = {};
|
|
570
|
+
for (let i: bigint = 0n; i < psize; i++) {
|
|
571
|
+
// console.log(debugInfo);
|
|
572
|
+
// appendFileSync("./test1.bin", JSON.stringify(debugInfo) + "\n");
|
|
573
|
+
const result = (await NBT.parse(data.subarray(paletteOffset), "little")) as unknown as {
|
|
574
|
+
parsed: NBTSchemas.NBTSchemaTypes.Block;
|
|
575
|
+
type: NBT.NBTFormat;
|
|
576
|
+
metadata: NBT.Metadata;
|
|
577
|
+
};
|
|
578
|
+
paletteOffset += result.metadata.size;
|
|
579
|
+
palette[`${i}`] = result.parsed;
|
|
580
|
+
// console.log(result);
|
|
581
|
+
// appendFileSync("./test1.bin", JSON.stringify(result));
|
|
582
|
+
}
|
|
583
|
+
currentOffset = paletteOffset;
|
|
584
|
+
const block_indices: number[] = [];
|
|
585
|
+
for (let i: number = 0; i < 4096; i++) {
|
|
586
|
+
const maskVal: number = getInt32Val(data, blockDataOffset + Math.floor(i / blocksPerWord) * 4);
|
|
587
|
+
const blockVal: number = (maskVal >> ((i % blocksPerWord) * bitsPerBlock)) & ((1 << bitsPerBlock) - 1);
|
|
588
|
+
// const blockType: DataTypes_Block | undefined = palette[`${blockVal as unknown as bigint}`];
|
|
589
|
+
// if (!blockType && blockVal !== -1) throw new ReferenceError(`Invalid block palette index ${blockVal} for block ${i}`);
|
|
590
|
+
/* const chunkOffset: Vector3 = {
|
|
591
|
+
x: (i >> 8) & 0xf,
|
|
592
|
+
y: (i >> 4) & 0xf,
|
|
593
|
+
z: (i >> 0) & 0xf,
|
|
594
|
+
}; */
|
|
595
|
+
block_indices.push(blockVal);
|
|
596
|
+
}
|
|
597
|
+
layers.push({
|
|
598
|
+
storageVersion: NBT.byte(storageVersion),
|
|
599
|
+
palette: NBT.comp(palette),
|
|
600
|
+
block_indices: { type: "list", value: { type: "int", value: block_indices } },
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return NBT.comp({
|
|
604
|
+
version: NBT.byte<0x08 | 0x09>(version),
|
|
605
|
+
layerCount: NBT.byte(numStorageBlocks),
|
|
606
|
+
layers: { type: "list", value: NBT.comp(layers) },
|
|
607
|
+
...(version >= 0x09
|
|
608
|
+
? {
|
|
609
|
+
subChunkIndex: NBT.byte(subChunkIndex!),
|
|
610
|
+
}
|
|
611
|
+
: {}),
|
|
612
|
+
});
|
|
613
|
+
},
|
|
614
|
+
serialize(data: DataTypes_SubChunkPrefix): Buffer {
|
|
615
|
+
const buffer: Buffer = Buffer.from([
|
|
616
|
+
data.value.version.value,
|
|
617
|
+
data.value.layerCount.value,
|
|
618
|
+
...(data.value.version.value >= 0x09 ? [data.value.subChunkIndex!.value] : []),
|
|
619
|
+
]);
|
|
620
|
+
const layerBuffers: Buffer[] = data.value.layers.value.value.map((layer: DataTypes_SubChunkPrefixLayer): Buffer => {
|
|
621
|
+
const bitsPerBlock: number = layer.storageVersion.value >> 1;
|
|
622
|
+
const blocksPerWord: number = Math.floor(32 / bitsPerBlock);
|
|
623
|
+
const numints: number = Math.ceil(4096 / blocksPerWord);
|
|
624
|
+
const bytes: number[] = [layer.storageVersion.value];
|
|
625
|
+
const blockIndicesBuffer: Buffer = Buffer.alloc(Math.ceil(numints * 4));
|
|
626
|
+
writeBlockIndices(blockIndicesBuffer, 0, layer.block_indices.value.value, bitsPerBlock, blocksPerWord);
|
|
627
|
+
bytes.push(...blockIndicesBuffer);
|
|
628
|
+
const paletteLengthBuffer: Buffer = Buffer.alloc(4);
|
|
629
|
+
setInt32Val(paletteLengthBuffer, 0, Object.keys(layer.palette.value).length);
|
|
630
|
+
bytes.push(...paletteLengthBuffer);
|
|
631
|
+
const paletteKeys: `${bigint}`[] = (Object.keys(layer.palette.value) as `${bigint}`[]).sort(
|
|
632
|
+
(a: `${bigint}`, b: `${bigint}`): number => Number(a) - Number(b)
|
|
633
|
+
);
|
|
634
|
+
for (let paletteIndex: number = 0; paletteIndex < paletteKeys.length; paletteIndex++) {
|
|
635
|
+
const block: NBTSchemas.NBTSchemaTypes.Block = layer.palette.value[paletteKeys[paletteIndex]!]!;
|
|
636
|
+
bytes.push(...NBT.writeUncompressed({ name: "", ...block }, "little"));
|
|
637
|
+
}
|
|
638
|
+
return Buffer.from(bytes);
|
|
639
|
+
});
|
|
640
|
+
return Buffer.concat([buffer, ...layerBuffers]);
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
/**
|
|
644
|
+
* @deprecated Only used in versions < 1.0.0.
|
|
645
|
+
*/
|
|
646
|
+
LegacyTerrain: { type: "unknown" },
|
|
647
|
+
BlockEntity: { type: "NBT" },
|
|
648
|
+
Entity: { type: "NBT" },
|
|
649
|
+
PendingTicks: { type: "NBT" },
|
|
650
|
+
/**
|
|
651
|
+
* @deprecated Only used in versions < 1.2.3.
|
|
652
|
+
*/
|
|
653
|
+
LegacyBlockExtraData: { type: "unknown" },
|
|
654
|
+
/**
|
|
655
|
+
* @todo Figure out how to parse this.
|
|
656
|
+
*/
|
|
657
|
+
BiomeState: { type: "unknown" },
|
|
658
|
+
FinalizedState: { type: "int", bytes: 4, format: "LE", signed: false },
|
|
659
|
+
/**
|
|
660
|
+
* @deprecated No longer used.
|
|
661
|
+
*/
|
|
662
|
+
ConversionData: { type: "unknown" },
|
|
663
|
+
/**
|
|
664
|
+
* @todo Figure out how to parse this.
|
|
665
|
+
*/
|
|
666
|
+
BorderBlocks: { type: "unknown" },
|
|
667
|
+
HardcodedSpawners: { type: "NBT" },
|
|
668
|
+
RandomTicks: { type: "NBT" },
|
|
669
|
+
/**
|
|
670
|
+
* @deprecated Only used in versions < 1.18.0.
|
|
671
|
+
*/
|
|
672
|
+
Checksums: { type: "unknown" },
|
|
673
|
+
/**
|
|
674
|
+
* @todo Figure out how to parse this.
|
|
675
|
+
*/
|
|
676
|
+
MetaDataHash: { type: "unknown" },
|
|
677
|
+
/**
|
|
678
|
+
* @deprecated Unused.
|
|
679
|
+
*/
|
|
680
|
+
GeneratedPreCavesAndCliffsBlending: { type: "unknown" },
|
|
681
|
+
/**
|
|
682
|
+
* @todo Figure out how to parse this.
|
|
683
|
+
*/
|
|
684
|
+
BlendingBiomeHeight: { type: "unknown" },
|
|
685
|
+
/**
|
|
686
|
+
* @todo Figure out how to parse this.
|
|
687
|
+
*/
|
|
688
|
+
BlendingData: { type: "unknown" },
|
|
689
|
+
/**
|
|
690
|
+
* @todo Figure out how to parse this.
|
|
691
|
+
*/
|
|
692
|
+
ActorDigestVersion: { type: "unknown" },
|
|
693
|
+
/**
|
|
694
|
+
* @deprecated Only used in versions < 1.16.100. Later versions use {@link entryContentTypeToFormatMap.Version}
|
|
695
|
+
*/
|
|
696
|
+
LegacyVersion: { type: "int", bytes: 1, format: "LE", signed: false },
|
|
697
|
+
VillageDwellers: { type: "NBT" },
|
|
698
|
+
VillageInfo: { type: "NBT" },
|
|
699
|
+
VillagePOI: { type: "NBT" },
|
|
700
|
+
VillagePlayers: { type: "NBT" },
|
|
701
|
+
Player: { type: "NBT" },
|
|
702
|
+
PlayerClient: { type: "NBT" },
|
|
703
|
+
ActorPrefix: { type: "NBT", format: "LE" },
|
|
704
|
+
/**
|
|
705
|
+
* @todo Figure out how to parse this.
|
|
706
|
+
*/
|
|
707
|
+
Digest: { type: "unknown" },
|
|
708
|
+
Map: { type: "NBT" },
|
|
709
|
+
Portals: { type: "NBT" },
|
|
710
|
+
SchedulerWT: { type: "NBT" },
|
|
711
|
+
StructureTemplate: { type: "NBT" },
|
|
712
|
+
TickingArea: { type: "NBT" },
|
|
713
|
+
FlatWorldLayers: { type: "ASCII" },
|
|
714
|
+
Scoreboard: { type: "NBT" },
|
|
715
|
+
Dimension: { type: "NBT" },
|
|
716
|
+
AutonomousEntities: { type: "NBT" },
|
|
717
|
+
BiomeData: { type: "NBT" },
|
|
718
|
+
MobEvents: { type: "NBT" },
|
|
719
|
+
LevelDat: { type: "NBT" },
|
|
720
|
+
/**
|
|
721
|
+
* @todo Figure out how to parse this.
|
|
722
|
+
*/
|
|
723
|
+
AABBVolumes: { type: "unknown" },
|
|
724
|
+
DynamicProperties: { type: "NBT" },
|
|
725
|
+
/**
|
|
726
|
+
* @todo Figure out how to parse this. (It looks like NBT with a bit of extra data at the beginning.)
|
|
727
|
+
*/
|
|
728
|
+
LevelChunkMetaDataDictionary: { type: "unknown" },
|
|
729
|
+
/**
|
|
730
|
+
* @todo Figure out how to parse this. (It seems that each one just has a value of 1 (`0x31`).)
|
|
731
|
+
*/
|
|
732
|
+
RealmsStoriesData: { type: "unknown" },
|
|
733
|
+
/**
|
|
734
|
+
* The content type used for LevelDB keys that are used for the forced world corruption feature of the developer version of Bedrock Edition.
|
|
735
|
+
*/
|
|
736
|
+
ForcedWorldCorruption: { type: "unknown" },
|
|
737
|
+
/**
|
|
738
|
+
* All data that has a key that is not recognized.
|
|
739
|
+
*/
|
|
740
|
+
Unknown: { type: "unknown" },
|
|
741
|
+
} as const satisfies {
|
|
742
|
+
[key in DBEntryContentType]: EntryContentTypeFormatData;
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* The format data for an entry content type.
|
|
747
|
+
*
|
|
748
|
+
* Use this type if you want to have support for content type formats that aren't currently is use but may be in the future.
|
|
749
|
+
*
|
|
750
|
+
* This is used in the {@link entryContentTypeToFormatMap} object.
|
|
751
|
+
*/
|
|
752
|
+
export type EntryContentTypeFormatData = (
|
|
753
|
+
| {
|
|
754
|
+
/**
|
|
755
|
+
* The format type of the data.
|
|
756
|
+
*/
|
|
757
|
+
type: "JSON" | "SNBT" | "ASCII" | "binary" | "binaryPlainText" | "hex" | "UTF-8" | "unknown";
|
|
758
|
+
}
|
|
759
|
+
| {
|
|
760
|
+
/**
|
|
761
|
+
* The format type of the data.
|
|
762
|
+
*/
|
|
763
|
+
type: "NBT";
|
|
764
|
+
/**
|
|
765
|
+
* The endianness of the data.
|
|
766
|
+
*
|
|
767
|
+
* If not present, `"LE"` should be assumed.
|
|
768
|
+
*
|
|
769
|
+
* - `"BE"`: Big Endian
|
|
770
|
+
* - `"LE"`: Little Endian
|
|
771
|
+
* - `"LEV"`: Little Varint
|
|
772
|
+
*
|
|
773
|
+
* @default "LE"
|
|
774
|
+
*/
|
|
775
|
+
format?: "BE" | "LE" | "LEV";
|
|
776
|
+
}
|
|
777
|
+
| {
|
|
778
|
+
/**
|
|
779
|
+
* The format type of the data.
|
|
780
|
+
*/
|
|
781
|
+
type: "int";
|
|
782
|
+
/**
|
|
783
|
+
* How many bytes this integer is.
|
|
784
|
+
*/
|
|
785
|
+
bytes: number;
|
|
786
|
+
/**
|
|
787
|
+
* The endianness of the data.
|
|
788
|
+
*/
|
|
789
|
+
format: "BE" | "LE";
|
|
790
|
+
/**
|
|
791
|
+
* The signedness of the data.
|
|
792
|
+
*/
|
|
793
|
+
signed: boolean;
|
|
794
|
+
}
|
|
795
|
+
| {
|
|
796
|
+
/**
|
|
797
|
+
* The format type of the data.
|
|
798
|
+
*/
|
|
799
|
+
type: "custom";
|
|
800
|
+
/**
|
|
801
|
+
* The format type that results from the {@link parse} method.
|
|
802
|
+
*/
|
|
803
|
+
resultType: "JSONNBT";
|
|
804
|
+
/**
|
|
805
|
+
* The function to parse the data.
|
|
806
|
+
*
|
|
807
|
+
* The {@link data} parameter should be the buffer read directly from the file or LevelDB entry.
|
|
808
|
+
*
|
|
809
|
+
* @param data The data to parse, as a buffer.
|
|
810
|
+
* @returns The parsed data.
|
|
811
|
+
*/
|
|
812
|
+
parse(data: Buffer): NBT.Compound | Promise<NBT.Compound>;
|
|
813
|
+
/**
|
|
814
|
+
* The function to serialize the data.
|
|
815
|
+
*
|
|
816
|
+
* This result of this can be written directly to the file or LevelDB entry.
|
|
817
|
+
*
|
|
818
|
+
* @param data The data to serialize.
|
|
819
|
+
* @returns The serialized data, as a buffer.
|
|
820
|
+
*/
|
|
821
|
+
serialize(data: NBT.Compound): Buffer | Promise<Buffer>;
|
|
822
|
+
}
|
|
823
|
+
| {
|
|
824
|
+
/**
|
|
825
|
+
* The format type of the data.
|
|
826
|
+
*/
|
|
827
|
+
type: "custom";
|
|
828
|
+
/**
|
|
829
|
+
* The format type that results from the {@link parse} method.
|
|
830
|
+
*/
|
|
831
|
+
resultType: "SNBT";
|
|
832
|
+
/**
|
|
833
|
+
* The function to parse the data.
|
|
834
|
+
*
|
|
835
|
+
* The {@link data} parameter should be the buffer read directly from the file or LevelDB entry.
|
|
836
|
+
*
|
|
837
|
+
* @param data The data to parse, as a buffer.
|
|
838
|
+
* @returns The parsed data.
|
|
839
|
+
*/
|
|
840
|
+
parse(data: Buffer): string | Promise<string>;
|
|
841
|
+
/**
|
|
842
|
+
* The function to serialize the data.
|
|
843
|
+
*
|
|
844
|
+
* This result of this can be written directly to the file or LevelDB entry.
|
|
845
|
+
*
|
|
846
|
+
* @param data The data to serialize.
|
|
847
|
+
* @returns The serialized data, as a buffer.
|
|
848
|
+
*/
|
|
849
|
+
serialize(data: string): Buffer | Promise<Buffer>;
|
|
850
|
+
}
|
|
851
|
+
| {
|
|
852
|
+
/**
|
|
853
|
+
* The format type of the data.
|
|
854
|
+
*/
|
|
855
|
+
type: "custom";
|
|
856
|
+
/**
|
|
857
|
+
* The format type that results from the {@link parse} method.
|
|
858
|
+
*/
|
|
859
|
+
resultType: "buffer";
|
|
860
|
+
/**
|
|
861
|
+
* The function to parse the data.
|
|
862
|
+
*
|
|
863
|
+
* The {@link data} parameter should be the buffer read directly from the file or LevelDB entry.
|
|
864
|
+
*
|
|
865
|
+
* @param data The data to parse, as a buffer.
|
|
866
|
+
* @returns The parsed data.
|
|
867
|
+
*/
|
|
868
|
+
parse(data: Buffer): Buffer | Promise<Buffer>;
|
|
869
|
+
/**
|
|
870
|
+
* The function to serialize the data.
|
|
871
|
+
*
|
|
872
|
+
* This result of this can be written directly to the file or LevelDB entry.
|
|
873
|
+
*
|
|
874
|
+
* @param data The data to serialize.
|
|
875
|
+
* @returns The serialized data, as a buffer.
|
|
876
|
+
*/
|
|
877
|
+
serialize(data: Buffer): Buffer | Promise<Buffer>;
|
|
878
|
+
}
|
|
879
|
+
| {
|
|
880
|
+
/**
|
|
881
|
+
* The format type of the data.
|
|
882
|
+
*/
|
|
883
|
+
type: "custom";
|
|
884
|
+
/**
|
|
885
|
+
* The format type that results from the {@link parse} method.
|
|
886
|
+
*/
|
|
887
|
+
resultType: "unknown";
|
|
888
|
+
/**
|
|
889
|
+
* The function to parse the data.
|
|
890
|
+
*
|
|
891
|
+
* The {@link data} parameter should be the buffer read directly from the file or LevelDB entry.
|
|
892
|
+
*
|
|
893
|
+
* @param data The data to parse, as a buffer.
|
|
894
|
+
* @returns The parsed data.
|
|
895
|
+
*/
|
|
896
|
+
parse(data: Buffer): any | Promise<any>;
|
|
897
|
+
/**
|
|
898
|
+
* The function to serialize the data.
|
|
899
|
+
*
|
|
900
|
+
* This result of this can be written directly to the file or LevelDB entry.
|
|
901
|
+
*
|
|
902
|
+
* @param data The data to serialize.
|
|
903
|
+
* @returns The serialized data, as a buffer.
|
|
904
|
+
*/
|
|
905
|
+
serialize(data: any): Buffer | Promise<Buffer>;
|
|
906
|
+
}
|
|
907
|
+
) & {
|
|
908
|
+
/**
|
|
909
|
+
* The raw file extension of the data.
|
|
910
|
+
*
|
|
911
|
+
* If not present, `"bin"` should be assumed.
|
|
912
|
+
*
|
|
913
|
+
* @default "bin"
|
|
914
|
+
*/
|
|
915
|
+
rawFileExtension?: string;
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
//#endregion Constants
|
|
919
|
+
|
|
920
|
+
// --------------------------------------------------------------------------------
|
|
921
|
+
// Types
|
|
922
|
+
// --------------------------------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
//#region Types
|
|
925
|
+
|
|
926
|
+
export interface Vector2 {
|
|
927
|
+
/**
|
|
928
|
+
* X component of this vector.
|
|
929
|
+
*/
|
|
930
|
+
x: number;
|
|
931
|
+
/**
|
|
932
|
+
* Y component of this vector.
|
|
933
|
+
*/
|
|
934
|
+
y: number;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export interface VectorXZ {
|
|
938
|
+
/**
|
|
939
|
+
* X component of this vector.
|
|
940
|
+
*/
|
|
941
|
+
x: number;
|
|
942
|
+
/**
|
|
943
|
+
* Z component of this vector.
|
|
944
|
+
*/
|
|
945
|
+
z: number;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export interface Vector3 {
|
|
949
|
+
/**
|
|
950
|
+
* X component of this vector.
|
|
951
|
+
*/
|
|
952
|
+
x: number;
|
|
953
|
+
/**
|
|
954
|
+
* Y component of this vector.
|
|
955
|
+
*/
|
|
956
|
+
y: number;
|
|
957
|
+
/**
|
|
958
|
+
* Z component of this vector.
|
|
959
|
+
*/
|
|
960
|
+
z: number;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export type Dimension = (typeof dimensions)[number];
|
|
964
|
+
|
|
965
|
+
export interface DimensionLocation extends Vector3 {
|
|
966
|
+
/**
|
|
967
|
+
* Dimension that this coordinate is associated with.
|
|
968
|
+
*/
|
|
969
|
+
dimension: Dimension;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
export interface DimensionVector2 extends Vector2 {
|
|
973
|
+
/**
|
|
974
|
+
* Dimension that this coordinate is associated with.
|
|
975
|
+
*/
|
|
976
|
+
dimension: Dimension;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
export interface DimensionVectorXZ extends VectorXZ {
|
|
980
|
+
/**
|
|
981
|
+
* Dimension that this coordinate is associated with.
|
|
982
|
+
*/
|
|
983
|
+
dimension: Dimension;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
export interface SubChunkIndexDimensionVectorXZ extends DimensionVectorXZ {
|
|
987
|
+
/**
|
|
988
|
+
* The index of this sub-chunk.
|
|
989
|
+
*
|
|
990
|
+
* Should be between 0 and 15 (inclusive).
|
|
991
|
+
*/
|
|
992
|
+
subChunkIndex: number;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* @todo
|
|
997
|
+
*/
|
|
998
|
+
export interface StructureSectionData extends NBT.Compound {
|
|
999
|
+
size: NBTSchemas.NBTSchemaTypes.StructureTemplate["value"]["size"];
|
|
1000
|
+
block_indices: {
|
|
1001
|
+
type: `${NBT.TagType.List}`;
|
|
1002
|
+
value: {
|
|
1003
|
+
type: `${NBT.TagType.List}`;
|
|
1004
|
+
value: [
|
|
1005
|
+
blockLayer: {
|
|
1006
|
+
type: `${NBT.TagType.Int}`;
|
|
1007
|
+
value: number[];
|
|
1008
|
+
},
|
|
1009
|
+
waterlogLayer: {
|
|
1010
|
+
type: `${NBT.TagType.Int}`;
|
|
1011
|
+
value: number[];
|
|
1012
|
+
}
|
|
1013
|
+
];
|
|
1014
|
+
};
|
|
1015
|
+
};
|
|
1016
|
+
palette: {
|
|
1017
|
+
type: `${NBT.TagType.List}`;
|
|
1018
|
+
value: {
|
|
1019
|
+
type: `${NBT.TagType.Compound}`;
|
|
1020
|
+
value: NBTSchemas.NBTSchemaTypes.Block["value"][];
|
|
1021
|
+
};
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Biome palette data.
|
|
1027
|
+
*/
|
|
1028
|
+
export interface BiomePalette {
|
|
1029
|
+
/**
|
|
1030
|
+
* The data for the individual blocks, or `null` if this sub-chunk has no biome data.
|
|
1031
|
+
*
|
|
1032
|
+
* The values of this map to the index of the biome in the palette.
|
|
1033
|
+
*/
|
|
1034
|
+
values: number[] | null;
|
|
1035
|
+
/**
|
|
1036
|
+
* The palette of biomes as a list of biome numeric IDs.
|
|
1037
|
+
*/
|
|
1038
|
+
palette: number[];
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// /**
|
|
1042
|
+
// * The correct NBT structure of a block in a block palette.
|
|
1043
|
+
// *
|
|
1044
|
+
// * Note: This is what the structure SHOULD be, it may not be, but if it is different then Minecraft would be unable to parse it.
|
|
1045
|
+
// */
|
|
1046
|
+
// export interface DataTypes_Block extends NBT.Compound {
|
|
1047
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1048
|
+
// value: {
|
|
1049
|
+
// /**
|
|
1050
|
+
// * The namespaced ID of the block.
|
|
1051
|
+
// */
|
|
1052
|
+
// name: NBT.String;
|
|
1053
|
+
// /**
|
|
1054
|
+
// * The block states of the block.
|
|
1055
|
+
// */
|
|
1056
|
+
// states: NBT.Compound;
|
|
1057
|
+
// /**
|
|
1058
|
+
// * The data version of the block.
|
|
1059
|
+
// */
|
|
1060
|
+
// version: NBT.Int;
|
|
1061
|
+
// };
|
|
1062
|
+
// }
|
|
1063
|
+
|
|
1064
|
+
// /**
|
|
1065
|
+
// * The NBT structure of the parsed data of the {@link entryContentTypeToFormatMap.Data3D | Data3D} content type.
|
|
1066
|
+
// *
|
|
1067
|
+
// * Note: This NBT structure is specific to the parser and serializer implemented by this module.
|
|
1068
|
+
// * This is because the actual data is stored in binary format.
|
|
1069
|
+
// */
|
|
1070
|
+
// export interface DataTypes_Data3D extends NBT.Compound {
|
|
1071
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1072
|
+
// value: {
|
|
1073
|
+
// /**
|
|
1074
|
+
// * The height map data.
|
|
1075
|
+
// */
|
|
1076
|
+
// heightMap: {
|
|
1077
|
+
// type: `${NBT.TagType.List}`;
|
|
1078
|
+
// value: {
|
|
1079
|
+
// type: `${NBT.TagType.List}`;
|
|
1080
|
+
// value: {
|
|
1081
|
+
// type: `${NBT.TagType.Short}`;
|
|
1082
|
+
// /**
|
|
1083
|
+
// * The height map values.
|
|
1084
|
+
// */
|
|
1085
|
+
// value: number[];
|
|
1086
|
+
// }[];
|
|
1087
|
+
// };
|
|
1088
|
+
// };
|
|
1089
|
+
// /**
|
|
1090
|
+
// * The biome data.
|
|
1091
|
+
// */
|
|
1092
|
+
// biomes: {
|
|
1093
|
+
// type: `${NBT.TagType.List}`;
|
|
1094
|
+
// value: {
|
|
1095
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1096
|
+
// value: {
|
|
1097
|
+
// /**
|
|
1098
|
+
// * The biome values.
|
|
1099
|
+
// *
|
|
1100
|
+
// * This is an array of indices in the biome palette, one for each block.
|
|
1101
|
+
// */
|
|
1102
|
+
// values: NBT.List<NBT.TagType.Int>;
|
|
1103
|
+
// /**
|
|
1104
|
+
// * The biome palette.
|
|
1105
|
+
// *
|
|
1106
|
+
// * These are the biome numeric IDs.
|
|
1107
|
+
// */
|
|
1108
|
+
// palette: NBT.List<NBT.TagType.Int>;
|
|
1109
|
+
// }[];
|
|
1110
|
+
// };
|
|
1111
|
+
// };
|
|
1112
|
+
// };
|
|
1113
|
+
// }
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
// TO-DO: Convert this into an NBT schema and move it into the nbtSchema.ts file
|
|
1117
|
+
export interface DataTypes_SubChunkPrefix extends NBT.Compound {
|
|
1118
|
+
type: `${NBT.TagType.Compound}`;
|
|
1119
|
+
value: {
|
|
1120
|
+
version: NBT.Byte & { value: 0x08 | 0x09 };
|
|
1121
|
+
layerCount: NBT.Byte;
|
|
1122
|
+
layers: {
|
|
1123
|
+
type: `${NBT.TagType.List}`;
|
|
1124
|
+
value: {
|
|
1125
|
+
type: `${NBT.TagType.Compound}`;
|
|
1126
|
+
value: DataTypes_SubChunkPrefixLayer[];
|
|
1127
|
+
};
|
|
1128
|
+
};
|
|
1129
|
+
subChunkIndex?: NBT.Byte;
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// TO-DO: Convert this into an NBT schema and move it into the nbtSchema.ts file
|
|
1134
|
+
export interface DataTypes_SubChunkPrefixLayer extends Omit<NBT.Compound["value"], never> {
|
|
1135
|
+
storageVersion: NBT.Byte;
|
|
1136
|
+
palette: {
|
|
1137
|
+
type: `${NBT.TagType.Compound}`;
|
|
1138
|
+
value: {
|
|
1139
|
+
[paletteIndex: string]: NBTSchemas.NBTSchemaTypes.Block;
|
|
1140
|
+
};
|
|
1141
|
+
};
|
|
1142
|
+
block_indices: NBT.List<NBT.TagType.Int>;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// /**
|
|
1146
|
+
// * The correct NBT structure of the {@link entryContentTypeToFormatMap.StructureTemplate | StructureTemplate} content type.
|
|
1147
|
+
// *
|
|
1148
|
+
// * Note: This is what the structure SHOULD be, it may not be, but if it is different then Minecraft would be unable to parse it.
|
|
1149
|
+
// */
|
|
1150
|
+
// export interface DataTypes_StructureTemplate extends NBT.Compound {
|
|
1151
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1152
|
+
// value: {
|
|
1153
|
+
// /**
|
|
1154
|
+
// * The structure data.
|
|
1155
|
+
// */
|
|
1156
|
+
// structure: {
|
|
1157
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1158
|
+
// value: {
|
|
1159
|
+
// /**
|
|
1160
|
+
// * The block palette.
|
|
1161
|
+
// */
|
|
1162
|
+
// palette: {
|
|
1163
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1164
|
+
// value: {
|
|
1165
|
+
// /**
|
|
1166
|
+
// * The default block palette.
|
|
1167
|
+
// */
|
|
1168
|
+
// default: {
|
|
1169
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1170
|
+
// value: {
|
|
1171
|
+
// /**
|
|
1172
|
+
// * The block entity data.
|
|
1173
|
+
// *
|
|
1174
|
+
// * @todo
|
|
1175
|
+
// */
|
|
1176
|
+
// block_position_data: {
|
|
1177
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1178
|
+
// value: {
|
|
1179
|
+
// [key in `${bigint}`]: NBTSchemas.NBTSchemaTypes.BlockEntity;
|
|
1180
|
+
// };
|
|
1181
|
+
// };
|
|
1182
|
+
// /**
|
|
1183
|
+
// * The block palette.
|
|
1184
|
+
// */
|
|
1185
|
+
// block_palette: {
|
|
1186
|
+
// type: `${NBT.TagType.List}`;
|
|
1187
|
+
// value: {
|
|
1188
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1189
|
+
// /**
|
|
1190
|
+
// * The block palette array.
|
|
1191
|
+
// */
|
|
1192
|
+
// value: NBTSchemas.NBTSchemaTypes.Block["value"][];
|
|
1193
|
+
// };
|
|
1194
|
+
// };
|
|
1195
|
+
// };
|
|
1196
|
+
// };
|
|
1197
|
+
// };
|
|
1198
|
+
// };
|
|
1199
|
+
// /**
|
|
1200
|
+
// * The block indices.
|
|
1201
|
+
// *
|
|
1202
|
+
// * These are two arrays of indices in the block palette.
|
|
1203
|
+
// *
|
|
1204
|
+
// * The first layer is the block layer.
|
|
1205
|
+
// *
|
|
1206
|
+
// * The second layer is the waterlog layer, even though it is mainly used for waterlogging, other blocks can be put here to,
|
|
1207
|
+
// * which allows for putting two blocks in the same location, or creating ghost blocks (as blocks in this layer cannot be interacted with,
|
|
1208
|
+
// * however when the corresponding block in the block layer is broken, this block gets moved to the block layer).
|
|
1209
|
+
// */
|
|
1210
|
+
// block_indices: {
|
|
1211
|
+
// /**
|
|
1212
|
+
// * The type of the NBT tag.
|
|
1213
|
+
// */
|
|
1214
|
+
// type: `${NBT.TagType.List}`;
|
|
1215
|
+
// /**
|
|
1216
|
+
// * The value of the NBT tag.
|
|
1217
|
+
// */
|
|
1218
|
+
// value: {
|
|
1219
|
+
// /**
|
|
1220
|
+
// * The type of the NBT tags in this list.
|
|
1221
|
+
// */
|
|
1222
|
+
// type: `${NBT.TagType.List}`;
|
|
1223
|
+
// /**
|
|
1224
|
+
// * The layers.
|
|
1225
|
+
// */
|
|
1226
|
+
// value: [
|
|
1227
|
+
// /**
|
|
1228
|
+
// * The block layer.
|
|
1229
|
+
// */
|
|
1230
|
+
// blockLayer: {
|
|
1231
|
+
// /**
|
|
1232
|
+
// * The type of the NBT tags in this list.
|
|
1233
|
+
// */
|
|
1234
|
+
// type: `${NBT.TagType.Int}`;
|
|
1235
|
+
// /**
|
|
1236
|
+
// * The block indices in this layer.
|
|
1237
|
+
// */
|
|
1238
|
+
// value: number[];
|
|
1239
|
+
// },
|
|
1240
|
+
// /**
|
|
1241
|
+
// * The waterlog layer.
|
|
1242
|
+
// */
|
|
1243
|
+
// waterlogLayer: {
|
|
1244
|
+
// /**
|
|
1245
|
+
// * The type of the NBT tags in this list.
|
|
1246
|
+
// */
|
|
1247
|
+
// type: `${NBT.TagType.Int}`;
|
|
1248
|
+
// /**
|
|
1249
|
+
// * The block indices in this layer.
|
|
1250
|
+
// */
|
|
1251
|
+
// value: number[];
|
|
1252
|
+
// }
|
|
1253
|
+
// ];
|
|
1254
|
+
// };
|
|
1255
|
+
// };
|
|
1256
|
+
// /**
|
|
1257
|
+
// * @todo
|
|
1258
|
+
// */
|
|
1259
|
+
// entities: {
|
|
1260
|
+
// type: `${NBT.TagType.List}`;
|
|
1261
|
+
// value: {
|
|
1262
|
+
// type: `${NBT.TagType.Compound}`;
|
|
1263
|
+
// value: DataTypes_ActorPrefix["value"][];
|
|
1264
|
+
// };
|
|
1265
|
+
// };
|
|
1266
|
+
// };
|
|
1267
|
+
// };
|
|
1268
|
+
// /**
|
|
1269
|
+
// * The size of the structure.
|
|
1270
|
+
// */
|
|
1271
|
+
// size: {
|
|
1272
|
+
// /**
|
|
1273
|
+
// * The type of the NBT tag.
|
|
1274
|
+
// */
|
|
1275
|
+
// type: `${NBT.TagType.List}`;
|
|
1276
|
+
// /**
|
|
1277
|
+
// * The value of the NBT tag.
|
|
1278
|
+
// */
|
|
1279
|
+
// value: {
|
|
1280
|
+
// /**
|
|
1281
|
+
// * The type of the NBT tags in this list.
|
|
1282
|
+
// */
|
|
1283
|
+
// type: `${NBT.TagType.Int}`;
|
|
1284
|
+
// /**
|
|
1285
|
+
// * The size, as a tuple of 3 integers.
|
|
1286
|
+
// */
|
|
1287
|
+
// value: [x: number, y: number, z: number];
|
|
1288
|
+
// };
|
|
1289
|
+
// };
|
|
1290
|
+
// /**
|
|
1291
|
+
// * The world origin of the structure.
|
|
1292
|
+
// *
|
|
1293
|
+
// * This is used for entity and block entity data, to get relative positions.
|
|
1294
|
+
// */
|
|
1295
|
+
// structure_world_origin: {
|
|
1296
|
+
// /**
|
|
1297
|
+
// * The type of the NBT tag.
|
|
1298
|
+
// */
|
|
1299
|
+
// type: `${NBT.TagType.List}`;
|
|
1300
|
+
// /**
|
|
1301
|
+
// * The value of the NBT tag.
|
|
1302
|
+
// */
|
|
1303
|
+
// value: {
|
|
1304
|
+
// /**
|
|
1305
|
+
// * The type of the NBT tags in this list.
|
|
1306
|
+
// */
|
|
1307
|
+
// type: `${NBT.TagType.Int}`;
|
|
1308
|
+
// /**
|
|
1309
|
+
// * The world origin, as a tuple of 3 integers.
|
|
1310
|
+
// */
|
|
1311
|
+
// value: [x: number, y: number, z: number];
|
|
1312
|
+
// };
|
|
1313
|
+
// };
|
|
1314
|
+
// /**
|
|
1315
|
+
// * The format version of the structure.
|
|
1316
|
+
// */
|
|
1317
|
+
// format_version: NBT.Int;
|
|
1318
|
+
// };
|
|
1319
|
+
// }
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* The content type of a LevelDB entry.
|
|
1323
|
+
*/
|
|
1324
|
+
export type DBEntryContentType = (typeof DBEntryContentTypes)[number];
|
|
1325
|
+
|
|
1326
|
+
//#endregion Types
|
|
1327
|
+
|
|
1328
|
+
// --------------------------------------------------------------------------------
|
|
1329
|
+
// Functions
|
|
1330
|
+
// --------------------------------------------------------------------------------
|
|
1331
|
+
|
|
1332
|
+
//#region Functions
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Parses an integer from a buffer gives the number of bytes, endianness, signedness and offset.
|
|
1336
|
+
*
|
|
1337
|
+
* @param buffer The buffer to read from.
|
|
1338
|
+
* @param bytes The number of bytes to read.
|
|
1339
|
+
* @param format The endianness of the data.
|
|
1340
|
+
* @param signed The signedness of the data. Defaults to `false`.
|
|
1341
|
+
* @param offset The offset to read from. Defaults to `0`.
|
|
1342
|
+
* @returns The parsed integer.
|
|
1343
|
+
*
|
|
1344
|
+
* @throws {RangeError} If the byte length is less than 1.
|
|
1345
|
+
* @throws {RangeError} If the buffer does not contain enough data at the specified offset.
|
|
1346
|
+
*/
|
|
1347
|
+
export function parseSpecificIntType(buffer: Buffer, bytes: number, format: "BE" | "LE", signed: boolean = false, offset: number = 0): bigint {
|
|
1348
|
+
if (bytes < 1) {
|
|
1349
|
+
throw new RangeError("Byte length must be at least 1");
|
|
1350
|
+
}
|
|
1351
|
+
if (offset + bytes > buffer.length) {
|
|
1352
|
+
throw new RangeError("Buffer does not contain enough data at the specified offset");
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
let result: bigint = 0n;
|
|
1356
|
+
|
|
1357
|
+
if (format === "BE") {
|
|
1358
|
+
for (let i: number = 0; i < bytes; i++) {
|
|
1359
|
+
result = (result << 8n) | BigInt(buffer[offset + i]!);
|
|
1360
|
+
}
|
|
1361
|
+
} else {
|
|
1362
|
+
for (let i: number = bytes - 1; i >= 0; i--) {
|
|
1363
|
+
result = (result << 8n) | BigInt(buffer[offset + i]!);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (signed) {
|
|
1368
|
+
const signBit: bigint = 1n << BigInt(bytes * 8 - 1);
|
|
1369
|
+
if (result & signBit) {
|
|
1370
|
+
result -= 1n << BigInt(bytes * 8);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
return result;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Options for {@link writeSpecificIntType}.
|
|
1379
|
+
*/
|
|
1380
|
+
export interface WriteSpecificIntTypeOptions {
|
|
1381
|
+
/**
|
|
1382
|
+
* Whether to wrap the value if it is out of range.
|
|
1383
|
+
*
|
|
1384
|
+
* If `false`, an error will be thrown if the value is out of range.
|
|
1385
|
+
*
|
|
1386
|
+
* @default false
|
|
1387
|
+
*/
|
|
1388
|
+
wrap?: boolean;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Writes an integer to a buffer.
|
|
1393
|
+
*
|
|
1394
|
+
* @param buffer The buffer to write to.
|
|
1395
|
+
* @param value The integer to write.
|
|
1396
|
+
* @param bytes The number of bytes to write.
|
|
1397
|
+
* @param format The endianness of the data.
|
|
1398
|
+
* @param signed The signedness of the data. Defaults to `false`.
|
|
1399
|
+
* @param offset The offset to write to. Defaults to `0`.
|
|
1400
|
+
* @param options The options to use.
|
|
1401
|
+
* @returns The buffer from the {@link buffer} parameter.
|
|
1402
|
+
*
|
|
1403
|
+
* @throws {RangeError} If the byte length is less than 1.
|
|
1404
|
+
* @throws {RangeError} If the buffer does not have enough space at the specified offset.
|
|
1405
|
+
* @throws {RangeError} If the value is out of range and {@link WriteSpecificIntTypeOptions.wrap | options.wrap} is `false`.
|
|
1406
|
+
*/
|
|
1407
|
+
export function writeSpecificIntType(
|
|
1408
|
+
buffer: Buffer,
|
|
1409
|
+
value: bigint,
|
|
1410
|
+
bytes: number,
|
|
1411
|
+
format: "BE" | "LE",
|
|
1412
|
+
signed: boolean = false,
|
|
1413
|
+
offset: number = 0,
|
|
1414
|
+
options?: WriteSpecificIntTypeOptions
|
|
1415
|
+
): Buffer {
|
|
1416
|
+
if (bytes < 1) {
|
|
1417
|
+
throw new RangeError("Byte length must be at least 1");
|
|
1418
|
+
}
|
|
1419
|
+
if (offset + bytes > buffer.length) {
|
|
1420
|
+
throw new RangeError("Buffer does not have enough space at the specified offset");
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const bitSize: bigint = BigInt(bytes * 8);
|
|
1424
|
+
const maxUnsigned: bigint = (1n << bitSize) - 1n;
|
|
1425
|
+
const minSigned: bigint = -(1n << (bitSize - 1n));
|
|
1426
|
+
const maxSigned: bigint = (1n << (bitSize - 1n)) - 1n;
|
|
1427
|
+
|
|
1428
|
+
if (signed) {
|
|
1429
|
+
if (value < minSigned || value > maxSigned) {
|
|
1430
|
+
if (options?.wrap) {
|
|
1431
|
+
value = (value + (1n << bitSize)) % (1n << bitSize);
|
|
1432
|
+
} else {
|
|
1433
|
+
throw new RangeError(`Signed value out of range for ${bytes} bytes`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (value < 0n) {
|
|
1437
|
+
value += 1n << bitSize;
|
|
1438
|
+
}
|
|
1439
|
+
} else {
|
|
1440
|
+
if (value < 0n || value > maxUnsigned) {
|
|
1441
|
+
if (options?.wrap) {
|
|
1442
|
+
value = value % (1n << bitSize);
|
|
1443
|
+
} else {
|
|
1444
|
+
throw new RangeError(`Unsigned value out of range for ${bytes} bytes`);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
for (let i: number = 0; i < bytes; i++) {
|
|
1450
|
+
const shift: bigint = format === "BE" ? BigInt((bytes - 1 - i) * 8) : BigInt(i * 8);
|
|
1451
|
+
buffer[offset + i] = Number((value >> shift) & 0xffn);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return buffer;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Sanitizes a filename.
|
|
1459
|
+
*
|
|
1460
|
+
* @param filename The filename to sanitize.
|
|
1461
|
+
* @returns The sanitized filename.
|
|
1462
|
+
*/
|
|
1463
|
+
export function sanitizeFilename(filename: string): string {
|
|
1464
|
+
return filename
|
|
1465
|
+
.replaceAll(fileNameCharacterFilterRegExp, /* (substring: string): string => encodeURIComponent(substring) */ "")
|
|
1466
|
+
.replaceAll(fileNameEncodeCharacterRegExp, (substring: string): string => encodeURIComponent(substring));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Sanitizes a display key.
|
|
1471
|
+
*
|
|
1472
|
+
* @param key The key to sanitize.
|
|
1473
|
+
* @returns The sanitized key.
|
|
1474
|
+
*/
|
|
1475
|
+
export function sanitizeDisplayKey(key: string): string {
|
|
1476
|
+
return key.replaceAll(/[^a-zA-Z0-9-_+,-.;=@~/:>?\\]/g, (substring: string): string => encodeURIComponent(substring) /* "" */);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Converts a chunk block index to an offset from the minimum corner of the chunk.
|
|
1481
|
+
*
|
|
1482
|
+
* @param index The chunk block index.
|
|
1483
|
+
* @returns The offset from the minimum corner of the chunk.
|
|
1484
|
+
*/
|
|
1485
|
+
export function chunkBlockIndexToOffset(index: number): Vector3 {
|
|
1486
|
+
return {
|
|
1487
|
+
x: (index >> 8) & 0xf,
|
|
1488
|
+
y: (index >> 4) & 0xf,
|
|
1489
|
+
z: (index >> 0) & 0xf,
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Converts an offset from the minimum corner of the chunk to a chunk block index.
|
|
1495
|
+
*
|
|
1496
|
+
* @param offset The offset from the minimum corner of the chunk.
|
|
1497
|
+
* @returns The chunk block index.
|
|
1498
|
+
*/
|
|
1499
|
+
export function offsetToChunkBlockIndex(offset: Vector3): number {
|
|
1500
|
+
return (offset.x & 0xf) | ((offset.y & 0xf) << 4) | ((offset.z & 0xf) << 8);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Reads a 32-bit integer value from a Buffer at the given offset (little-endian).
|
|
1505
|
+
*
|
|
1506
|
+
* @param data The Buffer to read from.
|
|
1507
|
+
* @param offset The offset to read from.
|
|
1508
|
+
* @returns The 32-bit integer value.
|
|
1509
|
+
*/
|
|
1510
|
+
export function getInt32Val(data: Buffer, offset: number): number {
|
|
1511
|
+
let retval: number = 0;
|
|
1512
|
+
// need to switch this to union based like the others.
|
|
1513
|
+
for (let i: number = 0; i < 4; i++) {
|
|
1514
|
+
// if I don't do the static cast, the top bit will be sign extended.
|
|
1515
|
+
retval |= data[offset + i]! << (i * 8);
|
|
1516
|
+
}
|
|
1517
|
+
return retval;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* Writes a 32-bit integer value into a Buffer at the given offset (little-endian).
|
|
1522
|
+
*
|
|
1523
|
+
* @param buffer The Buffer to write into.
|
|
1524
|
+
* @param offset The offset to write into.
|
|
1525
|
+
* @param value The 32-bit integer value to write.
|
|
1526
|
+
*/
|
|
1527
|
+
export function setInt32Val(buffer: Buffer, offset: number, value: number): void {
|
|
1528
|
+
for (let i: number = 0; i < 4; i++) {
|
|
1529
|
+
buffer[offset + i] = (value >> (i * 8)) & 0xff;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Splits a range into smaller ranges of a given size.
|
|
1535
|
+
*
|
|
1536
|
+
* @param param0 The range to split.
|
|
1537
|
+
* @param size The size of each range.
|
|
1538
|
+
* @returns The split ranges.
|
|
1539
|
+
*/
|
|
1540
|
+
export function splitRange([min, max]: [min: number, max: number], size: number): [from: number, to: number][] {
|
|
1541
|
+
const result: [from: number, to: number][] = [];
|
|
1542
|
+
let start: number = min;
|
|
1543
|
+
|
|
1544
|
+
while (start <= max) {
|
|
1545
|
+
const end: number = Math.min(start + size - 1, max);
|
|
1546
|
+
result.push([start, end]);
|
|
1547
|
+
start = end + 1;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
return result;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* Packs block indices into the buffer using the same scheme as the read loop.
|
|
1555
|
+
*
|
|
1556
|
+
* @param buffer The buffer to write into.
|
|
1557
|
+
* @param blockDataOffset The offset where block data begins in the buffer.
|
|
1558
|
+
* @param block_indices The list of block indices to pack.
|
|
1559
|
+
* @param bitsPerBlock The number of bits used per block.
|
|
1560
|
+
* @param blocksPerWord How many blocks fit inside one 32-bit integer.
|
|
1561
|
+
*/
|
|
1562
|
+
export function writeBlockIndices(buffer: Buffer, blockDataOffset: number, block_indices: number[], bitsPerBlock: number, blocksPerWord: number): void {
|
|
1563
|
+
const wordCount: number = Math.ceil(block_indices.length / blocksPerWord);
|
|
1564
|
+
|
|
1565
|
+
for (let wordIndex: number = 0; wordIndex < wordCount; wordIndex++) {
|
|
1566
|
+
let maskVal: number = 0;
|
|
1567
|
+
|
|
1568
|
+
for (let j: number = 0; j < blocksPerWord; j++) {
|
|
1569
|
+
const blockIndex: number = wordIndex * blocksPerWord + j;
|
|
1570
|
+
if (blockIndex >= block_indices.length) break;
|
|
1571
|
+
|
|
1572
|
+
const blockVal: number = block_indices[blockIndex]!;
|
|
1573
|
+
const shiftAmount: number = j * bitsPerBlock;
|
|
1574
|
+
maskVal |= blockVal /* & ((1 << bitsPerBlock) - 1) */ << shiftAmount;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
setInt32Val(buffer, blockDataOffset + wordIndex * 4, maskVal);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Gets the chunk indices from a LevelDB key.
|
|
1583
|
+
*
|
|
1584
|
+
* The key must be a [chunk key](https://minecraft.wiki/w/Bedrock_Edition_level_format#Chunk_key_format).
|
|
1585
|
+
*
|
|
1586
|
+
* @param key The key to get the chunk indices for, as a Buffer.
|
|
1587
|
+
* @returns The chunk indices.
|
|
1588
|
+
*/
|
|
1589
|
+
export function getChunkKeyIndices(key: Buffer): SubChunkIndexDimensionVectorXZ | DimensionVectorXZ {
|
|
1590
|
+
return {
|
|
1591
|
+
x: getInt32Val(key, 0),
|
|
1592
|
+
z: getInt32Val(key, 4),
|
|
1593
|
+
dimension: [13, 14].includes(key.length) ? dimensions[getInt32Val(key, 8)] ?? "overworld" : "overworld",
|
|
1594
|
+
subChunkIndex: [10, 14].includes(key.length) ? key.at(-1)! : undefined,
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Gets a human-readable version of a LevelDB key.
|
|
1600
|
+
*
|
|
1601
|
+
* @param key The key to get the display name for, as a Buffer.
|
|
1602
|
+
* @returns A human-readable version of the key.
|
|
1603
|
+
*/
|
|
1604
|
+
export function getKeyDisplayName(key: Buffer): string {
|
|
1605
|
+
const contentType: DBEntryContentType = getContentTypeFromDBKey(key);
|
|
1606
|
+
switch (contentType) {
|
|
1607
|
+
case "Data3D":
|
|
1608
|
+
case "Version":
|
|
1609
|
+
case "Data2D":
|
|
1610
|
+
case "Data2DLegacy":
|
|
1611
|
+
case "SubChunkPrefix":
|
|
1612
|
+
case "LegacyTerrain":
|
|
1613
|
+
case "BlockEntity":
|
|
1614
|
+
case "Entity":
|
|
1615
|
+
case "PendingTicks":
|
|
1616
|
+
case "LegacyBlockExtraData":
|
|
1617
|
+
case "BiomeState":
|
|
1618
|
+
case "FinalizedState":
|
|
1619
|
+
case "ConversionData":
|
|
1620
|
+
case "BorderBlocks":
|
|
1621
|
+
case "HardcodedSpawners":
|
|
1622
|
+
case "RandomTicks":
|
|
1623
|
+
case "Checksums":
|
|
1624
|
+
case "MetaDataHash":
|
|
1625
|
+
case "GeneratedPreCavesAndCliffsBlending":
|
|
1626
|
+
case "BlendingBiomeHeight":
|
|
1627
|
+
case "BlendingData":
|
|
1628
|
+
case "ActorDigestVersion":
|
|
1629
|
+
case "LegacyVersion":
|
|
1630
|
+
case "AABBVolumes": {
|
|
1631
|
+
const indices: SubChunkIndexDimensionVectorXZ | DimensionVectorXZ = getChunkKeyIndices(key);
|
|
1632
|
+
return `${indices.dimension}_${indices.x}_${indices.z}${
|
|
1633
|
+
"subChunkIndex" in indices && indices.subChunkIndex ? `_${indices.subChunkIndex}` : ""
|
|
1634
|
+
}_${contentType}`;
|
|
1635
|
+
}
|
|
1636
|
+
case "Digest": {
|
|
1637
|
+
const indices: DimensionVectorXZ = getChunkKeyIndices(key.subarray(4));
|
|
1638
|
+
return `digp_${indices.dimension}_${indices.x}_${indices.z}`;
|
|
1639
|
+
}
|
|
1640
|
+
case "ActorPrefix": {
|
|
1641
|
+
return `actorprefix_${getInt32Val(key, key.length - 8)}_${getInt32Val(key, key.length - 4)}`;
|
|
1642
|
+
}
|
|
1643
|
+
case "AutonomousEntities":
|
|
1644
|
+
case "BiomeData":
|
|
1645
|
+
case "Dimension":
|
|
1646
|
+
case "DynamicProperties":
|
|
1647
|
+
case "FlatWorldLayers":
|
|
1648
|
+
case "ForcedWorldCorruption":
|
|
1649
|
+
case "LevelChunkMetaDataDictionary":
|
|
1650
|
+
case "LevelDat":
|
|
1651
|
+
case "Map":
|
|
1652
|
+
case "MobEvents":
|
|
1653
|
+
case "Player":
|
|
1654
|
+
case "PlayerClient":
|
|
1655
|
+
case "Portals":
|
|
1656
|
+
case "RealmsStoriesData":
|
|
1657
|
+
case "SchedulerWT":
|
|
1658
|
+
case "Scoreboard":
|
|
1659
|
+
case "StructureTemplate":
|
|
1660
|
+
case "TickingArea":
|
|
1661
|
+
case "VillageDwellers":
|
|
1662
|
+
case "VillageInfo":
|
|
1663
|
+
case "VillagePOI":
|
|
1664
|
+
case "VillagePlayers":
|
|
1665
|
+
case "Unknown":
|
|
1666
|
+
default:
|
|
1667
|
+
return key.toString("binary");
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Gets the content type of a LevelDB key.
|
|
1673
|
+
*
|
|
1674
|
+
* @param key The key to get the content type for, as a Buffer.
|
|
1675
|
+
* @returns The content type of the key.
|
|
1676
|
+
*/
|
|
1677
|
+
export function getContentTypeFromDBKey(key: Buffer): DBEntryContentType {
|
|
1678
|
+
if ([9, 10, 13, 14].includes(key.length)) {
|
|
1679
|
+
switch (key.at([10, 14].includes(key.length) ? -2 : -1)) {
|
|
1680
|
+
case 0x2b:
|
|
1681
|
+
return "Data3D";
|
|
1682
|
+
case 0x2c:
|
|
1683
|
+
return "Version";
|
|
1684
|
+
case 0x2d:
|
|
1685
|
+
return "Data2D";
|
|
1686
|
+
case 0x2e:
|
|
1687
|
+
return "Data2DLegacy";
|
|
1688
|
+
case 0x2f:
|
|
1689
|
+
return "SubChunkPrefix";
|
|
1690
|
+
case 0x30:
|
|
1691
|
+
return "LegacyTerrain";
|
|
1692
|
+
case 0x31:
|
|
1693
|
+
return "BlockEntity";
|
|
1694
|
+
case 0x32:
|
|
1695
|
+
return "Entity";
|
|
1696
|
+
case 0x33:
|
|
1697
|
+
return "PendingTicks";
|
|
1698
|
+
case 0x34:
|
|
1699
|
+
return "LegacyBlockExtraData";
|
|
1700
|
+
case 0x35:
|
|
1701
|
+
return "BiomeState";
|
|
1702
|
+
case 0x36:
|
|
1703
|
+
return "FinalizedState";
|
|
1704
|
+
case 0x37:
|
|
1705
|
+
return "ConversionData";
|
|
1706
|
+
case 0x38:
|
|
1707
|
+
return "BorderBlocks";
|
|
1708
|
+
case 0x39:
|
|
1709
|
+
return "HardcodedSpawners";
|
|
1710
|
+
case 0x3a:
|
|
1711
|
+
return "RandomTicks";
|
|
1712
|
+
case 0x3b:
|
|
1713
|
+
return "Checksums";
|
|
1714
|
+
case 0x3d:
|
|
1715
|
+
return "MetaDataHash";
|
|
1716
|
+
case 0x3e:
|
|
1717
|
+
return "GeneratedPreCavesAndCliffsBlending";
|
|
1718
|
+
case 0x3f:
|
|
1719
|
+
return "BlendingBiomeHeight";
|
|
1720
|
+
case 0x40:
|
|
1721
|
+
return "BlendingData";
|
|
1722
|
+
case 0x41:
|
|
1723
|
+
return "ActorDigestVersion";
|
|
1724
|
+
case 0x76:
|
|
1725
|
+
return "LegacyVersion";
|
|
1726
|
+
case 0x77:
|
|
1727
|
+
return "AABBVolumes";
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const stringKey: string = key.toString();
|
|
1731
|
+
switch (stringKey) {
|
|
1732
|
+
case "~local_player":
|
|
1733
|
+
return "Player";
|
|
1734
|
+
case "game_flatworldlayers":
|
|
1735
|
+
return "FlatWorldLayers";
|
|
1736
|
+
case "Overworld":
|
|
1737
|
+
case "Nether":
|
|
1738
|
+
case "TheEnd":
|
|
1739
|
+
return "Dimension";
|
|
1740
|
+
case "mobevents":
|
|
1741
|
+
return "MobEvents";
|
|
1742
|
+
case "BiomeData":
|
|
1743
|
+
return "BiomeData";
|
|
1744
|
+
case "AutonomousEntities":
|
|
1745
|
+
return "AutonomousEntities";
|
|
1746
|
+
case "scoreboard":
|
|
1747
|
+
return "Scoreboard";
|
|
1748
|
+
case "schedulerWT":
|
|
1749
|
+
return "SchedulerWT";
|
|
1750
|
+
case "portals":
|
|
1751
|
+
return "Portals";
|
|
1752
|
+
case "DynamicProperties":
|
|
1753
|
+
return "DynamicProperties";
|
|
1754
|
+
case "LevelChunkMetaDataDictionary":
|
|
1755
|
+
return "LevelChunkMetaDataDictionary";
|
|
1756
|
+
case "SST_SALOG":
|
|
1757
|
+
case "SST_WORD":
|
|
1758
|
+
case "SST_WORD_":
|
|
1759
|
+
case "DedicatedServerForcedCorruption":
|
|
1760
|
+
return "ForcedWorldCorruption";
|
|
1761
|
+
}
|
|
1762
|
+
switch (true) {
|
|
1763
|
+
case stringKey.startsWith("actorprefix"):
|
|
1764
|
+
return "ActorPrefix";
|
|
1765
|
+
case stringKey.startsWith("structuretemplate"):
|
|
1766
|
+
return "StructureTemplate";
|
|
1767
|
+
case stringKey.startsWith("tickingarea"):
|
|
1768
|
+
return "TickingArea";
|
|
1769
|
+
case stringKey.startsWith("portals"):
|
|
1770
|
+
return "Portals";
|
|
1771
|
+
case stringKey.startsWith("map_"):
|
|
1772
|
+
return "Map";
|
|
1773
|
+
case stringKey.startsWith("player_server_"):
|
|
1774
|
+
return "Player";
|
|
1775
|
+
case stringKey.startsWith("player_"):
|
|
1776
|
+
return "PlayerClient";
|
|
1777
|
+
case stringKey.startsWith("digp"):
|
|
1778
|
+
return "Digest";
|
|
1779
|
+
case stringKey.startsWith("RealmsStoriesData_"):
|
|
1780
|
+
return "RealmsStoriesData";
|
|
1781
|
+
case /^VILLAGE_(?:[Oo][Vv][Ee][Rr][Ww][Oo][Rr][Ll][Dd]|[Tt][Hh][Ee]_[Ee][Nn][Dd]|[Nn][Ee][Tt][Hh][Ee][Rr])_[0-9a-f\\-]+_POI$/.test(stringKey):
|
|
1782
|
+
return "VillagePOI";
|
|
1783
|
+
case /^VILLAGE_(?:[Oo][Vv][Ee][Rr][Ww][Oo][Rr][Ll][Dd]|[Tt][Hh][Ee]_[Ee][Nn][Dd]|[Nn][Ee][Tt][Hh][Ee][Rr])_[0-9a-f\\-]+_INFO$/.test(stringKey):
|
|
1784
|
+
return "VillageInfo";
|
|
1785
|
+
case /^VILLAGE_(?:[Oo][Vv][Ee][Rr][Ww][Oo][Rr][Ll][Dd]|[Tt][Hh][Ee]_[Ee][Nn][Dd]|[Nn][Ee][Tt][Hh][Ee][Rr])_[0-9a-f\\-]+_DWELLERS$/.test(stringKey):
|
|
1786
|
+
return "VillageDwellers";
|
|
1787
|
+
case /^VILLAGE_(?:[Oo][Vv][Ee][Rr][Ww][Oo][Rr][Ll][Dd]|[Tt][Hh][Ee]_[Ee][Nn][Dd]|[Nn][Ee][Tt][Hh][Ee][Rr])_[0-9a-f\\-]+_PLAYERS$/.test(stringKey):
|
|
1788
|
+
return "VillagePlayers";
|
|
1789
|
+
}
|
|
1790
|
+
return "Unknown";
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Gets the biome type from its ID.
|
|
1795
|
+
*
|
|
1796
|
+
* @param id The ID of the biome.
|
|
1797
|
+
* @returns The biome type.
|
|
1798
|
+
*/
|
|
1799
|
+
export function getBiomeTypeFromID(id: number): keyof (typeof BiomeData)["int_map"] | undefined {
|
|
1800
|
+
return (Object.keys(BiomeData.int_map) as (keyof (typeof BiomeData)["int_map"])[]).find(
|
|
1801
|
+
(key: keyof (typeof BiomeData)["int_map"]): boolean => BiomeData.int_map[key] === id
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Gets the biome ID from its type.
|
|
1807
|
+
*
|
|
1808
|
+
* @param type The type of the biome.
|
|
1809
|
+
* @returns The biome ID.
|
|
1810
|
+
*/
|
|
1811
|
+
export function getBiomeIDFromType(type: keyof (typeof BiomeData)["int_map"]): number | undefined {
|
|
1812
|
+
return BiomeData.int_map[type];
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Removed as it turns out the long tags were just having their high and low parts swapped.
|
|
1816
|
+
// /**
|
|
1817
|
+
// * Converts a 64-bit Q32.32 Unix timestamp to a Date object.
|
|
1818
|
+
// *
|
|
1819
|
+
// * A 64-bit Q32.32 Unix timestamp is the format of the LastPlayed property in `level.dat`.
|
|
1820
|
+
// *
|
|
1821
|
+
// * @param q The timestamp to convert.
|
|
1822
|
+
// * @returns The corresponding Date object.
|
|
1823
|
+
// */
|
|
1824
|
+
// export function q32_32ToDate(q: bigint): Date {
|
|
1825
|
+
// // extract whole seconds
|
|
1826
|
+
// const seconds: number = Number(q >> 32n);
|
|
1827
|
+
|
|
1828
|
+
// // extract fractional part (0 .. 2^32-1)
|
|
1829
|
+
// const frac: number = Number(q & 0xffff_ffffn);
|
|
1830
|
+
|
|
1831
|
+
// // convert fraction to milliseconds
|
|
1832
|
+
// const millisFrac: number = (frac * 1000) / 2 ** 32;
|
|
1833
|
+
|
|
1834
|
+
// // total milliseconds since epoch
|
|
1835
|
+
// const ms: number = seconds * 1000 + millisFrac;
|
|
1836
|
+
|
|
1837
|
+
// return new Date(ms);
|
|
1838
|
+
// }
|
|
1839
|
+
|
|
1840
|
+
// /**
|
|
1841
|
+
// * Converts a Date object to a 64-bit Q32.32 Unix timestamp.
|
|
1842
|
+
// *
|
|
1843
|
+
// * A 64-bit Q32.32 Unix timestamp is the format of the LastPlayed property in `level.dat`.
|
|
1844
|
+
// *
|
|
1845
|
+
// * @param q The Date object to convert.
|
|
1846
|
+
// * @returns The corresponding timestamp.
|
|
1847
|
+
// */
|
|
1848
|
+
|
|
1849
|
+
// export function dateToQ32_32(date: Date): bigint {
|
|
1850
|
+
// // milliseconds since epoch
|
|
1851
|
+
// const ms: number = date.getTime();
|
|
1852
|
+
|
|
1853
|
+
// // whole seconds
|
|
1854
|
+
// const seconds: bigint = BigInt(Math.floor(ms / 1000));
|
|
1855
|
+
|
|
1856
|
+
// // fractional part in milliseconds
|
|
1857
|
+
// const msFrac: number = ms % 1000;
|
|
1858
|
+
|
|
1859
|
+
// // convert fraction to Q32.32 units (scale milliseconds to 2^32)
|
|
1860
|
+
// const frac: bigint = BigInt(Math.round((msFrac * 2 ** 32) / 1000));
|
|
1861
|
+
|
|
1862
|
+
// return (seconds << 32n) | frac;
|
|
1863
|
+
// }
|
|
1864
|
+
|
|
1865
|
+
//#endregion Functions
|
|
1866
|
+
|
|
1867
|
+
// --------------------------------------------------------------------------------
|
|
1868
|
+
// Classes
|
|
1869
|
+
// --------------------------------------------------------------------------------
|
|
1870
|
+
|
|
1871
|
+
//#region Classes
|
|
1872
|
+
|
|
1873
|
+
/**
|
|
1874
|
+
* @todo
|
|
1875
|
+
*/
|
|
1876
|
+
export class Structure {
|
|
1877
|
+
public target?:
|
|
1878
|
+
| {
|
|
1879
|
+
/**
|
|
1880
|
+
* The type of the target structure data.
|
|
1881
|
+
*/
|
|
1882
|
+
type: "LevelDBEntry";
|
|
1883
|
+
/**
|
|
1884
|
+
* The LevelDB containing the target structure data.
|
|
1885
|
+
*/
|
|
1886
|
+
db: LevelDB;
|
|
1887
|
+
/**
|
|
1888
|
+
* The key of the target structure data in the LevelDB.
|
|
1889
|
+
*/
|
|
1890
|
+
key: Buffer;
|
|
1891
|
+
}
|
|
1892
|
+
| {
|
|
1893
|
+
/**
|
|
1894
|
+
* The type of the target structure data.
|
|
1895
|
+
*/
|
|
1896
|
+
type: "File";
|
|
1897
|
+
/**
|
|
1898
|
+
* The absolute path to the target structure data.
|
|
1899
|
+
*/
|
|
1900
|
+
path: string;
|
|
1901
|
+
}
|
|
1902
|
+
| undefined;
|
|
1903
|
+
public constructor(options: { target?: Structure["target"] }) {
|
|
1904
|
+
this.target = options.target;
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* @todo
|
|
1908
|
+
*/
|
|
1909
|
+
public fillBlocks(from: Vector3, to: Vector3, block: NBTSchemas.NBTSchemaTypes.Block): any {
|
|
1910
|
+
throw new Error("Method not implemented.");
|
|
1911
|
+
}
|
|
1912
|
+
public saveChanges(): any {
|
|
1913
|
+
throw new Error("Method not implemented.");
|
|
1914
|
+
}
|
|
1915
|
+
public delete(): any {
|
|
1916
|
+
throw new Error("Method not implemented.");
|
|
1917
|
+
}
|
|
1918
|
+
public expand(min: Vector3, max: Vector3): any {
|
|
1919
|
+
throw new Error("Method not implemented.");
|
|
1920
|
+
}
|
|
1921
|
+
public shrink(min: Vector3, max: Vector3): any {
|
|
1922
|
+
throw new Error("Method not implemented.");
|
|
1923
|
+
}
|
|
1924
|
+
public move(min: Vector3, max: Vector3): any {
|
|
1925
|
+
throw new Error("Method not implemented.");
|
|
1926
|
+
}
|
|
1927
|
+
public scale(scale: Vector3 | number): any {
|
|
1928
|
+
throw new Error("Method not implemented.");
|
|
1929
|
+
}
|
|
1930
|
+
public rotate(angle: 0 | 90 | 180 | 270, axis: "x" | "y" | "z" = "y"): any {
|
|
1931
|
+
throw new Error("Method not implemented.");
|
|
1932
|
+
}
|
|
1933
|
+
public mirror(axis: "x" | "y" | "z"): any {
|
|
1934
|
+
throw new Error("Method not implemented.");
|
|
1935
|
+
}
|
|
1936
|
+
public clear(): any {
|
|
1937
|
+
throw new Error("Method not implemented.");
|
|
1938
|
+
}
|
|
1939
|
+
public clearSectionData(from: Vector3, to: Vector3): any {
|
|
1940
|
+
throw new Error("Method not implemented.");
|
|
1941
|
+
}
|
|
1942
|
+
public getSectionData(from: Vector3, to: Vector3): StructureSectionData {
|
|
1943
|
+
throw new Error("Method not implemented.");
|
|
1944
|
+
}
|
|
1945
|
+
public replaceSectionData(offset: Vector3, data: StructureSectionData, options: StructureReplaceSectionDataOptions = {}): any {
|
|
1946
|
+
throw new Error("Method not implemented.");
|
|
1947
|
+
}
|
|
1948
|
+
public exportPrismarineNBT(): NBTSchemas.NBTSchemaTypes.StructureTemplate {
|
|
1949
|
+
throw new Error("Method not implemented.");
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* Options for {@link Structure.replaceSectionData}.
|
|
1955
|
+
*/
|
|
1956
|
+
export interface StructureReplaceSectionDataOptions {
|
|
1957
|
+
/**
|
|
1958
|
+
* Whether to automatically expand the structure to fit the data if necessary, instead of cropping it.
|
|
1959
|
+
*
|
|
1960
|
+
* @default false
|
|
1961
|
+
*/
|
|
1962
|
+
autoExpand?: boolean;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
//#endregion Classes
|