minerender 2.1.0-alpha.141 → 2.1.0-alpha.143

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.
@@ -785,7 +785,10 @@ declare function toRadians(degrees: number): number;
785
785
  declare function toDegrees(radians: number): number;
786
786
  /** Resolves after a delay in milliseconds. */
787
787
  declare function sleep(timeout: number): Promise<void>;
788
- /** Yields to the next macrotask without timer clamping when the platform supports it. */
788
+ /**
789
+ * Yields to the next macrotask without timer clamping when the platform supports it.
790
+ * Not `scheduler.yield()`: Chrome runs its continuations ahead of rendering, so chained yields delay frames.
791
+ */
789
792
  declare function yieldToEventLoop(): Promise<void>;
790
793
  /** Reduces an angle modulo 360, retaining a negative sign for negative inputs. */
791
794
  declare function clampRotationDegrees(deg: number): number;
@@ -4284,6 +4287,12 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4284
4287
  private placeChunkGroups;
4285
4288
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
4286
4289
  placeChunk(chunk: AnvilChunk, executor?: BatchedExecutor): Promise<void>;
4290
+ /**
4291
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
4292
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
4293
+ */
4294
+ placeChunks(chunks: readonly AnvilChunk[]): Promise<void>;
4295
+ private placeColumn;
4287
4296
  /**
4288
4297
  * Removes blocks and owned entities at chunk-column coordinates, then refreshes neighboring faces and fluids.
4289
4298
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
@@ -4371,10 +4380,11 @@ declare class WorldStreamer {
4371
4380
  private desired;
4372
4381
  private center?;
4373
4382
  private loading?;
4383
+ private readonly reads;
4374
4384
  private running?;
4375
4385
  private disposal?;
4376
4386
  private disposed;
4377
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4387
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns"> & Partial<Pick<MineRenderWorld<boolean>, "placeChunks">>, source: WorldChunkSource, options?: WorldStreamerOptions);
4378
4388
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4379
4389
  get loadedChunks(): readonly {
4380
4390
  x: number;
@@ -4407,6 +4417,10 @@ declare class WorldStreamer {
4407
4417
  /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */
4408
4418
  dispose(): Promise<void>;
4409
4419
  private drain;
4420
+ private fillReadSlots;
4421
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
4422
+ private placeReadyChunks;
4423
+ private placeOwnedColumns;
4410
4424
  private needsLoad;
4411
4425
  private unloadMany;
4412
4426
  private validateChunk;
@@ -785,7 +785,10 @@ declare function toRadians(degrees: number): number;
785
785
  declare function toDegrees(radians: number): number;
786
786
  /** Resolves after a delay in milliseconds. */
787
787
  declare function sleep(timeout: number): Promise<void>;
788
- /** Yields to the next macrotask without timer clamping when the platform supports it. */
788
+ /**
789
+ * Yields to the next macrotask without timer clamping when the platform supports it.
790
+ * Not `scheduler.yield()`: Chrome runs its continuations ahead of rendering, so chained yields delay frames.
791
+ */
789
792
  declare function yieldToEventLoop(): Promise<void>;
790
793
  /** Reduces an angle modulo 360, retaining a negative sign for negative inputs. */
791
794
  declare function clampRotationDegrees(deg: number): number;
@@ -4284,6 +4287,12 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4284
4287
  private placeChunkGroups;
4285
4288
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
4286
4289
  placeChunk(chunk: AnvilChunk, executor?: BatchedExecutor): Promise<void>;
4290
+ /**
4291
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
4292
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
4293
+ */
4294
+ placeChunks(chunks: readonly AnvilChunk[]): Promise<void>;
4295
+ private placeColumn;
4287
4296
  /**
4288
4297
  * Removes blocks and owned entities at chunk-column coordinates, then refreshes neighboring faces and fluids.
4289
4298
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
@@ -4371,10 +4380,11 @@ declare class WorldStreamer {
4371
4380
  private desired;
4372
4381
  private center?;
4373
4382
  private loading?;
4383
+ private readonly reads;
4374
4384
  private running?;
4375
4385
  private disposal?;
4376
4386
  private disposed;
4377
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4387
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns"> & Partial<Pick<MineRenderWorld<boolean>, "placeChunks">>, source: WorldChunkSource, options?: WorldStreamerOptions);
4378
4388
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4379
4389
  get loadedChunks(): readonly {
4380
4390
  x: number;
@@ -4407,6 +4417,10 @@ declare class WorldStreamer {
4407
4417
  /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */
