minerender 2.1.0-alpha.90 → 2.1.0-alpha.92

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.
@@ -28972,17 +28972,138 @@ var ChunkData = class _ChunkData {
28972
28972
  }
28973
28973
  };
28974
28974
 
28975
+ // src/world/_compression/Lz4.ts
28976
+ init_MineRenderError();
28977
+ function decodeAnvilLz4(data) {
28978
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
28979
+ const blocks = [];
28980
+ let cursor = 0, totalSize = 0;
28981
+ while (true) {
28982
+ if (cursor + 21 > data.length) throw invalid("truncated block header or missing terminator");
28983
+ if (view.getUint32(cursor) !== 1280980034 || view.getUint32(cursor + 4) !== 1819239275) {
28984
+ throw invalid("invalid block signature");
28985
+ }
28986
+ const token = data[cursor + 8];
28987
+ const method = token & 240;
28988
+ const size = view.getInt32(cursor + 9, true);
28989
+ const decodedSize = view.getInt32(cursor + 13, true);
28990
+ const checksum = view.getUint32(cursor + 17, true);
28991
+ cursor += 21;
28992
+ if (method !== 16 && method !== 32) throw invalid("unsupported block method");
28993
+ if (size < 0 || decodedSize < 0 || decodedSize > 2 ** (10 + (token & 15)) || size === 0 !== (decodedSize === 0) || method === 16 && size !== decodedSize) {
28994
+ throw invalid("invalid block lengths");
28995
+ }
28996
+ if (size === 0) {
28997
+ if (checksum !== 0 || cursor !== data.length) throw invalid("invalid terminator or trailing bytes");
28998
+ break;
28999
+ }
29000
+ if (cursor + size > data.length || decodedSize > size * 255) throw invalid("invalid block lengths");
29001
+ blocks.push({ start: cursor, size, decodedSize, raw: method === 16, checksum });
29002
+ totalSize += decodedSize;
29003
+ cursor += size;
29004
+ }
29005
+ const output = new Uint8Array(totalSize);
29006
+ let offset = 0;
29007
+ for (const block of blocks) {
29008
+ const source = data.subarray(block.start, block.start + block.size);
29009
+ const target = output.subarray(offset, offset + block.decodedSize);
29010
+ if (block.raw) target.set(source);
29011
+ else decodeBlock(source, target);
29012
+ if (checksum32(target) !== block.checksum) throw invalid("block checksum mismatch");
29013
+ offset += block.decodedSize;
29014
+ }
29015
+ return output;
29016
+ }
29017
+ function decodeBlock(source, target) {
29018
+ let input = 0, output = 0, matched = false;
29019
+ const length = (nibble) => {
29020
+ let value = nibble;
29021
+ if (value === 15) {
29022
+ let extension;
29023
+ do {
29024
+ if (input === source.length) throw invalid("truncated sequence length");
29025
+ extension = source[input++];
29026
+ value += extension;
29027
+ } while (extension === 255);
29028
+ }
29029
+ return value;
29030
+ };
29031
+ while (input < source.length) {
29032
+ const token = source[input++];
29033
+ const literals = length(token >>> 4);
29034
+ if (input + literals > source.length || output + literals > target.length) {
29035
+ throw invalid("literal sequence exceeds block bounds");
29036
+ }
29037
+ target.set(source.subarray(input, input + literals), output);
29038
+ input += literals;
29039
+ output += literals;
29040
+ if (input === source.length) {
29041
+ if (output !== target.length || matched && literals < 5) throw invalid("invalid final literal sequence");
29042
+ return;
29043
+ }
29044
+ if (input + 2 > source.length) throw invalid("truncated match offset");
29045
+ const distance = source[input] | source[input + 1] << 8;
29046
+ input += 2;
29047
+ if (distance === 0 || distance > output) throw invalid("match offset exceeds block history");
29048
+ const count = length(token & 15) + 4;
29049
+ if (output > target.length - 12 || output + count > target.length - 5) {
29050
+ throw invalid("match exceeds block bounds");
29051
+ }
29052
+ for (let end = output + count; output < end; output++) target[output] = target[output - distance];
29053
+ matched = true;
29054
+ }
29055
+ throw invalid("missing final literal sequence");
29056
+ }
29057
+ function checksum32(data) {
29058
+ const prime1 = 2654435761, prime2 = 2246822519, prime3 = 3266489917, prime4 = 668265263, prime5 = 374761393;
29059
+ const seed = 2538058380;
29060
+ const rotate = (value, bits) => value << bits | value >>> 32 - bits;
29061
+ const round = (value, word) => Math.imul(rotate(value + Math.imul(word, prime2), 13), prime1);
29062
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
29063
+ let cursor = 0, hash;
29064
+ if (data.length >= 16) {
29065
+ let a = seed + prime1 + prime2, b = seed + prime2, c = seed, d = seed - prime1;
29066
+ while (cursor + 16 <= data.length) {
29067
+ a = round(a, view.getUint32(cursor, true));
29068
+ b = round(b, view.getUint32(cursor + 4, true));
29069
+ c = round(c, view.getUint32(cursor + 8, true));
29070
+ d = round(d, view.getUint32(cursor + 12, true));
29071
+ cursor += 16;
29072
+ }
29073
+ hash = rotate(a, 1) + rotate(b, 7) + rotate(c, 12) + rotate(d, 18);
29074
+ } else {
29075
+ hash = seed + prime5;
29076
+ }
29077
+ hash += data.length;
29078
+ while (cursor + 4 <= data.length) {
29079
+ hash = Math.imul(rotate(hash + Math.imul(view.getUint32(cursor, true), prime3), 17), prime4);
29080
+ cursor += 4;
29081
+ }
29082
+ while (cursor < data.length) {
29083
+ hash = Math.imul(rotate(hash + Math.imul(data[cursor++], prime5), 11), prime1);
29084
+ }
29085
+ hash = Math.imul(hash ^ hash >>> 15, prime2);
29086
+ hash = Math.imul(hash ^ hash >>> 13, prime3);
29087
+ return (hash ^ hash >>> 16) & 268435455;
29088
+ }
29089
+ function invalid(detail) {
29090
+ return new MineRenderError(`Invalid Anvil LZ4 stream: ${detail}`);
29091
+ }
29092
+
28975
29093
  // src/world/AnvilParser.ts
