minerender 2.1.0-alpha.142 → 2.1.0-alpha.144

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.
@@ -264,8 +264,6 @@ async function sleep(timeout) {
264
264
  });
265
265
  }
266
266
  function yieldToEventLoop() {
267
- const scheduler = globalThis.scheduler;
268
- if (scheduler?.yield) return scheduler.yield();
269
267
  return new Promise((resolve) => {
270
268
  if (typeof MessageChannel !== "undefined") {
271
269
  const channel = new MessageChannel();
@@ -18065,6 +18063,7 @@ var AnvilWorldSource = class {
18065
18063
  };
18066
18064
 
18067
18065
  // src/world/Chunk.ts
18066
+ init_BlockObject();
18068
18067
  var import_three47 = require("three");
18069
18068
  init_BlockStates();
18070
18069
  init_AssetKey();
@@ -18075,6 +18074,7 @@ init_Model();
18075
18074
  init_util();
18076
18075
  init_AssetKey();
18077
18076
  init_BlockStates();
18077
+ init_BlockEntities();
18078
18078
  init_CubeFace();
18079
18079
 
18080
18080
  // src/world/SectionModels.ts
@@ -18172,6 +18172,10 @@ var SectionModels = class {
18172
18172
  biomeTint: source ? [...templates.values()].some((template) => template.biomeTint?.some((value) => value === 1)) || void 0 : void 0
18173
18173
  };
18174
18174
  }
18175
+ /** Starts loading the state's models and atlases so a later `prepareState` with the same properties finds them loaded. */
18176
+ async preload(blockState3, properties = {}) {
18177
+ await this.state(blockState3, properties);
18178
+ }
18175
18179
  async state(blockState3, properties) {
18176
18180
  let states = this.states.get(blockState3);
18177
18181
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
@@ -18194,22 +18198,24 @@ var SectionModels = class {
18194
18198
  async prepare(blockState3, properties) {
18195
18199
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
18196
18200
  const groups = BlockStateResolver.matching(blockState3, state);
18197
- if (!groups.length) return void 0;
18201
+ if (!groups.length || groups.some((group) => Array.isArray(group) && !group.length)) return void 0;
18202
+ const variants = groups.flatMap((group) => Array.isArray(group) ? group : [group]);
18203
+ if (variants.some((variant) => !variant.model)) return void 0;
18204
+ const settled = await Promise.allSettled(variants.map(async (variant) => {
18205
+ const modelKey = AssetKey.parse("models", variant.model);
18206
+ modelKey.root = blockState3.key?.root;
18207
+ const model = await Models.getMerged(modelKey);
18208
+ if (!model?.elements?.length) return void 0;
18209
+ const atlas = await UVMapper.getAtlas(model);
18210
+ const rotation = BlockStateResolver.rotation(variant);
18211
+ if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
18212
+ return { model, atlas, rotation };
18213
+ }));
18198
18214
  const prepared = /* @__PURE__ */ new Map();
18199
- for (const group of groups) {
18200
- const choices = Array.isArray(group) ? group : [group];
18201
- if (!choices.length) return void 0;
18202
- for (const variant of choices) {
18203
- if (!variant.model) return void 0;
18204
- const modelKey = AssetKey.parse("models", variant.model);
18205
- modelKey.root = blockState3.key?.root;
18206
- const model = await Models.getMerged(modelKey);
18207
- if (!model?.elements?.length) return void 0;
18208
- const atlas = await UVMapper.getAtlas(model);
18209
- const rotation = BlockStateResolver.rotation(variant);
18210
- if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
18211
- prepared.set(variant, { model, atlas, rotation });
18212
- }
18215
+ for (const [i, result] of settled.entries()) {
18216
+ if (result.status === "rejected") throw result.reason;
18217
+ if (!result.value) return void 0;
18218
+ prepared.set(variants[i], result.value);
18213
18219
  }
18214
18220
  return { state, groups, choices: prepared };
18215
18221
  }
@@ -18669,6 +18675,7 @@ function skipsBlockFace(data, rule, block, neighbor, face) {
18669
18675
  // src/world/MineRenderWorld.ts
18670
18676
  init_log();
18671
18677
  var log19 = logger("MineRenderWorld");
18678
+ var YIELD_INTERVAL_MS = 8;
18672
18679
  var MineRenderWorld = class {
18673
18680
  constructor(scene, options = {}) {
18674
18681
  this._chunks = /* @__PURE__ */ new Map();
@@ -18782,7 +18789,7 @@ var MineRenderWorld = class {
18782
18789
  failures.set(chunk, error);
18783
18790
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18784
18791
  }
18785
- if (performance.now() - sliceStart > 8) {
18792
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18786
18793
  await yieldToEventLoop();
18787
18794
  sliceStart = performance.now();
18788
18795
  }
@@ -18883,15 +18890,18 @@ var MineRenderWorld = class {
18883
18890
  const key = AssetKey.parse("blockstates", block.type);
18884
18891
  keys.set(key.serialize(), key);
18885
18892
  }
18886
- await BlockStates.getAll(keys.values());
18893
+ const loaded = await BlockStates.getAll(keys.values());
18894
+ const states = new Map([...keys.keys()].map((key, i) => [key, loaded[i]]));
18887
18895
  if (!useBatches) {
18888
18896
  for (const block of multiblock.blocks) await place(block);
18889
18897
  return;
18890
18898
  }
18891
18899
  if (!executor) {
18892
18900
  const groups2 = /* @__PURE__ */ new Map();
18901
+ const distinct = /* @__PURE__ */ new Map();
18893
18902
  const position = new import_three45.Vector3();
18894
18903
  for (const block of multiblock.blocks) {
18904
+ if (this.sectionModels && !Chunk.isAir(block)) distinct.set(ChunkData.paletteKey(block), block);
18895
18905
  const [x, y, z] = block.position;
18896
18906
  this.validatePosBounds(position.set(x, y, z));
18897
18907
  const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
@@ -18902,6 +18912,16 @@ var MineRenderWorld = class {
18902
18912
  if (!group) groups2.set(chunk, group = []);
18903
18913
  group.push({ index: (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16, block });
18904
18914
  }
18915
+ if (this.sectionModels) {
18916
+ for (const block of distinct.values()) {
18917
+ const state = states.get(AssetKey.parse("blockstates", block.type).serialize());
18918
+ if (!state) continue;
18919
+ void (async () => {
18920
+ if (state.key && BlockEntities.entry(await BlockEntities.getIndex(state.key.root), state.key.toNamespacedString())) return;
18921
+ await this.sectionModels.preload(state, block.properties);
18922
+ })().catch(() => void 0);
18923
+ }
18924
+ }
18905
18925
  await this.placeChunkGroups(groups2, changes);
18906
18926
  return;
18907
18927
  }
@@ -18937,7 +18957,7 @@ var MineRenderWorld = class {
18937
18957
  )));
18938
18958
  for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
18939
18959
  }
18940
- if (performance.now() - sliceStart > 8) {
18960
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18941
18961
  await yieldToEventLoop();
18942
18962
  sliceStart = performance.now();
18943
18963
  }
@@ -18945,6 +18965,62 @@ var MineRenderWorld = class {
18945
18965
  }
18946
18966
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
18947
18967
  async placeChunk(chunk, executor) {
18968
+ return this.placeColumn(chunk, executor);
18969
+ }
18970
+ /**
18971
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
18972
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
18973
+ */
18974
+ async placeChunks(chunks) {
18975
+ const columns = [];
18976
+ for (const chunk of chunks) {
18977
+ const { x, z } = chunk ?? {};
18978
+ if (![x, z].every(Number.isSafeInteger)) {
18979
+ throw new RangeError("Chunk column coordinates must be safe integers");
18980
+ }
18981
+ const sections = Array.from(chunk.sections, (section) => {
18982
+ const { y } = section ?? {};
18983
+ if (!Number.isSafeInteger(y)) throw new RangeError("Chunk section coordinates must be safe integers");
18984
+ return { ...section, y };
18985
+ });
18986
+ columns.push({ ...chunk, x, z, sections });
18987
+ }
18988
+ if (!columns.length) return;
18989
+ await this.culling;
18990
+ let failed = false, failure, sliceStart = performance.now();
18991
+ try {
18992
+ for (const column of columns) {
18993
+ await this.placeColumn(column, void 0, true);
18994
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18995
+ await yieldToEventLoop();
18996
+ sliceStart = performance.now();
18997
+ }
18998
+ }
18999
+ } catch (error) {
19000
+ failed = true;
19001
+ failure = error;
19002
+ } finally {
19003
+ try {
19004
+ const failures = await this.refreshBiomes(true);
19005
+ if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
19006
+ } catch (error) {
19007
+ if (!failed) {
19008
+ failed = true;
19009
+ failure = error;
19010
+ }
19011
+ }
19012
+ try {
19013
+ await this.updateCulling([]);
19014
+ } catch (error) {
19015
+ if (!failed) {
19016
+ failed = true;
19017
+ failure = error;
19018
+ }
19019
+ }
19020
+ }
19021
+ if (failed) throw failure;
19022
+ }
19023
+ async placeColumn(chunk, executor, deferCulling = false) {
18948
19024
  if (![chunk.x, chunk.z].every(Number.isSafeInteger)) {
18949
19025
  throw new RangeError("Chunk column coordinates must be safe integers");
18950
19026
  }
@@ -19006,7 +19082,7 @@ var MineRenderWorld = class {
19006
19082
  } finally {
19007
19083
  this.markSectionChanged(target);
19008
19084
  }
19009
- if (performance.now() - sliceStart > 8) {
19085
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19010
19086
  await yieldToEventLoop();
19011
19087
  sliceStart = performance.now();
19012
19088
  }
@@ -19029,9 +19105,12 @@ var MineRenderWorld = class {
19029
19105
  await placeEntities?.();
19030
19106
  } finally {
19031
19107
  this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
19032
- const failures = await this.refreshBiomes();
19033
- if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
19034
- await this.updateCulling([...changes.values()]);
19108
+ if (deferCulling) await this.updateCulling([...changes.values()], true);
19109
+ else {
19110
+ const failures = await this.refreshBiomes();
19111
+ if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
19112
+ await this.updateCulling([...changes.values()]);
19113
+ }
19035
19114
  }
19036
19115
  }
19037
19116
  /**
@@ -19061,7 +19140,7 @@ var MineRenderWorld = class {
19061
19140
  for (const { x, z } of columns.values()) {
19062
19141
  this.entities?.clearColumn(x, z);
19063
19142
  await this.clearChunkColumn(x, z);
19064
- if (performance.now() - sliceStart > 8) {
19143
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19065
19144
  await yieldToEventLoop();
19066
19145
  sliceStart = performance.now();
19067
19146
  }
@@ -19200,15 +19279,25 @@ var MineRenderWorld = class {
19200
19279
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19201
19280
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
19202
19281
  chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19203
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19282
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19204
19283
  await yieldToEventLoop();
19205
19284
  sliceStart = performance.now();
19206
19285
  }
19207
19286
  }
19208
- const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19209
- await chunk.rebuildSectionMesh();
19210
- batch.delete(chunk);
19211
- }));
19287
+ const pending = [];
19288
+ for (const chunk of [...batch.keys()]) {
19289
+ const build = (async () => {
19290
+ await chunk.rebuildSectionMesh();
19291
+ batch.delete(chunk);
19292
+ })();
19293
+ void build.catch(() => void 0);
19294
+ pending.push(build);
19295
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19296
+ await yieldToEventLoop();
19297
+ sliceStart = performance.now();
19298
+ }
19299
+ }
19300
+ const builds = await Promise.allSettled(pending);
19212
19301
  const failure = builds.find((result) => result.status === "rejected");
19213
19302
  if (failure?.status === "rejected") throw failure.reason;
19214
19303
  } catch (error) {
@@ -20159,8 +20248,10 @@ var Chunk = class _Chunk {
20159
20248
  if (rejected?.status === "rejected") throw rejected.reason;
20160
20249
  }
20161
20250
  const positions = [];
20162
- let failed = false, failure;
20163
- for (const { index, key, block } of placements) {
20251
+ const jobs = /* @__PURE__ */ new Map();
20252
+ let failureOrder = Infinity, failure;
20253
+ for (const [order, { index, key, block }] of placements.entries()) {
20254
+ jobs.delete(index);
20164
20255
  this.hiddenBlocks.delete(index);
20165
20256
  this.data.assign(index, this.data.intern(block), block?.nbt);
20166
20257
  const readBlock = this.data.snapshot(index);
@@ -20176,7 +20267,6 @@ var Chunk = class _Chunk {
20176
20267
  this.z * 16 + Math.floor(index / 16) % 16
20177
20268
  );
20178
20269
  positions.push(worldPos);
20179
- let object4;
20180
20270
  try {
20181
20271
  if (!readBlock) continue;
20182
20272
  const resolved = resolutions.get(key);
@@ -20198,7 +20288,7 @@ var Chunk = class _Chunk {
20198
20288
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
20199
20289
  this.meshDirty = true;
20200
20290
  } else {
20201
- object4 = await this.scene.addBlock(blockState3, {
20291
+ jobs.set(index, { order, readBlock, blockState: blockState3, position: MineRenderWorld.worldToScenePosition(worldPos), options: {
20202
20292
  mergeMeshes: true,
20203
20293
  instanceMeshes: true,
20204
20294
  maxInstanceCount: 2e3,
@@ -20206,13 +20296,12 @@ var Chunk = class _Chunk {
20206
20296
  variantPosition,
20207
20297
  biome: context?.biome,
20208
20298
  biomePosition: this.biomePosition(index, blockState3, stored)
20209
- });
20210
- object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
20299
+ } });
20300
+ continue;
20211
20301
  }
20212
20302
  this.renderedBlocks.set(index, { get block() {
20213
20303
  return readBlock();
20214
- }, object: object4 });
20215
- if (object4) this.objectBlocks.add(index);
20304
+ }, object: void 0 });
20216
20305
  } catch (error) {
20217
20306
  if (preserveDataOnFailure && readBlock) {
20218
20307
  this.renderedBlocks.set(index, { get block() {
@@ -20220,12 +20309,35 @@ var Chunk = class _Chunk {
20220
20309
  }, object: void 0 });
20221
20310
  } else this.data.assign(index, 0);
20222
20311
  this.fluidCells[index] = 0;
20223
- object4?.removeFromScene();
20224
- if (!failed) failure = error;
20225
- failed = true;
20312
+ if (order < failureOrder) {
20313
+ failureOrder = order;
20314
+ failure = error;
20315
+ }
20226
20316
  }
20227
20317
  }
20228
- if (failed) throw failure;
20318
+ const pending = [...jobs];
20319
+ const objects = await Promise.allSettled(pending.map(([, job]) => this.createBlockObject(job.blockState, job.position, job.options)));
20320
+ for (const [i, [index, { order, readBlock }]] of pending.entries()) {
20321
+ const result = objects[i];
20322
+ if (result.status === "fulfilled") {
20323
+ this.renderedBlocks.set(index, { get block() {
20324
+ return readBlock();
20325
+ }, object: result.value });
20326
+ this.objectBlocks.add(index);
20327
+ } else {
20328
+ if (preserveDataOnFailure) {
20329
+ this.renderedBlocks.set(index, { get block() {
20330
+ return readBlock();
20331
+ }, object: void 0 });
20332
+ } else this.data.assign(index, 0);
20333
+ this.fluidCells[index] = 0;
20334
+ if (order < failureOrder) {
20335
+ failureOrder = order;
20336
+ failure = result.reason;
20337
+ }
20338
+ }
20339
+ }
20340
+ if (failureOrder < Infinity) throw failure;
20229
20341
  return positions;
20230
20342
  }
20231
20343
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
@@ -20296,37 +20408,42 @@ var Chunk = class _Chunk {
20296
20408
  failed = true;
20297
20409
  }
20298
20410
  }
20299
- for (const { index, blockState: blockState3, biome } of perBlocks) {
20300
- let object4;
20301
- try {
20302
- object4 = await this.scene.addBlock(blockState3, {
20303
- mergeMeshes: true,
20304
- instanceMeshes: true,
20305
- maxInstanceCount: 2e3,
20306
- initialState: this.data.stateAt(this.data.idAt(index)).properties,
20307
- variantPosition: this.positionAt(index),
20308
- biome,
20309
- biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
20310
- });
20311
- object4.setPosition(new import_three47.Vector3(
20312
- this.x * 256 + index % 16 * 16,
20313
- this.y * 256 + Math.floor(index / 256) * 16,
20314
- this.z * 256 + Math.floor(index / 16) % 16 * 16
20315
- ));
20316
- this.renderedBlocks.get(index).object = object4;
20411
+ const objects = await Promise.allSettled(perBlocks.map(({ index, blockState: blockState3, biome }) => this.createBlockObject(blockState3, new import_three47.Vector3(
20412
+ this.x * 256 + index % 16 * 16,
20413
+ this.y * 256 + Math.floor(index / 256) * 16,
20414
+ this.z * 256 + Math.floor(index / 16) % 16 * 16
20415
+ ), {
20416
+ mergeMeshes: true,
20417
+ instanceMeshes: true,
20418
+ maxInstanceCount: 2e3,
20419
+ initialState: this.data.stateAt(this.data.idAt(index)).properties,
20420
+ variantPosition: this.positionAt(index),
20421
+ biome,
20422
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
20423
+ })));
20424
+ for (const [i, { index }] of perBlocks.entries()) {
20425
+ const result = objects[i];
20426
+ if (result.status === "fulfilled") {
20427
+ this.renderedBlocks.get(index).object = result.value;
20317
20428
  this.objectBlocks.add(index);
20318
- } catch (error) {
20429
+ } else {
20319
20430
  this.data.assign(index, 0);
20320
20431
  this.fluidCells[index] = 0;
20321
20432
  this.renderedBlocks.delete(index);
20322
20433
  this.objectBlocks.delete(index);
20323
- object4?.removeFromScene();
20324
- if (!failed) failure = error;
20434
+ if (!failed) failure = result.reason;
20325
20435
  failed = true;
20326
20436
  }
20327
20437
  }
20328
20438
  if (failed) throw failure;
20329
20439
  }
20440
+ createBlockObject(blockState3, position, options) {
20441
+ return this.scene.addSceneObject(blockState3, () => {
20442
+ const object4 = new BlockObject(blockState3, options);
20443
+ object4.position.copy(position);
20444
+ return object4;
20445
+ }, options);
20446
+ }
20330
20447
  positionAt(index) {
20331
20448
  return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
20332
20449
  }
@@ -20667,6 +20784,7 @@ var Chunk = class _Chunk {
20667
20784
  };
20668
20785
 
20669
20786
  // src/world/WorldStreamer.ts
20787
+ var MAX_PREFETCHED_COLUMNS = 4;
20670
20788
  var WorldStreamer = class {
20671
20789
  constructor(world, source, options = {}) {
20672
20790
  this.world = world;
@@ -20676,6 +20794,7 @@ var WorldStreamer = class {
20676
20794
  this.missing = /* @__PURE__ */ new Map();
20677
20795
  this.failed = /* @__PURE__ */ new Map();
20678
20796
  this.desired = [];
20797
+ this.reads = /* @__PURE__ */ new Map();
20679
20798
  this.disposed = false;
20680
20799
  this.loadRadius = options.loadRadius ?? 2;
20681
20800
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -20720,6 +20839,12 @@ var WorldStreamer = class {
20720
20839
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
20721
20840
  this.loading.controller.abort();
20722
20841
  }
20842
+ for (const [key, read] of this.reads) {
20843
+ if (!this.within(read.position, this.loadRadius)) {
20844
+ read.controller.abort();
20845
+ if (read.ready) this.reads.delete(key);
20846
+ }
20847
+ }
20723
20848
  }
20724
20849
  return this.running ?? (this.running = Promise.resolve().then(() => this.drain()));
20725
20850
  }
@@ -20740,8 +20865,11 @@ var WorldStreamer = class {
20740
20865
  this.desired = [];
20741
20866
  this.center = void 0;
20742
20867
  this.loading?.controller.abort();
20868
+ for (const read of this.reads.values()) read.controller.abort();
20743
20869
  return this.disposal ?? (this.disposal = Promise.resolve().then(async () => {
20744
20870
  await this.running?.catch(() => void 0);
20871
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20872
+ this.reads.clear();
20745
20873
  try {
20746
20874
  await this.unloadMany([...this.owned.values()]);
20747
20875
  } finally {
@@ -20767,6 +20895,10 @@ var WorldStreamer = class {
20767
20895
  await this.unloadMany(obsolete);
20768
20896
  continue;
20769
20897
  }
20898
+ if (this.world.placeChunks) {
20899
+ if (await this.placeReadyChunks()) continue;
20900
+ return;
20901
+ }
20770
20902
  const position = this.desired.find((position2) => this.needsLoad(position2));
20771
20903
  if (!position) return;
20772
20904
  let chunk;
@@ -20789,26 +20921,105 @@ var WorldStreamer = class {
20789
20921
  continue;
20790
20922
  }
20791
20923
  this.owned.set(this.key(position), position);
20924
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
20925
+ }
20926
+ } finally {
20927
+ for (const [key, read] of this.reads) {
20928
+ read.controller.abort();
20929
+ if (read.ready) this.reads.delete(key);
20930
+ }
20931
+ this.running = void 0;
20932
+ }
20933
+ }
20934
+ fillReadSlots() {
20935
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
20936
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
20937
+ if (!position) return;
20938
+ const key = this.key(position);
20939
+ const read = {
20940
+ position,
20941
+ controller: new AbortController(),
20942
+ promise: Promise.resolve(),
20943
+ ready: false,
20944
+ failed: false
20945
+ };
20946
+ this.reads.set(key, read);
20947
+ read.promise = (async () => {
20792
20948
  try {
20793
- await this.world.placeChunk(chunk);
20794
- this.loaded.set(this.key(position), position);
20949
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
20950
+ if (read.chunk) this.validateChunk(read.chunk, position);
20795
20951
  } catch (error) {
20796
- try {
20797
- await this.unloadMany([position]);
20798
- } catch (cleanupError) {
20799
- const failure = error instanceof Error ? error : new Error(String(error));
20800
- try {
20801
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20802
- } catch {
20803
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
20804
- }
20805
- throw failure;
20952
+ read.failed = true;
20953
+ read.error = error;
20954
+ } finally {
20955
+ read.ready = true;
20956
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
20957
+ this.reads.delete(key);
20806
20958
  }
20807
- throw error;
20959
+ }
20960
+ })();
20961
+ }
20962
+ }
20963
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
20964
+ async placeReadyChunks() {
20965
+ this.fillReadSlots();
20966
+ const position = this.desired.find((position2) => this.needsLoad(position2));
20967
+ if (!position) {
20968
+ if (!this.reads.size) return false;
20969
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20970
+ return true;
20971
+ }
20972
+ const head = this.reads.get(this.key(position));
20973
+ if (!head) {
20974
+ for (let index = this.desired.length - 1; index >= 0; index--) {
20975
+ const key = this.key(this.desired[index]);
20976
+ if (this.reads.get(key)?.ready) {
20977
+ this.reads.delete(key);
20978
+ return true;
20808
20979
  }
20809
20980
  }
20810
- } finally {
20811
- this.running = void 0;
20981
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
20982
+ return true;
20983
+ }
20984
+ if (!head.ready) {
20985
+ await head.promise;
20986
+ return true;
20987
+ }
20988
+ const chunks = [], positions = [];
20989
+ for (const desired of this.desired) {
20990
+ if (!this.needsLoad(desired)) continue;
20991
+ const key = this.key(desired), read = this.reads.get(key);
20992
+ if (!read?.ready) break;
20993
+ this.reads.delete(key);
20994
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
20995
+ else if (!read.chunk) this.missing.set(key, desired);
20996
+ else {
20997
+ chunks.push(read.chunk);
20998
+ positions.push(desired);
20999
+ }
21000
+ }
21001
+ if (!chunks.length) return true;
21002
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
21003
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
21004
+ return true;
21005
+ }
21006
+ async placeOwnedColumns(positions, place) {
21007
+ try {
21008
+ await place();
21009
+ for (const position of positions) this.loaded.set(this.key(position), position);
21010
+ } catch (error) {
21011
+ try {
21012
+ await this.unloadMany(positions);
21013
+ } catch (cleanupError) {
21014
+ const failure = error instanceof Error ? error : new Error(String(error));
21015
+ try {
21016
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
21017
+ } catch {
21018
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
21019
+ }
21020
+ throw failure;
21021
+ }
21022
+ throw error;
20812
21023
  }
20813
21024
  }
20814
21025
  needsLoad(position) {