4408
4418
  dispose(): Promise<void>;
4409
4419
  private drain;
4420
+ private fillReadSlots;
4421
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
4422
+ private placeReadyChunks;
4423
+ private placeOwnedColumns;
4410
4424
  private needsLoad;
4411
4425
  private unloadMany;
4412
4426
  private validateChunk;
@@ -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();
@@ -18696,6 +18694,7 @@ function skipsBlockFace(data, rule, block, neighbor, face) {
18696
18694
  // src/world/MineRenderWorld.ts
18697
18695
  init_log();
18698
18696
  var log18 = logger("MineRenderWorld");
18697
+ var YIELD_INTERVAL_MS = 8;
18699
18698
  var MineRenderWorld = class {
18700
18699
  constructor(scene, options = {}) {
18701
18700
  this._chunks = /* @__PURE__ */ new Map();
@@ -18809,7 +18808,7 @@ var MineRenderWorld = class {
18809
18808
  failures.set(chunk, error);
18810
18809
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18811
18810
  }
18812
- if (performance.now() - sliceStart > 8) {
18811
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18813
18812
  await yieldToEventLoop();
18814
18813
  sliceStart = performance.now();
18815
18814
  }
@@ -18964,7 +18963,7 @@ var MineRenderWorld = class {
18964
18963
  ));
18965
18964
  for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
18966
18965
  }
18967
- if (performance.now() - sliceStart > 8) {
18966
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18968
18967
  await yieldToEventLoop();
18969
18968
  sliceStart = performance.now();
18970
18969
  }
@@ -18972,6 +18971,62 @@ var MineRenderWorld = class {
18972
18971
  }
18973
18972
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
18974
18973
  async placeChunk(chunk, executor) {
18974
+ return this.placeColumn(chunk, executor);
18975
+ }
18976
+ /**
18977
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
18978
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
18979
+ */
18980
+ async placeChunks(chunks) {
18981
+ const columns = [];
18982
+ for (const chunk of chunks) {
18983
+ const { x, z } = chunk ?? {};
18984
+ if (![x, z].every(Number.isSafeInteger)) {
18985
+ throw new RangeError("Chunk column coordinates must be safe integers");
18986
+ }
18987
+ const sections = Array.from(chunk.sections, (section) => {
18988
+ const { y } = section ?? {};
18989
+ if (!Number.isSafeInteger(y)) throw new RangeError("Chunk section coordinates must be safe integers");
18990
+ return { ...section, y };
18991
+ });
18992
+ columns.push({ ...chunk, x, z, sections });
18993
+ }
18994
+ if (!columns.length) return;
18995
+ await this.culling;
18996
+ let failed = false, failure, sliceStart = performance.now();
18997
+ try {
18998
+ for (const column of columns) {
18999
+ await this.placeColumn(column, void 0, true);
19000
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19001
+ await yieldToEventLoop();
19002
+ sliceStart = performance.now();
19003
+ }
19004
+ }
19005
+ } catch (error) {
19006
+ failed = true;
19007
+ failure = error;
19008
+ } finally {
19009
+ try {
19010
+ const failures = await this.refreshBiomes(true);
19011
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
19012
+ } catch (error) {
19013
+ if (!failed) {
19014
+ failed = true;
19015
+ failure = error;
19016
+ }
19017
+ }
19018
+ try {
19019
+ await this.updateCulling([]);
19020
+ } catch (error) {
19021
+ if (!failed) {
19022
+ failed = true;
19023
+ failure = error;
19024
+ }
19025
+ }
19026
+ }
19027
+ if (failed) throw failure;
19028
+ }
19029
+ async placeColumn(chunk, executor, deferCulling = false) {
18975
19030
  if (![chunk.x, chunk.z].every(Number.isSafeInteger)) {
18976
19031
  throw new RangeError("Chunk column coordinates must be safe integers");
18977
19032
  }
@@ -19033,7 +19088,7 @@ var MineRenderWorld = class {
19033
19088
  } finally {
19034
19089
  this.markSectionChanged(target);
19035
19090
  }
19036
- if (performance.now() - sliceStart > 8) {
19091
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19037
19092
  await yieldToEventLoop();
19038
19093
  sliceStart = performance.now();
19039
19094
  }
@@ -19056,9 +19111,12 @@ var MineRenderWorld = class {
19056
19111
  await placeEntities?.();
19057
19112
  } finally {
19058
19113
  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()]);
