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.
@@ -184,8 +184,6 @@ async function sleep(timeout) {
184
184
  });
185
185
  }
186
186
  function yieldToEventLoop() {
187
- const scheduler = globalThis.scheduler;
188
- if (scheduler?.yield) return scheduler.yield();
189
187
  return new Promise((resolve) => {
190
188
  if (typeof MessageChannel !== "undefined") {
191
189
  const channel = new MessageChannel();
@@ -18092,6 +18090,7 @@ var AnvilWorldSource = class {
18092
18090
  };
18093
18091
 
18094
18092
  // src/world/Chunk.ts
18093
+ init_BlockObject();
18095
18094
  var import_three47 = require("three");
18096
18095
  init_BlockStates();
18097
18096
  init_AssetKey();
@@ -18102,6 +18101,7 @@ init_Model();
18102
18101
  init_util();
18103
18102
  init_AssetKey();
18104
18103
  init_BlockStates();
18104
+ init_BlockEntities();
18105
18105
  init_CubeFace();
18106
18106
 
18107
18107
  // src/world/SectionModels.ts
@@ -18199,6 +18199,10 @@ var SectionModels = class {
18199
18199
  biomeTint: source ? [...templates.values()].some((template) => template.biomeTint?.some((value) => value === 1)) || void 0 : void 0
18200
18200
  };
18201
18201
  }
18202
+ /** Starts loading the state's models and atlases so a later `prepareState` with the same properties finds them loaded. */
18203
+ async preload(blockState3, properties = {}) {
18204
+ await this.state(blockState3, properties);
18205
+ }
18202
18206
  async state(blockState3, properties) {
18203
18207
  let states = this.states.get(blockState3);
18204
18208
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
@@ -18221,22 +18225,24 @@ var SectionModels = class {
18221
18225
  async prepare(blockState3, properties) {
18222
18226
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
18223
18227
  const groups = BlockStateResolver.matching(blockState3, state);
18224
- if (!groups.length) return void 0;
18228
+ if (!groups.length || groups.some((group) => Array.isArray(group) && !group.length)) return void 0;
18229
+ const variants = groups.flatMap((group) => Array.isArray(group) ? group : [group]);
18230
+ if (variants.some((variant) => !variant.model)) return void 0;
18231
+ const settled = await Promise.allSettled(variants.map(async (variant) => {
18232
+ const modelKey = AssetKey.parse("models", variant.model);
18233
+ modelKey.root = blockState3.key?.root;
18234
+ const model = await Models.getMerged(modelKey);
18235
+ if (!model?.elements?.length) return void 0;
18236
+ const atlas = await UVMapper.getAtlas(model);
18237
+ const rotation = BlockStateResolver.rotation(variant);
18238
+ if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
18239
+ return { model, atlas, rotation };
18240
+ }));
18225
18241
  const prepared = /* @__PURE__ */ new Map();
18226
- for (const group of groups) {
18227
- const choices = Array.isArray(group) ? group : [group];
18228
- if (!choices.length) return void 0;
18229
- for (const variant of choices) {
18230
- if (!variant.model) return void 0;
18231
- const modelKey = AssetKey.parse("models", variant.model);
18232
- modelKey.root = blockState3.key?.root;
18233
- const model = await Models.getMerged(modelKey);
18234
- if (!model?.elements?.length) return void 0;
18235
- const atlas = await UVMapper.getAtlas(model);
18236
- const rotation = BlockStateResolver.rotation(variant);
18237
- if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
18238
- prepared.set(variant, { model, atlas, rotation });
18239
- }
18242
+ for (const [i, result] of settled.entries()) {
18243
+ if (result.status === "rejected") throw result.reason;
18244
+ if (!result.value) return void 0;
18245
+ prepared.set(variants[i], result.value);
18240
18246
  }
18241
18247
  return { state, groups, choices: prepared };
18242
18248
  }
@@ -18696,6 +18702,7 @@ function skipsBlockFace(data, rule, block, neighbor, face) {
18696
18702
  // src/world/MineRenderWorld.ts
18697
18703
  init_log();
18698
18704
  var log18 = logger("MineRenderWorld");
18705
+ var YIELD_INTERVAL_MS = 8;
18699
18706
  var MineRenderWorld = class {
18700
18707
  constructor(scene, options = {}) {
18701
18708
  this._chunks = /* @__PURE__ */ new Map();
@@ -18809,7 +18816,7 @@ var MineRenderWorld = class {
18809
18816
  failures.set(chunk, error);
18810
18817
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18811
18818
  }
18812
- if (performance.now() - sliceStart > 8) {
18819
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18813
18820
  await yieldToEventLoop();
18814
18821
  sliceStart = performance.now();
18815
18822
  }
@@ -18910,15 +18917,18 @@ var MineRenderWorld = class {
18910
18917
  const key = AssetKey.parse("blockstates", block.type);
18911
18918
  keys.set(key.serialize(), key);
18912
18919
  }
18913
- await BlockStates.getAll(keys.values());
18920
+ const loaded = await BlockStates.getAll(keys.values());
18921
+ const states = new Map([...keys.keys()].map((key, i) => [key, loaded[i]]));
18914
18922
  if (!useBatches) {
18915
18923
  for (const block of multiblock.blocks) await place(block);
18916
18924
  return;
18917
18925
  }
18918
18926
  if (!executor) {
18919
18927
  const groups2 = /* @__PURE__ */ new Map();
18928
+ const distinct = /* @__PURE__ */ new Map();
18920
18929
  const position = new import_three45.Vector3();
18921
18930
  for (const block of multiblock.blocks) {
18931
+ if (this.sectionModels && !Chunk.isAir(block)) distinct.set(ChunkData.paletteKey(block), block);
18922
18932
  const [x, y, z] = block.position;
18923
18933
  this.validatePosBounds(position.set(x, y, z));
18924
18934
  const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
@@ -18929,6 +18939,16 @@ var MineRenderWorld = class {
18929
18939
  if (!group) groups2.set(chunk, group = []);
18930
18940
  group.push({ index: (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16, block });
18931
18941
  }
18942
+ if (this.sectionModels) {
18943
+ for (const block of distinct.values()) {
18944
+ const state = states.get(AssetKey.parse("blockstates", block.type).serialize());
18945
+ if (!state) continue;
18946
+ void (async () => {
18947
+ if (state.key && BlockEntities.entry(await BlockEntities.getIndex(state.key.root), state.key.toNamespacedString())) return;
18948
+ await this.sectionModels.preload(state, block.properties);
18949
+ })().catch(() => void 0);
18950
+ }
18951
+ }
18932
18952
  await this.placeChunkGroups(groups2, changes);
18933
18953
  return;
18934
18954
  }
@@ -18964,7 +18984,7 @@ var MineRenderWorld = class {
18964
18984
  ));
18965
18985
  for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
18966
18986
  }
18967
- if (performance.now() - sliceStart > 8) {
18987
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18968
18988
  await yieldToEventLoop();
18969
18989
  sliceStart = performance.now();
18970
18990
  }
@@ -18972,6 +18992,62 @@ var MineRenderWorld = class {
18972
18992
  }
18973
18993
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
18974
18994
  async placeChunk(chunk, executor) {
18995
+ return this.placeColumn(chunk, executor);
18996
+ }
18997
+ /**
18998
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
18999
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
19000
+ */
19001
+ async placeChunks(chunks) {
19002
+ const columns = [];
19003
+ for (const chunk of chunks) {
19004
+ const { x, z } = chunk ?? {};
19005
+ if (![x, z].every(Number.isSafeInteger)) {
19006
+ throw new RangeError("Chunk column coordinates must be safe integers");
19007
+ }
19008
+ const sections = Array.from(chunk.sections, (section) => {
19009
+ const { y } = section ?? {};
19010
+ if (!Number.isSafeInteger(y)) throw new RangeError("Chunk section coordinates must be safe integers");
19011
+ return { ...section, y };
19012
+ });
19013
+ columns.push({ ...chunk, x, z, sections });
19014
+ }
19015
+ if (!columns.length) return;
19016
+ await this.culling;
19017
+ let failed = false, failure, sliceStart = performance.now();
19018
+ try {
19019
+ for (const column of columns) {
19020
+ await this.placeColumn(column, void 0, true);
19021
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19022
+ await yieldToEventLoop();
19023
+ sliceStart = performance.now();
19024
+ }
19025
+ }
19026
+ } catch (error) {
19027
+ failed = true;
19028
+ failure = error;
19029
+ } finally {
19030
+ try {
19031
+ const failures = await this.refreshBiomes(true);
19032
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
19033
+ } catch (error) {
19034
+ if (!failed) {
19035
+ failed = true;
19036
+ failure = error;
19037
+ }
19038
+ }
19039
+ try {
19040
+ await this.updateCulling([]);
19041
+ } catch (error) {
19042
+ if (!failed) {
19043
+ failed = true;
19044
+ failure = error;
19045
+ }
19046
+ }
19047
+ }
19048
+ if (failed) throw failure;
19049
+ }
19050
+ async placeColumn(chunk, executor, deferCulling = false) {
18975
19051
  if (![chunk.x, chunk.z].every(Number.isSafeInteger)) {
18976
19052
  throw new RangeError("Chunk column coordinates must be safe integers");
18977
19053
  }
@@ -19033,7 +19109,7 @@ var MineRenderWorld = class {
19033
19109
  } finally {
19034
19110
  this.markSectionChanged(target);
19035
19111
  }
19036
- if (performance.now() - sliceStart > 8) {
19112
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19037
19113
  await yieldToEventLoop();
19038
19114
  sliceStart = performance.now();
19039
19115
  }
@@ -19056,9 +19132,12 @@ var MineRenderWorld = class {
19056
19132
  await placeEntities?.();
19057
19133
  } finally {
19058
19134
  this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
19059
- const failures = await this.refreshBiomes();
19060
- if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
19061
- await this.updateCulling([...changes.values()]);
19135
+ if (deferCulling) await this.updateCulling([...changes.values()], true);
19136
+ else {
19137
+ const failures = await this.refreshBiomes();
19138
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
19139
+ await this.updateCulling([...changes.values()]);
19140
+ }
19062
19141
  }
19063
19142
  }
19064
19143
  /**
@@ -19088,7 +19167,7 @@ var MineRenderWorld = class {
19088
19167
  for (const { x, z } of columns.values()) {
19089
19168
  this.entities?.clearColumn(x, z);
19090
19169
  await this.clearChunkColumn(x, z);
19091
- if (performance.now() - sliceStart > 8) {
19170
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19092
19171
  await yieldToEventLoop();
19093
19172
  sliceStart = performance.now();
19094
19173
  }
@@ -19227,15 +19306,25 @@ var MineRenderWorld = class {
19227
19306
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19228
19307
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
19229
19308
  chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19230
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19309
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19231
19310
  await yieldToEventLoop();
19232
19311
  sliceStart = performance.now();
19233
19312
  }
19234
19313
  }
19235
- const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19236
- await chunk.rebuildSectionMesh();
19237
- batch.delete(chunk);
19238
- }));
19314
+ const pending = [];
19315
+ for (const chunk of [...batch.keys()]) {
19316
+ const build = (async () => {
19317
+ await chunk.rebuildSectionMesh();
19318
+ batch.delete(chunk);
19319
+ })();
19320
+ void build.catch(() => void 0);
19321
+ pending.push(build);
19322
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19323
+ await yieldToEventLoop();
19324
+ sliceStart = performance.now();
19325
+ }
19326
+ }
19327
+ const builds = await Promise.allSettled(pending);
19239
19328
  const failure = builds.find((result) => result.status === "rejected");
19240
19329
  if (failure?.status === "rejected") throw failure.reason;
19241
19330
  } catch (error) {
@@ -20187,8 +20276,10 @@ var Chunk = class _Chunk {
20187
20276
  if (rejected?.status === "rejected") throw rejected.reason;
20188
20277
  }
20189
20278
  const positions = [];
20190
- let failed = false, failure;
20191
- for (const { index, key, block } of placements) {
20279
+ const jobs = /* @__PURE__ */ new Map();
20280
+ let failureOrder = Infinity, failure;
20281
+ for (const [order, { index, key, block }] of placements.entries()) {
20282
+ jobs.delete(index);
20192
20283
  this.hiddenBlocks.delete(index);
20193
20284
  this.data.assign(index, this.data.intern(block), block?.nbt);
20194
20285
  const readBlock = this.data.snapshot(index);
@@ -20204,7 +20295,6 @@ var Chunk = class _Chunk {
20204
20295
  this.z * 16 + Math.floor(index / 16) % 16
20205
20296
  );
20206
20297
  positions.push(worldPos);
20207
- let object4;
20208
20298
  try {
20209
20299
  if (!readBlock) continue;
20210
20300
  const resolved = resolutions.get(key);
@@ -20226,7 +20316,7 @@ var Chunk = class _Chunk {
20226
20316
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
20227
20317
  this.meshDirty = true;
20228
20318
  } else {
20229
- object4 = await this.scene.addBlock(blockState3, {
20319
+ jobs.set(index, { order, readBlock, blockState: blockState3, position: MineRenderWorld.worldToScenePosition(worldPos), options: {
20230
20320
  mergeMeshes: true,
20231
20321
  instanceMeshes: true,
20232
20322
  maxInstanceCount: 2e3,
@@ -20234,13 +20324,12 @@ var Chunk = class _Chunk {
20234
20324
  variantPosition,
20235
20325
  biome: context?.biome,
20236
20326
  biomePosition: this.biomePosition(index, blockState3, stored)
20237
- });
20238
- object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
20327
+ } });
20328
+ continue;
20239
20329
  }
20240
20330
  this.renderedBlocks.set(index, { get block() {
20241
20331
  return readBlock();
20242
- }, object: object4 });
20243
- if (object4) this.objectBlocks.add(index);
20332
+ }, object: void 0 });
20244
20333
  } catch (error) {
20245
20334
  if (preserveDataOnFailure && readBlock) {
20246
20335
  this.renderedBlocks.set(index, { get block() {
@@ -20248,12 +20337,35 @@ var Chunk = class _Chunk {
20248
20337
  }, object: void 0 });
20249
20338
  } else this.data.assign(index, 0);
20250
20339
  this.fluidCells[index] = 0;
20251
- object4?.removeFromScene();
20252
- if (!failed) failure = error;
20253
- failed = true;
20340
+ if (order < failureOrder) {
20341
+ failureOrder = order;
20342
+ failure = error;
20343
+ }
20254
20344
  }
20255
20345
  }
20256
- if (failed) throw failure;
20346
+ const pending = [...jobs];
20347
+ const objects = await Promise.allSettled(pending.map(([, job]) => this.createBlockObject(job.blockState, job.position, job.options)));
20348
+ for (const [i, [index, { order, readBlock }]] of pending.entries()) {
20349
+ const result = objects[i];
20350
+ if (result.status === "fulfilled") {
20351
+ this.renderedBlocks.set(index, { get block() {
20352
+ return readBlock();
20353
+ }, object: result.value });
20354
+ this.objectBlocks.add(index);
20355
+ } else {
20356
+ if (preserveDataOnFailure) {
20357
+ this.renderedBlocks.set(index, { get block() {
20358
+ return readBlock();
20359
+ }, object: void 0 });
20360
+ } else this.data.assign(index, 0);
20361
+ this.fluidCells[index] = 0;
20362
+ if (order < failureOrder) {
20363
+ failureOrder = order;
20364
+ failure = result.reason;
20365
+ }
20366
+ }
20367
+ }
20368
+ if (failureOrder < Infinity) throw failure;
20257
20369
  return positions;
20258
20370
  }
20259
20371
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
@@ -20324,37 +20436,42 @@ var Chunk = class _Chunk {
20324
20436
  failed = true;
20325
20437
  }
20326
20438
  }
20327
- for (const { index, blockState: blockState3, biome } of perBlocks) {
20328
- let object4;
20329
- try {
20330
- object4 = await this.scene.addBlock(blockState3, {
20331
- mergeMeshes: true,
20332
- instanceMeshes: true,
20333
- maxInstanceCount: 2e3,
20334
- initialState: this.data.stateAt(this.data.idAt(index)).properties,
20335
- variantPosition: this.positionAt(index),
20336
- biome,
20337
- biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
20338
- });
20339
- object4.setPosition(new import_three47.Vector3(
20340
- this.x * 256 + index % 16 * 16,
20341
- this.y * 256 + Math.floor(index / 256) * 16,
20342
- this.z * 256 + Math.floor(index / 16) % 16 * 16
20343
- ));
20344
- this.renderedBlocks.get(index).object = object4;
20439
+ const objects = await Promise.allSettled(perBlocks.map(({ index, blockState: blockState3, biome }) => this.createBlockObject(blockState3, new import_three47.Vector3(
20440
+ this.x * 256 + index % 16 * 16,
20441
+ this.y * 256 + Math.floor(index / 256) * 16,
20442
+ this.z * 256 + Math.floor(index / 16) % 16 * 16
20443
+ ), {
20444
+ mergeMeshes: true,
20445
+ instanceMeshes: true,
20446
+ maxInstanceCount: 2e3,
20447
+ initialState: this.data.stateAt(this.data.idAt(index)).properties,
20448
+ variantPosition: this.positionAt(index),
20449
+ biome,
20450
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
20451
+ })));
20452
+ for (const [i, { index }] of perBlocks.entries()) {
20453
+ const result = objects[i];
20454
+ if (result.status === "fulfilled") {
20455
+ this.renderedBlocks.get(index).object = result.value;
20345
20456
  this.objectBlocks.add(index);
20346
- } catch (error) {
20457
+ } else {
20347
20458
  this.data.assign(index, 0);
20348
20459
  this.fluidCells[index] = 0;
20349
20460
  this.renderedBlocks.delete(index);
20350
20461
  this.objectBlocks.delete(index);
20351
- object4?.removeFromScene();
20352
- if (!failed) failure = error;
20462
+ if (!failed) failure = result.reason;
20353
20463
  failed = true;
20354
20464
  }
20355
20465
  }
20356
20466
  if (failed) throw failure;
20357
20467
  }
20468
+ createBlockObject(blockState3, position, options) {
20469
+ return this.scene.addSceneObject(blockState3, () => {
20470
+ const object4 = new BlockObject(blockState3, options);
20471
+ object4.position.copy(position);
20472
+ return object4;
20473
+ }, options);
20474
+ }
20358
20475
  positionAt(index) {
20359
20476
  return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
20360
20477
  }
@@ -20695,6 +20812,7 @@ var Chunk = class _Chunk {
20695
20812
  };
20696
20813
 
20697
20814
  // src/world/WorldStreamer.ts
20815
+ var MAX_PREFETCHED_COLUMNS = 4;
20698
20816
  var WorldStreamer = class {
20699
20817
  constructor(world, source, options = {}) {
20700
20818
  this.world = world;
@@ -20704,6 +20822,7 @@ var WorldStreamer = class {
20704
20822
  this.missing = /* @__PURE__ */ new Map();
20705
20823
  this.failed = /* @__PURE__ */ new Map();
20706
20824
  this.desired = [];
20825
+ this.reads = /* @__PURE__ */ new Map();
20707
20826
  this.disposed = false;
20708
20827
  this.loadRadius = options.loadRadius ?? 2;
20709
20828
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -20748,6 +20867,12 @@ var WorldStreamer = class {
20748
20867
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
20749
20868
  this.loading.controller.abort();
20750
20869
  }
20870
+ for (const [key, read] of this.reads) {
20871
+ if (!this.within(read.position, this.loadRadius)) {
20872
+ read.controller.abort();
20873
+ if (read.ready) this.reads.delete(key);
20874
+ }
20875
+ }
20751
20876
  }
20752
20877
  return this.running ??= Promise.resolve().then(() => this.drain());
20753
20878
  }
@@ -20768,8 +20893,11 @@ var WorldStreamer = class {
20768
20893
  this.desired = [];
20769
20894
  this.center = void 0;
20770
20895
  this.loading?.controller.abort();
20896
+ for (const read of this.reads.values()) read.controller.abort();
20771
20897
  return this.disposal ??= Promise.resolve().then(async () => {
20772
20898
  await this.running?.catch(() => void 0);
20899
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20900
+ this.reads.clear();
20773
20901
  try {
20774
20902
  await this.unloadMany([...this.owned.values()]);
20775
20903
  } finally {
@@ -20795,6 +20923,10 @@ var WorldStreamer = class {
20795
20923
  await this.unloadMany(obsolete);
20796
20924
  continue;
20797
20925
  }
20926
+ if (this.world.placeChunks) {
20927
+ if (await this.placeReadyChunks()) continue;
20928
+ return;
20929
+ }
20798
20930
  const position = this.desired.find((position2) => this.needsLoad(position2));
20799
20931
  if (!position) return;
20800
20932
  let chunk;
@@ -20817,26 +20949,105 @@ var WorldStreamer = class {
20817
20949
  continue;
20818
20950
  }
20819
20951
  this.owned.set(this.key(position), position);
20952
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
20953
+ }
20954
+ } finally {
20955
+ for (const [key, read] of this.reads) {
20956
+ read.controller.abort();
20957
+ if (read.ready) this.reads.delete(key);
20958
+ }
20959
+ this.running = void 0;
20960
+ }
20961
+ }
20962
+ fillReadSlots() {
20963
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
20964
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
20965
+ if (!position) return;
20966
+ const key = this.key(position);
20967
+ const read = {
20968
+ position,
20969
+ controller: new AbortController(),
20970
+ promise: Promise.resolve(),
20971
+ ready: false,
20972
+ failed: false
20973
+ };
20974
+ this.reads.set(key, read);
20975
+ read.promise = (async () => {
20820
20976
  try {
20821
- await this.world.placeChunk(chunk);
20822
- this.loaded.set(this.key(position), position);
20977
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
20978
+ if (read.chunk) this.validateChunk(read.chunk, position);
20823
20979
  } catch (error) {
20824
- try {
20825
- await this.unloadMany([position]);
20826
- } catch (cleanupError) {
20827
- const failure = error instanceof Error ? error : new Error(String(error));
20828
- try {
20829
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20830
- } catch {
20831
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
20832
- }
20833
- throw failure;
20980
+ read.failed = true;
20981
+ read.error = error;
20982
+ } finally {
20983
+ read.ready = true;
20984
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
20985
+ this.reads.delete(key);
20834
20986
  }
20835
- throw error;
20987
+ }
20988
+ })();
20989
+ }
20990
+ }
20991
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
20992
+ async placeReadyChunks() {
20993
+ this.fillReadSlots();
20994
+ const position = this.desired.find((position2) => this.needsLoad(position2));
20995
+ if (!position) {
20996
+ if (!this.reads.size) return false;
20997
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20998
+ return true;
20999
+ }
21000
+ const head = this.reads.get(this.key(position));
21001
+ if (!head) {
21002
+ for (let index = this.desired.length - 1; index >= 0; index--) {
21003
+ const key = this.key(this.desired[index]);
21004
+ if (this.reads.get(key)?.ready) {
21005
+ this.reads.delete(key);
21006
+ return true;
20836
21007
  }
20837
21008
  }
20838
- } finally {
20839
- this.running = void 0;
21009
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
21010
+ return true;
21011
+ }
21012
+ if (!head.ready) {
21013
+ await head.promise;
21014
+ return true;
21015
+ }
21016
+ const chunks = [], positions = [];
21017
+ for (const desired of this.desired) {
21018
+ if (!this.needsLoad(desired)) continue;
21019
+ const key = this.key(desired), read = this.reads.get(key);
21020
+ if (!read?.ready) break;
21021
+ this.reads.delete(key);
21022
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
21023
+ else if (!read.chunk) this.missing.set(key, desired);
21024
+ else {
21025
+ chunks.push(read.chunk);
21026
+ positions.push(desired);
21027
+ }
21028
+ }
21029
+ if (!chunks.length) return true;
21030
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
21031
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
21032
+ return true;
21033
+ }
21034
+ async placeOwnedColumns(positions, place) {
21035
+ try {
21036
+ await place();
21037
+ for (const position of positions) this.loaded.set(this.key(position), position);
21038
+ } catch (error) {
21039
+ try {
21040
+ await this.unloadMany(positions);
21041
+ } catch (cleanupError) {
21042
+ const failure = error instanceof Error ? error : new Error(String(error));
21043
+ try {
21044
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
21045
+ } catch {
21046
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
21047
+ }
21048
+ throw failure;
21049
+ }
21050
+ throw error;
20840
21051
  }
20841
21052
  }
20842
21053
  needsLoad(position) {