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();
@@ -17924,6 +17922,7 @@ var AnvilWorldSource = class {
17924
17922
  };
17925
17923
 
17926
17924
  // src/world/Chunk.ts
17925
+ init_BlockObject();
17927
17926
  import { Color as Color13, Vector3 as Vector321 } from "three";
17928
17927
  init_BlockStates();
17929
17928
  init_AssetKey();
@@ -17934,6 +17933,7 @@ init_util();
17934
17933
  import { Vector3 as Vector319 } from "three";
17935
17934
  init_AssetKey();
17936
17935
  init_BlockStates();
17936
+ init_BlockEntities();
17937
17937
  init_CubeFace();
17938
17938
 
17939
17939
  // src/world/SectionModels.ts
@@ -18031,6 +18031,10 @@ var SectionModels = class {
18031
18031
  biomeTint: source ? [...templates.values()].some((template) => template.biomeTint?.some((value) => value === 1)) || void 0 : void 0
18032
18032
  };
18033
18033
  }
18034
+ /** Starts loading the state's models and atlases so a later `prepareState` with the same properties finds them loaded. */
18035
+ async preload(blockState3, properties = {}) {
18036
+ await this.state(blockState3, properties);
18037
+ }
18034
18038
  async state(blockState3, properties) {
18035
18039
  let states = this.states.get(blockState3);
18036
18040
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
@@ -18053,22 +18057,24 @@ var SectionModels = class {
18053
18057
  async prepare(blockState3, properties) {
18054
18058
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
18055
18059
  const groups = BlockStateResolver.matching(blockState3, state);
18056
- if (!groups.length) return void 0;
18060
+ if (!groups.length || groups.some((group) => Array.isArray(group) && !group.length)) return void 0;
18061
+ const variants = groups.flatMap((group) => Array.isArray(group) ? group : [group]);
18062
+ if (variants.some((variant) => !variant.model)) return void 0;
18063
+ const settled = await Promise.allSettled(variants.map(async (variant) => {
18064
+ const modelKey = AssetKey.parse("models", variant.model);
18065
+ modelKey.root = blockState3.key?.root;
18066
+ const model = await Models.getMerged(modelKey);
18067
+ if (!model?.elements?.length) return void 0;
18068
+ const atlas = await UVMapper.getAtlas(model);
18069
+ const rotation = BlockStateResolver.rotation(variant);
18070
+ if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
18071
+ return { model, atlas, rotation };
18072
+ }));
18057
18073
  const prepared = /* @__PURE__ */ new Map();
18058
- for (const group of groups) {
18059
- const choices = Array.isArray(group) ? group : [group];
18060
- if (!choices.length) return void 0;
18061
- for (const variant of choices) {
18062
- if (!variant.model) return void 0;
18063
- const modelKey = AssetKey.parse("models", variant.model);
18064
- modelKey.root = blockState3.key?.root;
18065
- const model = await Models.getMerged(modelKey);
18066
- if (!model?.elements?.length) return void 0;
18067
- const atlas = await UVMapper.getAtlas(model);
18068
- const rotation = BlockStateResolver.rotation(variant);
18069
- if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
18070
- prepared.set(variant, { model, atlas, rotation });
18071
- }
18074
+ for (const [i, result] of settled.entries()) {
18075
+ if (result.status === "rejected") throw result.reason;
18076
+ if (!result.value) return void 0;
18077
+ prepared.set(variants[i], result.value);
18072
18078
  }
18073
18079
  return { state, groups, choices: prepared };
18074
18080
  }
@@ -18528,6 +18534,7 @@ function skipsBlockFace(data, rule, block, neighbor, face) {
18528
18534
  // src/world/MineRenderWorld.ts
18529
18535
  init_log();
18530
18536
  var log18 = logger("MineRenderWorld");
18537
+ var YIELD_INTERVAL_MS = 8;
18531
18538
  var MineRenderWorld = class {
18532
18539
  constructor(scene, options = {}) {
18533
18540
  this._chunks = /* @__PURE__ */ new Map();
@@ -18641,7 +18648,7 @@ var MineRenderWorld = class {
18641
18648
  failures.set(chunk, error);
18642
18649
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18643
18650
  }
18644
- if (performance.now() - sliceStart > 8) {
18651
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18645
18652
  await yieldToEventLoop();
18646
18653
  sliceStart = performance.now();
18647
18654
  }
@@ -18742,15 +18749,18 @@ var MineRenderWorld = class {
18742
18749
  const key = AssetKey.parse("blockstates", block.type);
18743
18750
  keys.set(key.serialize(), key);
18744
18751
  }
18745
- await BlockStates.getAll(keys.values());
18752
+ const loaded = await BlockStates.getAll(keys.values());
18753
+ const states = new Map([...keys.keys()].map((key, i) => [key, loaded[i]]));
18746
18754
  if (!useBatches) {
18747
18755
  for (const block of multiblock.blocks) await place(block);
18748
18756
  return;
18749
18757
  }
18750
18758
  if (!executor) {
18751
18759
  const groups2 = /* @__PURE__ */ new Map();
18760
+ const distinct = /* @__PURE__ */ new Map();
18752
18761
  const position = new Vector319();
18753
18762
  for (const block of multiblock.blocks) {
18763
+ if (this.sectionModels && !Chunk.isAir(block)) distinct.set(ChunkData.paletteKey(block), block);
18754
18764
  const [x, y, z] = block.position;
18755
18765
  this.validatePosBounds(position.set(x, y, z));
18756
18766
  const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
@@ -18761,6 +18771,16 @@ var MineRenderWorld = class {
18761
18771
  if (!group) groups2.set(chunk, group = []);
18762
18772
  group.push({ index: (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16, block });
18763
18773
  }
18774
+ if (this.sectionModels) {
18775
+ for (const block of distinct.values()) {
18776
+ const state = states.get(AssetKey.parse("blockstates", block.type).serialize());
18777
+ if (!state) continue;
18778
+ void (async () => {
18779
+ if (state.key && BlockEntities.entry(await BlockEntities.getIndex(state.key.root), state.key.toNamespacedString())) return;
18780
+ await this.sectionModels.preload(state, block.properties);
18781
+ })().catch(() => void 0);
18782
+ }
18783
+ }
18764
18784
  await this.placeChunkGroups(groups2, changes);
18765
18785
  return;
18766
18786
  }
@@ -18796,7 +18816,7 @@ var MineRenderWorld = class {
18796
18816
  ));
18797
18817
  for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
18798
18818
  }
18799
- if (performance.now() - sliceStart > 8) {
18819
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18800
18820
  await yieldToEventLoop();
18801
18821
  sliceStart = performance.now();
18802
18822
  }
@@ -18804,6 +18824,62 @@ var MineRenderWorld = class {
18804
18824
  }
18805
18825
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
18806
18826
  async placeChunk(chunk, executor) {
18827
+ return this.placeColumn(chunk, executor);
18828
+ }
18829
+ /**
18830
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
18831
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
18832
+ */
18833
+ async placeChunks(chunks) {
18834
+ const columns = [];
18835
+ for (const chunk of chunks) {
18836
+ const { x, z } = chunk ?? {};
18837
+ if (![x, z].every(Number.isSafeInteger)) {
18838
+ throw new RangeError("Chunk column coordinates must be safe integers");
18839
+ }
18840
+ const sections = Array.from(chunk.sections, (section) => {
18841
+ const { y } = section ?? {};
18842
+ if (!Number.isSafeInteger(y)) throw new RangeError("Chunk section coordinates must be safe integers");
18843
+ return { ...section, y };
18844
+ });
18845
+ columns.push({ ...chunk, x, z, sections });
18846
+ }
18847
+ if (!columns.length) return;
18848
+ await this.culling;
18849
+ let failed = false, failure, sliceStart = performance.now();
18850
+ try {
18851
+ for (const column of columns) {
18852
+ await this.placeColumn(column, void 0, true);
18853
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18854
+ await yieldToEventLoop();
18855
+ sliceStart = performance.now();
18856
+ }
18857
+ }
18858
+ } catch (error) {
18859
+ failed = true;
18860
+ failure = error;
18861
+ } finally {
18862
+ try {
18863
+ const failures = await this.refreshBiomes(true);
18864
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18865
+ } catch (error) {
18866
+ if (!failed) {
18867
+ failed = true;
18868
+ failure = error;
18869
+ }
18870
+ }
18871
+ try {
18872
+ await this.updateCulling([]);
18873
+ } catch (error) {
18874
+ if (!failed) {
18875
+ failed = true;
18876
+ failure = error;
18877
+ }
18878
+ }
18879
+ }
18880
+ if (failed) throw failure;
18881
+ }
18882
+ async placeColumn(chunk, executor, deferCulling = false) {
18807
18883
  if (![chunk.x, chunk.z].every(Number.isSafeInteger)) {
18808
18884
  throw new RangeError("Chunk column coordinates must be safe integers");
18809
18885
  }
@@ -18865,7 +18941,7 @@ var MineRenderWorld = class {
18865
18941
  } finally {
18866
18942
  this.markSectionChanged(target);
18867
18943
  }
18868
- if (performance.now() - sliceStart > 8) {
18944
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18869
18945
  await yieldToEventLoop();
18870
18946
  sliceStart = performance.now();
18871
18947
  }
@@ -18888,9 +18964,12 @@ var MineRenderWorld = class {
18888
18964
  await placeEntities?.();
18889
18965
  } finally {
18890
18966
  this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
18891
- const failures = await this.refreshBiomes();
18892
- if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18893
- await this.updateCulling([...changes.values()]);
18967
+ if (deferCulling) await this.updateCulling([...changes.values()], true);
18968
+ else {
18969
+ const failures = await this.refreshBiomes();
18970
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18971
+ await this.updateCulling([...changes.values()]);
18972
+ }
18894
18973
  }
18895
18974
  }
18896
18975
  /**
@@ -18920,7 +18999,7 @@ var MineRenderWorld = class {
18920
18999
  for (const { x, z } of columns.values()) {
18921
19000
  this.entities?.clearColumn(x, z);
18922
19001
  await this.clearChunkColumn(x, z);
18923
- if (performance.now() - sliceStart > 8) {
19002
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18924
19003
  await yieldToEventLoop();
18925
19004
  sliceStart = performance.now();
18926
19005
  }
@@ -19059,15 +19138,25 @@ var MineRenderWorld = class {
19059
19138
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19060
19139
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
19061
19140
  chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19062
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19141
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19063
19142
  await yieldToEventLoop();
19064
19143
  sliceStart = performance.now();
19065
19144
  }
19066
19145
  }
19067
- const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19068
- await chunk.rebuildSectionMesh();
19069
- batch.delete(chunk);
19070
- }));
19146
+ const pending = [];
19147
+ for (const chunk of [...batch.keys()]) {
19148
+ const build = (async () => {
19149
+ await chunk.rebuildSectionMesh();
19150
+ batch.delete(chunk);
19151
+ })();
19152
+ void build.catch(() => void 0);
19153
+ pending.push(build);
19154
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19155
+ await yieldToEventLoop();
19156
+ sliceStart = performance.now();
19157
+ }
19158
+ }
19159
+ const builds = await Promise.allSettled(pending);
19071
19160
  const failure = builds.find((result) => result.status === "rejected");
19072
19161
  if (failure?.status === "rejected") throw failure.reason;
19073
19162
  } catch (error) {
@@ -20019,8 +20108,10 @@ var Chunk = class _Chunk {
20019
20108
  if (rejected?.status === "rejected") throw rejected.reason;
20020
20109
  }
20021
20110
  const positions = [];
20022
- let failed = false, failure;
20023
- for (const { index, key, block } of placements) {
20111
+ const jobs = /* @__PURE__ */ new Map();
20112
+ let failureOrder = Infinity, failure;
20113
+ for (const [order, { index, key, block }] of placements.entries()) {
20114
+ jobs.delete(index);
20024
20115
  this.hiddenBlocks.delete(index);
20025
20116
  this.data.assign(index, this.data.intern(block), block?.nbt);
20026
20117
  const readBlock = this.data.snapshot(index);
@@ -20036,7 +20127,6 @@ var Chunk = class _Chunk {
20036
20127
  this.z * 16 + Math.floor(index / 16) % 16
20037
20128
  );
20038
20129
  positions.push(worldPos);
20039
- let object4;
20040
20130
  try {
20041
20131
  if (!readBlock) continue;
20042
20132
  const resolved = resolutions.get(key);
@@ -20058,7 +20148,7 @@ var Chunk = class _Chunk {
20058
20148
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
20059
20149
  this.meshDirty = true;
20060
20150
  } else {
20061
- object4 = await this.scene.addBlock(blockState3, {
20151
+ jobs.set(index, { order, readBlock, blockState: blockState3, position: MineRenderWorld.worldToScenePosition(worldPos), options: {
20062
20152
  mergeMeshes: true,
20063
20153
  instanceMeshes: true,
20064
20154
  maxInstanceCount: 2e3,
@@ -20066,13 +20156,12 @@ var Chunk = class _Chunk {
20066
20156
  variantPosition,
20067
20157
  biome: context?.biome,
20068
20158
  biomePosition: this.biomePosition(index, blockState3, stored)
20069
- });
20070
- object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
20159
+ } });
20160
+ continue;
20071
20161
  }
20072
20162
  this.renderedBlocks.set(index, { get block() {
20073
20163
  return readBlock();
20074
- }, object: object4 });
20075
- if (object4) this.objectBlocks.add(index);
20164
+ }, object: void 0 });
20076
20165
  } catch (error) {
20077
20166
  if (preserveDataOnFailure && readBlock) {
20078
20167
  this.renderedBlocks.set(index, { get block() {
@@ -20080,12 +20169,35 @@ var Chunk = class _Chunk {
20080
20169
  }, object: void 0 });
20081
20170
  } else this.data.assign(index, 0);
20082
20171
  this.fluidCells[index] = 0;
20083
- object4?.removeFromScene();
20084
- if (!failed) failure = error;
20085
- failed = true;
20172
+ if (order < failureOrder) {
20173
+ failureOrder = order;
20174
+ failure = error;
20175
+ }
20086
20176
  }
20087
20177
  }
20088
- if (failed) throw failure;
20178
+ const pending = [...jobs];
20179
+ const objects = await Promise.allSettled(pending.map(([, job]) => this.createBlockObject(job.blockState, job.position, job.options)));
20180
+ for (const [i, [index, { order, readBlock }]] of pending.entries()) {
20181
+ const result = objects[i];
20182
+ if (result.status === "fulfilled") {
20183
+ this.renderedBlocks.set(index, { get block() {
20184
+ return readBlock();
20185
+ }, object: result.value });
20186
+ this.objectBlocks.add(index);
20187
+ } else {
20188
+ if (preserveDataOnFailure) {
20189
+ this.renderedBlocks.set(index, { get block() {
20190
+ return readBlock();
20191
+ }, object: void 0 });
20192
+ } else this.data.assign(index, 0);
20193
+ this.fluidCells[index] = 0;
20194
+ if (order < failureOrder) {
20195
+ failureOrder = order;
20196
+ failure = result.reason;
20197
+ }
20198
+ }
20199
+ }
20200
+ if (failureOrder < Infinity) throw failure;
20089
20201
  return positions;
20090
20202
  }
20091
20203
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
@@ -20156,37 +20268,42 @@ var Chunk = class _Chunk {
20156
20268
  failed = true;
20157
20269
  }
20158
20270
  }
20159
- for (const { index, blockState: blockState3, biome } of perBlocks) {
20160
- let object4;
20161
- try {
20162
- object4 = await this.scene.addBlock(blockState3, {
20163
- mergeMeshes: true,
20164
- instanceMeshes: true,
20165
- maxInstanceCount: 2e3,
20166
- initialState: this.data.stateAt(this.data.idAt(index)).properties,
20167
- variantPosition: this.positionAt(index),
20168
- biome,
20169
- biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
20170
- });
20171
- object4.setPosition(new Vector321(
20172
- this.x * 256 + index % 16 * 16,
20173
- this.y * 256 + Math.floor(index / 256) * 16,
20174
- this.z * 256 + Math.floor(index / 16) % 16 * 16
20175
- ));
20176
- this.renderedBlocks.get(index).object = object4;
20271
+ const objects = await Promise.allSettled(perBlocks.map(({ index, blockState: blockState3, biome }) => this.createBlockObject(blockState3, new Vector321(
20272
+ this.x * 256 + index % 16 * 16,
20273
+ this.y * 256 + Math.floor(index / 256) * 16,
20274
+ this.z * 256 + Math.floor(index / 16) % 16 * 16
20275
+ ), {
20276
+ mergeMeshes: true,
20277
+ instanceMeshes: true,
20278
+ maxInstanceCount: 2e3,
20279
+ initialState: this.data.stateAt(this.data.idAt(index)).properties,
20280
+ variantPosition: this.positionAt(index),
20281
+ biome,
20282
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
20283
+ })));
20284
+ for (const [i, { index }] of perBlocks.entries()) {
20285
+ const result = objects[i];
20286
+ if (result.status === "fulfilled") {
20287
+ this.renderedBlocks.get(index).object = result.value;
20177
20288
  this.objectBlocks.add(index);
20178
- } catch (error) {
20289
+ } else {
20179
20290
  this.data.assign(index, 0);
20180
20291
  this.fluidCells[index] = 0;
20181
20292
  this.renderedBlocks.delete(index);
20182
20293
  this.objectBlocks.delete(index);
20183
- object4?.removeFromScene();
20184
- if (!failed) failure = error;
20294
+ if (!failed) failure = result.reason;
20185
20295
  failed = true;
20186
20296
  }
20187
20297
  }
20188
20298
  if (failed) throw failure;
20189
20299
  }
20300
+ createBlockObject(blockState3, position, options) {
20301
+ return this.scene.addSceneObject(blockState3, () => {
20302
+ const object4 = new BlockObject(blockState3, options);
20303
+ object4.position.copy(position);
20304
+ return object4;
20305
+ }, options);
20306
+ }
20190
20307
  positionAt(index) {
20191
20308
  return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
20192
20309
  }
@@ -20527,6 +20644,7 @@ var Chunk = class _Chunk {
20527
20644
  };
20528
20645
 
20529
20646
  // src/world/WorldStreamer.ts
20647
+ var MAX_PREFETCHED_COLUMNS = 4;
20530
20648
  var WorldStreamer = class {
20531
20649
  constructor(world, source, options = {}) {
20532
20650
  this.world = world;
@@ -20536,6 +20654,7 @@ var WorldStreamer = class {
20536
20654
  this.missing = /* @__PURE__ */ new Map();
20537
20655
  this.failed = /* @__PURE__ */ new Map();
20538
20656
  this.desired = [];
20657
+ this.reads = /* @__PURE__ */ new Map();
20539
20658
  this.disposed = false;
20540
20659
  this.loadRadius = options.loadRadius ?? 2;
20541
20660
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -20580,6 +20699,12 @@ var WorldStreamer = class {
20580
20699
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
20581
20700
  this.loading.controller.abort();
20582
20701
  }
20702
+ for (const [key, read] of this.reads) {
20703
+ if (!this.within(read.position, this.loadRadius)) {
20704
+ read.controller.abort();
20705
+ if (read.ready) this.reads.delete(key);
20706
+ }
20707
+ }
20583
20708
  }
20584
20709
  return this.running ??= Promise.resolve().then(() => this.drain());
20585
20710
  }
@@ -20600,8 +20725,11 @@ var WorldStreamer = class {
20600
20725
  this.desired = [];
20601
20726
  this.center = void 0;
20602
20727
  this.loading?.controller.abort();
20728
+ for (const read of this.reads.values()) read.controller.abort();
20603
20729
  return this.disposal ??= Promise.resolve().then(async () => {
20604
20730
  await this.running?.catch(() => void 0);
20731
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20732
+ this.reads.clear();
20605
20733
  try {
20606
20734
  await this.unloadMany([...this.owned.values()]);
20607
20735
  } finally {
@@ -20627,6 +20755,10 @@ var WorldStreamer = class {
20627
20755
  await this.unloadMany(obsolete);
20628
20756
  continue;
20629
20757
  }
20758
+ if (this.world.placeChunks) {
20759
+ if (await this.placeReadyChunks()) continue;
20760
+ return;
20761
+ }
20630
20762
  const position = this.desired.find((position2) => this.needsLoad(position2));
20631
20763
  if (!position) return;
20632
20764
  let chunk;
@@ -20649,26 +20781,105 @@ var WorldStreamer = class {
20649
20781
  continue;
20650
20782
  }
20651
20783
  this.owned.set(this.key(position), position);
20784
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
20785
+ }
20786
+ } finally {
20787
+ for (const [key, read] of this.reads) {
20788
+ read.controller.abort();
20789
+ if (read.ready) this.reads.delete(key);
20790
+ }
20791
+ this.running = void 0;
20792
+ }
20793
+ }
20794
+ fillReadSlots() {
20795
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
20796
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
20797
+ if (!position) return;
20798
+ const key = this.key(position);
20799
+ const read = {
20800
+ position,
20801
+ controller: new AbortController(),
20802
+ promise: Promise.resolve(),
20803
+ ready: false,
20804
+ failed: false
20805
+ };
20806
+ this.reads.set(key, read);
20807
+ read.promise = (async () => {
20652
20808
  try {
20653
- await this.world.placeChunk(chunk);
20654
- this.loaded.set(this.key(position), position);
20809
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
20810
+ if (read.chunk) this.validateChunk(read.chunk, position);
20655
20811
  } catch (error) {
20656
- try {
20657
- await this.unloadMany([position]);
20658
- } catch (cleanupError) {
20659
- const failure = error instanceof Error ? error : new Error(String(error));
20660
- try {
20661
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20662
- } catch {
20663
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
20664
- }
20665
- throw failure;
20812
+ read.failed = true;
20813
+ read.error = error;
20814
+ } finally {
20815
+ read.ready = true;
20816
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
20817
+ this.reads.delete(key);
20666
20818
  }
20667
- throw error;
20819
+ }
20820
+ })();
20821
+ }
20822
+ }
20823
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
20824
+ async placeReadyChunks() {
20825
+ this.fillReadSlots();
20826
+ const position = this.desired.find((position2) => this.needsLoad(position2));
20827
+ if (!position) {
20828
+ if (!this.reads.size) return false;
20829
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20830
+ return true;
20831
+ }
20832
+ const head = this.reads.get(this.key(position));
20833
+ if (!head) {
20834
+ for (let index = this.desired.length - 1; index >= 0; index--) {
20835
+ const key = this.key(this.desired[index]);
20836
+ if (this.reads.get(key)?.ready) {
20837
+ this.reads.delete(key);
20838
+ return true;
20668
20839
  }
20669
20840
  }
20670
- } finally {
20671
- this.running = void 0;
20841
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
20842
+ return true;
20843
+ }
20844
+ if (!head.ready) {
20845
+ await head.promise;
20846
+ return true;
20847
+ }
20848
+ const chunks = [], positions = [];
20849
+ for (const desired of this.desired) {
20850
+ if (!this.needsLoad(desired)) continue;
20851
+ const key = this.key(desired), read = this.reads.get(key);
20852
+ if (!read?.ready) break;
20853
+ this.reads.delete(key);
20854
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
20855
+ else if (!read.chunk) this.missing.set(key, desired);
20856
+ else {
20857
+ chunks.push(read.chunk);
20858
+ positions.push(desired);
20859
+ }
20860
+ }
20861
+ if (!chunks.length) return true;
20862
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
20863
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
20864
+ return true;
20865
+ }
20866
+ async placeOwnedColumns(positions, place) {
20867
+ try {
20868
+ await place();
20869
+ for (const position of positions) this.loaded.set(this.key(position), position);
20870
+ } catch (error) {
20871
+ try {
20872
+ await this.unloadMany(positions);
20873
+ } catch (cleanupError) {
20874
+ const failure = error instanceof Error ? error : new Error(String(error));
20875
+ try {
20876
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20877
+ } catch {
20878
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
20879
+ }
20880
+ throw failure;
20881
+ }
20882
+ throw error;
20672
20883
  }
20673
20884
  }
20674
20885
  needsLoad(position) {