19114
+ if (deferCulling) await this.updateCulling([...changes.values()], true);
19115
+ else {
19116
+ const failures = await this.refreshBiomes();
19117
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
19118
+ await this.updateCulling([...changes.values()]);
19119
+ }
19062
19120
  }
19063
19121
  }
19064
19122
  /**
@@ -19088,7 +19146,7 @@ var MineRenderWorld = class {
19088
19146
  for (const { x, z } of columns.values()) {
19089
19147
  this.entities?.clearColumn(x, z);
19090
19148
  await this.clearChunkColumn(x, z);
19091
- if (performance.now() - sliceStart > 8) {
19149
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19092
19150
  await yieldToEventLoop();
19093
19151
  sliceStart = performance.now();
19094
19152
  }
@@ -19227,15 +19285,25 @@ var MineRenderWorld = class {
19227
19285
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19228
19286
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
19229
19287
  chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19230
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19288
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19231
19289
  await yieldToEventLoop();
19232
19290
  sliceStart = performance.now();
19233
19291
  }
19234
19292
  }
19235
- const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19236
- await chunk.rebuildSectionMesh();
19237
- batch.delete(chunk);
19238
- }));
19293
+ const pending = [];
19294
+ for (const chunk of [...batch.keys()]) {
19295
+ const build = (async () => {
19296
+ await chunk.rebuildSectionMesh();
19297
+ batch.delete(chunk);
19298
+ })();
19299
+ void build.catch(() => void 0);
19300
+ pending.push(build);
19301
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19302
+ await yieldToEventLoop();
19303
+ sliceStart = performance.now();
19304
+ }
19305
+ }
19306
+ const builds = await Promise.allSettled(pending);
19239
19307
  const failure = builds.find((result) => result.status === "rejected");
19240
19308
  if (failure?.status === "rejected") throw failure.reason;
19241
19309
  } catch (error) {
@@ -19679,6 +19747,9 @@ var SectionWorker = class _SectionWorker {
19679
19747
  };
19680
19748
 
19681
19749
  // src/world/SectionMesh.ts
19750
+ var BLOCK_SIZE = 16;
19751
+ var QUAD_VERTICES = 4;
19752
+ var QUAD_INDICES = 6;
19682
19753
  var templateCache = /* @__PURE__ */ new WeakMap();
19683
19754
  var WATER_TINT = new import_three46.Color(4159204).toArray();
19684
19755
  function toInput(entries, maxAtlasSize, fluids, lighting) {
@@ -19715,8 +19786,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19715
19786
  if (!data) {
19716
19787
  const indices = template.geometry.getIndex();
19717
19788
  const position = template.geometry.getAttribute("position");
19718
- const quads = indices.count / 6;
19719
- if (indices.count % 6 !== 0 || position.count !== quads * 4) {
19789
+ const quads = indices.count / QUAD_INDICES;
19790
+ if (indices.count % QUAD_INDICES !== 0 || position.count !== quads * QUAD_VERTICES) {
19720
19791
  throw new RangeError("Section template geometry must hold four vertices and six indices per quad");
19721
19792
  }
19722
19793
  data = {
@@ -19726,8 +19797,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19726
19797
  uvs: new Float32Array(quads * 8),
19727
19798
  uvBounds: new Float32Array(quads * 16),
19728
19799
  colors: new Float32Array(quads * 12),
19729
- shade: new Float32Array(quads * 4),
19730
- indices: new Uint16Array(quads * 6),
19800
+ shade: new Float32Array(quads * QUAD_VERTICES),
19801
+ indices: new Uint16Array(quads * QUAD_INDICES),
19731
19802
  cullFaces: new Uint8Array(template.cullFaces),
19732
19803
  biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
19733
19804
  atlas: 0,
@@ -19739,7 +19810,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19739
19810
  const bounds = template.geometry.getAttribute("uvBounds");
19740
19811
  const color2 = template.geometry.getAttribute("color");
19741
19812
  const shade = template.geometry.getAttribute("shade");
19742
- for (let vertex = 0; vertex < quads * 4; vertex++) {
19813
+ for (let vertex = 0; vertex < quads * QUAD_VERTICES; vertex++) {
19743
19814
  data.positions.set([position.getX(vertex), position.getY(vertex), position.getZ(vertex)], vertex * 3);
19744
19815
  data.normals.set([normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex)], vertex * 3);
19745
19816
  data.uvs.set([uv.getX(vertex), uv.getY(vertex)], vertex * 2);
@@ -19752,7 +19823,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19752
19823
  data.colors.set([color2?.getX(vertex) ?? 1, color2?.getY(vertex) ?? 1, color2?.getZ(vertex) ?? 1], vertex * 3);
19753
19824
  data.shade[vertex] = shade?.getX(vertex) ?? 1;
19754
19825
  }
19755
- for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2) - 4 * Math.floor(i2 / 6);
19826
+ for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2) - QUAD_VERTICES * Math.floor(i2 / QUAD_INDICES);
19756
19827
  templateCache.set(template, data);
19757
19828
  }
19758
19829
  id = input.templateData.length;
@@ -19785,6 +19856,54 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19785
19856
  }
