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.
@@ -28923,17 +28923,138 @@ var ChunkData = class _ChunkData {
28923
28923
  }
28924
28924
  };
28925
28925
 
28926
+ // src/world/_compression/Lz4.ts
28927
+ init_MineRenderError();
28928
+ function decodeAnvilLz4(data) {
28929
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
28930
+ const blocks = [];
28931
+ let cursor = 0, totalSize = 0;
28932
+ while (true) {
28933
+ if (cursor + 21 > data.length) throw invalid("truncated block header or missing terminator");
28934
+ if (view.getUint32(cursor) !== 1280980034 || view.getUint32(cursor + 4) !== 1819239275) {
28935
+ throw invalid("invalid block signature");
28936
+ }
28937
+ const token = data[cursor + 8];
28938
+ const method = token & 240;
28939
+ const size = view.getInt32(cursor + 9, true);
28940
+ const decodedSize = view.getInt32(cursor + 13, true);
28941
+ const checksum = view.getUint32(cursor + 17, true);
28942
+ cursor += 21;
28943
+ if (method !== 16 && method !== 32) throw invalid("unsupported block method");
28944
+ if (size < 0 || decodedSize < 0 || decodedSize > 2 ** (10 + (token & 15)) || size === 0 !== (decodedSize === 0) || method === 16 && size !== decodedSize) {
28945
+ throw invalid("invalid block lengths");
28946
+ }
28947
+ if (size === 0) {
28948
+ if (checksum !== 0 || cursor !== data.length) throw invalid("invalid terminator or trailing bytes");
28949
+ break;
28950
+ }
28951
+ if (cursor + size > data.length || decodedSize > size * 255) throw invalid("invalid block lengths");
28952
+ blocks.push({ start: cursor, size, decodedSize, raw: method === 16, checksum });
28953
+ totalSize += decodedSize;
28954
+ cursor += size;
28955
+ }
28956
+ const output = new Uint8Array(totalSize);
28957
+ let offset = 0;
28958
+ for (const block of blocks) {
28959
+ const source = data.subarray(block.start, block.start + block.size);
28960
+ const target = output.subarray(offset, offset + block.decodedSize);
28961
+ if (block.raw) target.set(source);
28962
+ else decodeBlock(source, target);
28963
+ if (checksum32(target) !== block.checksum) throw invalid("block checksum mismatch");
28964
+ offset += block.decodedSize;
28965
+ }
28966
+ return output;
28967
+ }
28968
+ function decodeBlock(source, target) {
28969
+ let input = 0, output = 0, matched = false;
28970
+ const length = (nibble) => {
28971
+ let value = nibble;
28972
+ if (value === 15) {
28973
+ let extension;
28974
+ do {
28975
+ if (input === source.length) throw invalid("truncated sequence length");
28976
+ extension = source[input++];
28977
+ value += extension;
28978
+ } while (extension === 255);
28979
+ }
28980
+ return value;
28981
+ };
28982
+ while (input < source.length) {
28983
+ const token = source[input++];
28984
+ const literals = length(token >>> 4);
28985
+ if (input + literals > source.length || output + literals > target.length) {
28986
+ throw invalid("literal sequence exceeds block bounds");
28987
+ }
28988
+ target.set(source.subarray(input, input + literals), output);
28989
+ input += literals;
28990
+ output += literals;
28991
+ if (input === source.length) {
28992
+ if (output !== target.length || matched && literals < 5) throw invalid("invalid final literal sequence");
28993
+ return;
28994
+ }
28995
+ if (input + 2 > source.length) throw invalid("truncated match offset");
28996
+ const distance = source[input] | source[input + 1] << 8;
28997
+ input += 2;
28998
+ if (distance === 0 || distance > output) throw invalid("match offset exceeds block history");
28999
+ const count = length(token & 15) + 4;
29000
+ if (output > target.length - 12 || output + count > target.length - 5) {
29001
+ throw invalid("match exceeds block bounds");
29002
+ }
29003
+ for (let end = output + count; output < end; output++) target[output] = target[output - distance];
29004
+ matched = true;
29005
+ }
29006
+ throw invalid("missing final literal sequence");
29007
+ }
29008
+ function checksum32(data) {
29009
+ const prime1 = 2654435761, prime2 = 2246822519, prime3 = 3266489917, prime4 = 668265263, prime5 = 374761393;
29010
+ const seed = 2538058380;
29011
+ const rotate = (value, bits) => value << bits | value >>> 32 - bits;
29012
+ const round = (value, word) => Math.imul(rotate(value + Math.imul(word, prime2), 13), prime1);
29013
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
29014
+ let cursor = 0, hash;
29015
+ if (data.length >= 16) {
29016
+ let a = seed + prime1 + prime2, b = seed + prime2, c = seed, d = seed - prime1;
29017
+ while (cursor + 16 <= data.length) {
29018
+ a = round(a, view.getUint32(cursor, true));
29019
+ b = round(b, view.getUint32(cursor + 4, true));
29020
+ c = round(c, view.getUint32(cursor + 8, true));
29021
+ d = round(d, view.getUint32(cursor + 12, true));
29022
+ cursor += 16;
29023
+ }
29024
+ hash = rotate(a, 1) + rotate(b, 7) + rotate(c, 12) + rotate(d, 18);
29025
+ } else {
29026
+ hash = seed + prime5;
29027
+ }
29028
+ hash += data.length;
29029
+ while (cursor + 4 <= data.length) {
29030
+ hash = Math.imul(rotate(hash + Math.imul(view.getUint32(cursor, true), prime3), 17), prime4);
29031
+ cursor += 4;
29032
+ }
29033
+ while (cursor < data.length) {
29034
+ hash = Math.imul(rotate(hash + Math.imul(data[cursor++], prime5), 11), prime1);
29035
+ }
29036
+ hash = Math.imul(hash ^ hash >>> 15, prime2);
29037
+ hash = Math.imul(hash ^ hash >>> 13, prime3);
29038
+ return (hash ^ hash >>> 16) & 268435455;
29039
+ }
29040
+ function invalid(detail) {
29041
+ return new MineRenderError(`Invalid Anvil LZ4 stream: ${detail}`);
29042
+ }
29043
+
28926
29044
  // src/world/AnvilParser.ts