28976
29094
  var AnvilParser = class {
28977
29095
  /** Lists occupied chunk positions within the region, using local coordinates from 0 to 31. */
28978
29096
  static getChunkList(data) {
28979
29097
  return this.locations(this.bytes(data)).map(({ x, z }) => ({ x, z }));
28980
29098
  }
28981
- /** Decodes all stored chunks. Gzip, zlib, and uncompressed payloads are supported. */
28982
- static async parse(data) {
29099
+ /** Decodes all stored chunks. Gzip, zlib, LZ4, and uncompressed payloads are supported. */
29100
+ static async parse(data, options = {}) {
29101
+ options.signal?.throwIfAborted();
28983
29102
  const bytes = this.bytes(data);
28984
29103
  const chunks = [];
28985
- for (const location of this.locations(bytes)) chunks.push(await this.readChunk(bytes, location));
29104
+ for (const location of this.locations(bytes)) {
29105
+ chunks.push(await this.abortable(this.readChunk(bytes, location, options), options.signal));
29106
+ }
28986
29107
  return { chunks };
28987
29108
  }
28988
29109
  /**
@@ -28991,13 +29112,28 @@ var AnvilParser = class {
28991
29112
  * @param localZ - Chunk z within the region, from 0 to 31.
28992
29113
  * @returns The chunk with absolute coordinates, or `undefined` if the region has no chunk there.
28993
29114
  */
28994
- static async parseChunk(data, localX, localZ) {
29115
+ static async parseChunk(data, localX, localZ, options = {}) {
29116
+ options.signal?.throwIfAborted();
28995
29117
  if (![localX, localZ].every((value) => Number.isInteger(value) && value >= 0 && value < 32)) {
28996
29118
  throw new RangeError("Region-local chunk coordinates must be integers from 0 to 31");
28997
29119
  }
28998
29120
  const bytes = this.bytes(data);
28999
29121
  const location = this.locations(bytes).find(({ x, z }) => x === localX && z === localZ);
29000
- return location ? this.readChunk(bytes, location) : void 0;
29122
+ return location ? this.abortable(this.readChunk(bytes, location, options), options.signal) : void 0;
29123
+ }
29124
+ static async abortable(promise, signal) {
29125
+ if (!signal) return promise;
29126
+ let abort;
29127
+ const cancelled = new Promise((_, reject) => {
29128
+ abort = () => reject(signal.reason);
29129
+ signal.addEventListener("abort", abort, { once: true });
29130
+ if (signal.aborted) abort();
29131
+ });
29132
+ try {
29133
+ return await Promise.race([promise, cancelled]);
29134
+ } finally {
29135
+ signal.removeEventListener("abort", abort);
29136
+ }
29001
29137
  }
29002
29138
  static bytes(data) {
29003
29139
  return data instanceof Uint8Array ? data : new Uint8Array(data);
@@ -29023,18 +29159,41 @@ var AnvilParser = class {
29023
29159
  }
29024
29160
  return locations;
29025
29161
  }
29026
- static async readChunk(data, location) {
29027
- const compression = data[location.offset + 4];
29028
- if (compression & 128) throw new MineRenderError("External Anvil .mcc chunks are not supported");
29029
- if (compression !== 1 && compression !== 2 && compression !== 3) {
29030
- throw new MineRenderError(`Unsupported Anvil compression ${compression}${compression === 4 ? " (LZ4)" : ""}`);
29162
+ static async readChunk(data, location, options) {
29163
+ options.signal?.throwIfAborted();
29164
+ const format2 = data[location.offset + 4];
29165
+ const external = (format2 & 128) !== 0;
29166
+ const compression = format2 & 127;
29167
+ if (compression !== 1 && compression !== 2 && compression !== 3 && compression !== 4) {
29168
+ throw new MineRenderError(`Unsupported Anvil compression ${compression}`);
29031
29169
  }
29032
29170
  let payload = data.subarray(location.offset + 5, location.offset + 4 + location.length);
29033
- if (compression !== 3) {
29171
+ let expected;
29172
+ if (external) {
29173
+ if (!options.region || !options.readExternalChunk) {
29174
+ throw new MineRenderError("External Anvil .mcc chunks require region coordinates and readExternalChunk");
29175
+ }
29176
+ if (![options.region.x, options.region.z].every(Number.isSafeInteger)) {
29177
+ throw new RangeError("Anvil region coordinates must be safe integers");
29178
+ }
29179
+ expected = { x: options.region.x * 32 + location.x, z: options.region.z * 32 + location.z };
29180
+ if (![expected.x, expected.z].every(Number.isSafeInteger)) {
29181
+ throw new RangeError("External Anvil chunk coordinates must be safe integers");
29182
+ }
29183
+ const externalData = await options.readExternalChunk(expected.x, expected.z, options.signal);
29184
+ options.signal?.throwIfAborted();
29185
+ if (externalData === void 0) throw new MineRenderError(`External Anvil chunk c.${expected.x}.${expected.z}.mcc is missing`);
29186
+ payload = this.bytes(externalData);
29187
+ }
29188
+ if (compression === 4) {
29189
+ payload = decodeAnvilLz4(payload);
29190
+ } else if (compression !== 3) {
29034
29191
  const stream = new Blob([payload]).stream().pipeThrough(new DecompressionStream(compression === 1 ? "gzip" : "deflate"));
29035
29192
  payload = new Uint8Array(await new Response(stream).arrayBuffer());
29036
29193
  }
29194
+ options.signal?.throwIfAborted();
29037
29195
  const nbt = await NBTHelper.fromBuffer(payload, "big");
29196
+ options.signal?.throwIfAborted();
29038
29197
  const root = nbt.value;
29039
29198
  const level = root.Level?.type === "compound" ? root.Level.value : root;
29040
29199
  const dataVersion = root.DataVersion?.type === "int" ? root.DataVersion.value : void 0;
@@ -29047,6 +29206,9 @@ var AnvilParser = class {
29047
29206
  if ((chunk.x % 32 + 32) % 32 !== location.x || (chunk.z % 32 + 32) % 32 !== location.z) {
29048
29207
  throw new MineRenderError("Anvil chunk coordinates do not match its region location");
29049
29208
  }
29209
+ if (expected && (chunk.x !== expected.x || chunk.z !== expected.z)) {
29210
+ throw new MineRenderError(`Anvil chunk coordinates do not match external file c.${expected.x}.${expected.z}.mcc`);
29211
+ }
29050
29212
  for (const section of this.compounds(level.sections ?? level.Sections, "sections")) {
29051
29213
  const y = this.integer(section.Y, "section Y");
29052
29214
  if (section.Blocks || section.Data || section.Add) {
@@ -29178,6 +29340,7 @@ var AnvilWorldSource = class {
29178
29340
  this.cachedBytes = 0;
29179
29341
  this.maxCachedRegions = options.maxCachedRegions ?? 4;
29180
29342
  this.maxCachedBytes = options.maxCachedBytes ?? 64 * 1024 * 1024;
29343
+ this.readExternalChunk = options.readExternalChunk;
29181
29344
  for (const [name, value] of Object.entries({ maxCachedRegions: this.maxCachedRegions, maxCachedBytes: this.maxCachedBytes })) {
29182
29345
  if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a nonnegative safe integer`);
29183
29346
  }
@@ -29191,7 +29354,11 @@ var AnvilWorldSource = class {
29191
29354
  const data = await this.getRegion(regionX, regionZ, signal);
29192
29355
  signal?.throwIfAborted();
29193
29356
  if (!data) return void 0;
29194
- const chunk = await abortable2(AnvilParser.parseChunk(data, x - regionX * 32, z - regionZ * 32), signal);
29357
+ const chunk = await AnvilParser.parseChunk(data, x - regionX * 32, z - regionZ * 32, {
29358
+ region: { x: regionX, z: regionZ },
29359
+ readExternalChunk: this.readExternalChunk,
29360
+ signal
29361
+ });
29195
29362
  signal?.throwIfAborted();
29196
29363
  if (chunk && (chunk.x !== x || chunk.z !== z)) {
29197
29364
  throw new MineRenderError(`Anvil chunk coordinates ${chunk.x},${chunk.z} do not match requested column ${x},${z}`);
@@ -29222,7 +29389,7 @@ var AnvilWorldSource = class {
29222
29389
  settled: false,
29223
29390
  promise: Promise.resolve().then(() => {
29224
29391
  controller.signal.throwIfAborted();
29225
- return this.readRegion(x, z, controller.signal);
29392
+ return abortable2(this.readRegion(x, z, controller.signal), controller.signal);
29226
29393
  }).then((data) => {
29227
29394
  controller.signal.throwIfAborted();
29228
29395
  let bytes = data instanceof Uint8Array ? data : data === void 0 ? void 0 : new Uint8Array(data);