19786
19857
  return { input, atlases: atlases2 };
19787
19858
  }
19859
+ function sortTransparentQuads(mesh) {
19860
+ const index = mesh.geometry.getIndex();
19861
+ if (index.count <= QUAD_INDICES) return;
19862
+ index.setUsage(import_three46.DynamicDrawUsage);
19863
+ const count = index.count / QUAD_INDICES;
19864
+ let source, centers, depths, order;
19865
+ const view = new import_three46.Matrix4(), localCamera = new import_three46.Vector3(), key = new import_three46.Vector3(), previous = new import_three46.Vector3();
19866
+ let previousOrthographic;
19867
+ mesh.onBeforeRender = (_renderer, _scene, camera) => {
19868
+ localCamera.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(view.copy(mesh.matrixWorld).invert());
19869
+ const orthographic = !!camera.isOrthographicCamera;
19870
+ if (orthographic) {
19871
+ view.multiplyMatrices(camera.matrixWorldInverse, mesh.matrixWorld);
19872
+ key.set(view.elements[2], view.elements[6], view.elements[10]);
19873
+ } else {
19874
+ key.copy(localCamera).divideScalar(BLOCK_SIZE).floor();
19875
+ }
19876
+ if (orthographic === previousOrthographic && key.equals(previous)) return;
19877
+ previous.copy(key);
19878
+ previousOrthographic = orthographic;
19879
+ if (!centers) {
19880
+ source = new Uint32Array(index.array);
19881
+ centers = new Float32Array(count * 3);
19882
+ depths = new Float32Array(count);
19883
+ order = Uint32Array.from({ length: count }, (_, quad) => quad);
19884
+ const positions = mesh.geometry.getAttribute("position");
19885
+ for (let quad = 0; quad < count; quad++) {
19886
+ for (let axis = 0; axis < 3; axis++) {
19887
+ let center = 0;
19888
+ for (let corner = 0; corner < QUAD_VERTICES; corner++) center += positions.getComponent(quad * QUAD_VERTICES + corner, axis);
19889
+ centers[quad * 3 + axis] = center / QUAD_VERTICES;
19890
+ }
19891
+ }
19892
+ }
19893
+ for (let quad = 0; quad < count; quad++) {
19894
+ const x = centers[quad * 3], y = centers[quad * 3 + 1], z = centers[quad * 3 + 2];
19895
+ depths[quad] = orthographic ? key.x * x + key.y * y + key.z * z : -((x - localCamera.x) ** 2 + (y - localCamera.y) ** 2 + (z - localCamera.z) ** 2);
19896
+ }
19897
+ order.sort((a, b) => depths[a] - depths[b] || a - b);
19898
+ if (!order.some((quad, offset) => index.array[offset * QUAD_INDICES] !== source[quad * QUAD_INDICES])) return;
19899
+ for (let offset = 0; offset < count; offset++) {
19900
+ for (let indexOffset = 0; indexOffset < QUAD_INDICES; indexOffset++) {
19901
+ index.array[offset * QUAD_INDICES + indexOffset] = source[order[offset] * QUAD_INDICES + indexOffset];
19902
+ }
19903
+ }
19904
+ index.needsUpdate = true;
19905
+ };
19906
+ }
19788
19907
  var SectionMesh = class _SectionMesh extends import_three46.Group {
19789
19908
  constructor() {
19790
19909
  super(...arguments);
@@ -19794,7 +19913,11 @@ var SectionMesh = class _SectionMesh extends import_three46.Group {
19794
19913
  /** Returns the owning section cell for a triangle hit on this mesh's merged geometry. */
19795
19914
  getBlockIndex(hit) {
19796
19915
  if (hit.faceIndex === void 0 || hit.faceIndex === null) return void 0;
19797
- return this.blockIndices.get(hit.object)?.[Math.floor(hit.faceIndex / 2)];
19916
+ const mesh = hit.object;
19917
+ const owners = this.blockIndices.get(mesh);
19918
+ if (!owners) return void 0;
19919
+ const vertex = mesh.geometry.getIndex().getX(hit.faceIndex * 3);
19920
+ return owners[Math.floor(vertex / QUAD_VERTICES)];
19798
19921
  }
19799
19922
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
19800
19923
  static build(entries, maxAtlasSize = 2048, fluids, lighting) {
@@ -19830,13 +19953,14 @@ var SectionMesh = class _SectionMesh extends import_three46.Group {
19830
19953
  const material = Materials.createShadedCanvasMaterial(canvas, transparent, false, true);
19831
19954
  material.side = import_three46.FrontSide;
19832
19955
  if (page.layer === 2) {
19956
+ material.depthWrite = !material.transparent;
19833
19957
  material.side = import_three46.DoubleSide;
19834
19958
  material.forceSinglePass = true;
19835
- material.depthWrite = !material.transparent;
19836
19959
  }
19837
19960
  material.vertexColors = true;
19838
19961
  const texture = material.uniforms?.map?.value ?? material.map;
19839
19962
  const mesh = new import_three46.Mesh(geometry, material);
19963
+ if (transparent) sortTransparentQuads(mesh);
19840
19964
  section.add(mesh);
19841
19965
  section.blockIndices.set(mesh, page.blockIndices);
19842
19966
  const unsubscribe = [];
@@ -20639,6 +20763,7 @@ var Chunk = class _Chunk {
20639
20763
  };
20640
20764
 
20641
20765
  // src/world/WorldStreamer.ts
20766
+ var MAX_PREFETCHED_COLUMNS = 4;
20642
20767
  var WorldStreamer = class {
20643
20768
  constructor(world, source, options = {}) {
20644
20769
  this.world = world;
@@ -20648,6 +20773,7 @@ var WorldStreamer = class {
20648
20773
  this.missing = /* @__PURE__ */ new Map();
20649
20774
  this.failed = /* @__PURE__ */ new Map();
20650
20775
  this.desired = [];
20776
+ this.reads = /* @__PURE__ */ new Map();
20651
20777
  this.disposed = false;
20652
20778
  this.loadRadius = options.loadRadius ?? 2;
20653
20779
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -20692,6 +20818,12 @@ var WorldStreamer = class {
20692
20818
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
20693
20819
  this.loading.controller.abort();
20694
20820
  }
20821
+ for (const [key, read] of this.reads) {
20822
+ if (!this.within(read.position, this.loadRadius)) {
20823
+ read.controller.abort();
20824
+ if (read.ready) this.reads.delete(key);
20825
+ }
20826
+ }
20695
20827
  }
20696
20828
  return this.running ??= Promise.resolve().then(() => this.drain());
20697
20829
  }
@@ -20712,8 +20844,11 @@ var WorldStreamer = class {
20712
20844
  this.desired = [];
20713
20845
  this.center = void 0;
20714
20846
  this.loading?.controller.abort();
20847
+ for (const read of this.reads.values()) read.controller.abort();
20715
20848
  return this.disposal ??= Promise.resolve().then(async () => {
20716
20849
  await this.running?.catch(() => void 0);
20850
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20851
+ this.reads.clear();
20717
20852
  try {
20718
20853
  await this.unloadMany([...this.owned.values()]);
20719
20854
  } finally {
@@ -20739,6 +20874,10 @@ var WorldStreamer = class {
20739
20874
  await this.unloadMany(obsolete);
20740
20875
  continue;
20741
20876
  }
20877
+ if (this.world.placeChunks) {
20878
+ if (await this.placeReadyChunks()) continue;
20879
+ return;
20880
+ }
20742
20881
  const position = this.desired.find((position2) => this.needsLoad(position2));
20743
20882
  if (!position) return;
20744
20883
  let chunk;
@@ -20761,26 +20900,105 @@ var WorldStreamer = class {
20761
20900
  continue;
20762
20901
  }
20763
20902
  this.owned.set(this.key(position), position);
20903
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
20904
+ }
20905
+ } finally {
20906
+ for (const [key, read] of this.reads) {
20907
+ read.controller.abort();
20908
+ if (read.ready) this.reads.delete(key);
20909
+ }
20910
+ this.running = void 0;
20911
+ }
20912
+ }
20913
+ fillReadSlots() {
20914
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
20915
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
20916
+ if (!position) return;
20917
+ const key = this.key(position);
20918
+ const read = {
20919
+ position,
20920
+ controller: new AbortController(),
20921
+ promise: Promise.resolve(),
20922
+ ready: false,
20923
+ failed: false
20924
+ };
20925
+ this.reads.set(key, read);
20926
+ read.promise = (async () => {
20764
20927
  try {
20765
- await this.world.placeChunk(chunk);
20766
- this.loaded.set(this.key(position), position);
20928
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
20929
+ if (read.chunk) this.validateChunk(read.chunk, position);
20767
20930
  } catch (error) {
20768
- try {
20769
- await this.unloadMany([position]);
20770
- } catch (cleanupError) {
20771
- const failure = error instanceof Error ? error : new Error(String(error));
20772
- try {
20773
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20774
- } catch {
20775
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
20776
- }
20777
- throw failure;
20931
+ read.failed = true;
20932
+ read.error = error;
20933
+ } finally {
20934
+ read.ready = true;
20935
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
20936
+ this.reads.delete(key);
20778
20937
  }
20779
- throw error;
20938
+ }
20939
+ })();
20940
+ }
20941
+ }
20942
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
20943
+ async placeReadyChunks() {
20944
+ this.fillReadSlots();
20945
+ const position = this.desired.find((position2) => this.needsLoad(position2));
20946
+ if (!position) {
20947
+ if (!this.reads.size) return false;
20948
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20949
+ return true;
20950
+ }
20951
+ const head = this.reads.get(this.key(position));
20952
+ if (!head) {
20953
+ for (let index = this.desired.length - 1; index >= 0; index--) {
20954
+ const key = this.key(this.desired[index]);
20955
+ if (this.reads.get(key)?.ready) {
20956
+ this.reads.delete(key);
20957
+ return true;
20780
20958
  }
20781
20959
  }
20782
- } finally {
20783
- this.running = void 0;
20960
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
20961
+ return true;
20962
+ }
20963
+ if (!head.ready) {
20964
+ await head.promise;
20965
+ return true;
20966
+ }
20967
+ const chunks = [], positions = [];
20968
+ for (const desired of this.desired) {
20969
+ if (!this.needsLoad(desired)) continue;
20970
+ const key = this.key(desired), read = this.reads.get(key);
20971
+ if (!read?.ready) break;
20972
+ this.reads.delete(key);
20973
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
20974
+ else if (!read.chunk) this.missing.set(key, desired);
20975
+ else {
20976
+ chunks.push(read.chunk);
20977
+ positions.push(desired);
20978
+ }
20979
+ }
20980
+ if (!chunks.length) return true;
20981
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
20982
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
20983
+ return true;
20984
+ }
20985
+ async placeOwnedColumns(positions, place) {
20986
+ try {
20987
+ await place();
20988
+ for (const position of positions) this.loaded.set(this.key(position), position);
20989
+ } catch (error) {
20990
+ try {
20991
+ await this.unloadMany(positions);
20992
+ } catch (cleanupError) {
20993
+ const failure = error instanceof Error ? error : new Error(String(error));
20994
+ try {
20995
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20996
+ } catch {
20997
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
20998
+ }
20999
+ throw failure;
21000
+ }
21001
+ throw error;
20784
21002
  }
20785
21003
  }
20786
21004
  needsLoad(position) {