28927
29045
  var AnvilParser = class {
28928
29046
  /** Lists occupied chunk positions within the region, using local coordinates from 0 to 31. */
28929
29047
  static getChunkList(data) {
28930
29048
  return this.locations(this.bytes(data)).map(({ x, z }) => ({ x, z }));
28931
29049
  }
28932
- /** Decodes all stored chunks. Gzip, zlib, and uncompressed payloads are supported. */
28933
- static async parse(data) {
29050
+ /** Decodes all stored chunks. Gzip, zlib, LZ4, and uncompressed payloads are supported. */
29051
+ static async parse(data, options = {}) {
29052
+ options.signal?.throwIfAborted();
28934
29053
  const bytes = this.bytes(data);
28935
29054
  const chunks = [];
28936
- for (const location of this.locations(bytes)) chunks.push(await this.readChunk(bytes, location));
29055
+ for (const location of this.locations(bytes)) {
29056
+ chunks.push(await this.abortable(this.readChunk(bytes, location, options), options.signal));
29057
+ }
28937
29058
  return { chunks };
28938
29059
  }
28939
29060
  /**
@@ -28942,13 +29063,28 @@ var AnvilParser = class {
28942
29063
  * @param localZ - Chunk z within the region, from 0 to 31.
28943
29064
  * @returns The chunk with absolute coordinates, or `undefined` if the region has no chunk there.
28944
29065
  */
28945
- static async parseChunk(data, localX, localZ) {
29066
+ static async parseChunk(data, localX, localZ, options = {}) {
29067
+ options.signal?.throwIfAborted();
28946
29068
  if (![localX, localZ].every((value) => Number.isInteger(value) && value >= 0 && value < 32)) {
28947
29069
  throw new RangeError("Region-local chunk coordinates must be integers from 0 to 31");
28948
29070
  }
28949
29071
  const bytes = this.bytes(data);
28950
29072
  const location = this.locations(bytes).find(({ x, z }) => x === localX && z === localZ);
28951
- return location ? this.readChunk(bytes, location) : void 0;
29073
+ return location ? this.abortable(this.readChunk(bytes, location, options), options.signal) : void 0;
29074
+ }
29075
+ static async abortable(promise, signal) {
29076
+ if (!signal) return promise;
29077
+ let abort;
29078
+ const cancelled = new Promise((_, reject) => {
29079
+ abort = () => reject(signal.reason);
29080
+ signal.addEventListener("abort", abort, { once: true });
29081
+ if (signal.aborted) abort();
29082
+ });
29083
+ try {
29084
+ return await Promise.race([promise, cancelled]);
29085
+ } finally {
29086
+ signal.removeEventListener("abort", abort);
29087
+ }
28952
29088
  }
28953
29089
  static bytes(data) {
28954
29090
  return data instanceof Uint8Array ? data : new Uint8Array(data);
@@ -28974,18 +29110,41 @@ var AnvilParser = class {
28974
29110
  }
28975
29111
  return locations;
28976
29112
  }
28977
- static async readChunk(data, location) {
28978
- const compression = data[location.offset + 4];
28979
- if (compression & 128) throw new MineRenderError("External Anvil .mcc chunks are not supported");
28980
- if (compression !== 1 && compression !== 2 && compression !== 3) {
28981
- throw new MineRenderError(`Unsupported Anvil compression ${compression}${compression === 4 ? " (LZ4)" : ""}`);
29113
+ static async readChunk(data, location, options) {
29114
+ options.signal?.throwIfAborted();
29115
+ const format2 = data[location.offset + 4];
29116
+ const external = (format2 & 128) !== 0;
29117
+ const compression = format2 & 127;
29118
+ if (compression !== 1 && compression !== 2 && compression !== 3 && compression !== 4) {
29119
+ throw new MineRenderError(`Unsupported Anvil compression ${compression}`);
28982
29120
  }
28983
29121
  let payload = data.subarray(location.offset + 5, location.offset + 4 + location.length);
28984
- if (compression !== 3) {
29122
+ let expected;
29123
+ if (external) {
29124
+ if (!options.region || !options.readExternalChunk) {
29125
+ throw new MineRenderError("External Anvil .mcc chunks require region coordinates and readExternalChunk");
29126
+ }
29127
+ if (![options.region.x, options.region.z].every(Number.isSafeInteger)) {
29128
+ throw new RangeError("Anvil region coordinates must be safe integers");
29129
+ }
29130
+ expected = { x: options.region.x * 32 + location.x, z: options.region.z * 32 + location.z };
29131
+ if (![expected.x, expected.z].every(Number.isSafeInteger)) {
29132
+ throw new RangeError("External Anvil chunk coordinates must be safe integers");
29133
+ }
29134
+ const externalData = await options.readExternalChunk(expected.x, expected.z, options.signal);
29135
+ options.signal?.throwIfAborted();
29136
+ if (externalData === void 0) throw new MineRenderError(`External Anvil chunk c.${expected.x}.${expected.z}.mcc is missing`);
29137
+ payload = this.bytes(externalData);
29138
+ }
29139
+ if (compression === 4) {
29140
+ payload = decodeAnvilLz4(payload);
29141
+ } else if (compression !== 3) {
28985
29142
  const stream = new Blob([payload]).stream().pipeThrough(new DecompressionStream(compression === 1 ? "gzip" : "deflate"));
28986
29143
  payload = new Uint8Array(await new Response(stream).arrayBuffer());
28987
29144
  }
29145
+ options.signal?.throwIfAborted();
28988
29146
  const nbt = await NBTHelper.fromBuffer(payload, "big");
29147
+ options.signal?.throwIfAborted();
28989
29148
  const root = nbt.value;
28990
29149
  const level = root.Level?.type === "compound" ? root.Level.value : root;
28991
29150
  const dataVersion = root.DataVersion?.type === "int" ? root.DataVersion.value : void 0;
@@ -28998,6 +29157,9 @@ var AnvilParser = class {
28998
29157
  if ((chunk.x % 32 + 32) % 32 !== location.x || (chunk.z % 32 + 32) % 32 !== location.z) {
28999
29158
  throw new MineRenderError("Anvil chunk coordinates do not match its region location");
29000
29159
  }
29160
+ if (expected && (chunk.x !== expected.x || chunk.z !== expected.z)) {
29161
+ throw new MineRenderError(`Anvil chunk coordinates do not match external file c.${expected.x}.${expected.z}.mcc`);
29162
+ }
29001
29163
  for (const section of this.compounds(level.sections ?? level.Sections, "sections")) {
29002
29164
  const y = this.integer(section.Y, "section Y");
29003
29165
  if (section.Blocks || section.Data || section.Add) {
@@ -29129,6 +29291,7 @@ var AnvilWorldSource = class {
29129
29291
  this.cachedBytes = 0;
29130
29292
  this.maxCachedRegions = options.maxCachedRegions ?? 4;
29131
29293
  this.maxCachedBytes = options.maxCachedBytes ?? 64 * 1024 * 1024;
29294
+ this.readExternalChunk = options.readExternalChunk;
29132
29295
  for (const [name, value] of Object.entries({ maxCachedRegions: this.maxCachedRegions, maxCachedBytes: this.maxCachedBytes })) {
29133
29296
  if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a nonnegative safe integer`);
29134
29297
  }
@@ -29142,7 +29305,11 @@ var AnvilWorldSource = class {
29142
29305
  const data = await this.getRegion(regionX, regionZ, signal);
29143
29306
  signal?.throwIfAborted();
29144
29307
  if (!data) return void 0;
29145
- const chunk = await abortable2(AnvilParser.parseChunk(data, x - regionX * 32, z - regionZ * 32), signal);
29308
+ const chunk = await AnvilParser.parseChunk(data, x - regionX * 32, z - regionZ * 32, {
29309
+ region: { x: regionX, z: regionZ },
29310
+ readExternalChunk: this.readExternalChunk,
29311
+ signal
29312
+ });
29146
29313
  signal?.throwIfAborted();
29147
29314
  if (chunk && (chunk.x !== x || chunk.z !== z)) {
29148
29315
  throw new MineRenderError(`Anvil chunk coordinates ${chunk.x},${chunk.z} do not match requested column ${x},${z}`);
@@ -29173,7 +29340,7 @@ var AnvilWorldSource = class {
29173
29340
  settled: false,
29174
29341
  promise: Promise.resolve().then(() => {
29175
29342
  controller.signal.throwIfAborted();
29176
- return this.readRegion(x, z, controller.signal);
29343
+ return abortable2(this.readRegion(x, z, controller.signal), controller.signal);
29177
29344
  }).then((data) => {
29178
29345
  controller.signal.throwIfAborted();
29179
29346
  let bytes = data instanceof Uint8Array ? data : data === void 0 ? void 0 : new Uint8Array(data);