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.
@@ -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,7 +17922,7 @@ var AnvilWorldSource = class {
17924
17922
  };
17925
17923
 
17926
17924
  // src/world/Chunk.ts
17927
- import { Color as Color13, Vector3 as Vector320 } from "three";
17925
+ import { Color as Color13, Vector3 as Vector321 } from "three";
17928
17926
  init_BlockStates();
17929
17927
  init_AssetKey();
17930
17928
 
@@ -18528,6 +18526,7 @@ function skipsBlockFace(data, rule, block, neighbor, face) {
18528
18526
  // src/world/MineRenderWorld.ts
18529
18527
  init_log();
18530
18528
  var log18 = logger("MineRenderWorld");
18529
+ var YIELD_INTERVAL_MS = 8;
18531
18530
  var MineRenderWorld = class {
18532
18531
  constructor(scene, options = {}) {
18533
18532
  this._chunks = /* @__PURE__ */ new Map();
@@ -18641,7 +18640,7 @@ var MineRenderWorld = class {
18641
18640
  failures.set(chunk, error);
18642
18641
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18643
18642
  }
18644
- if (performance.now() - sliceStart > 8) {
18643
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18645
18644
  await yieldToEventLoop();
18646
18645
  sliceStart = performance.now();
18647
18646
  }
@@ -18796,7 +18795,7 @@ var MineRenderWorld = class {
18796
18795
  ));
18797
18796
  for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
18798
18797
  }
18799
- if (performance.now() - sliceStart > 8) {
18798
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18800
18799
  await yieldToEventLoop();
18801
18800
  sliceStart = performance.now();
18802
18801
  }
@@ -18804,6 +18803,62 @@ var MineRenderWorld = class {
18804
18803
  }
18805
18804
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
18806
18805
  async placeChunk(chunk, executor) {
18806
+ return this.placeColumn(chunk, executor);
18807
+ }
18808
+ /**
18809
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
18810
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
18811
+ */
18812
+ async placeChunks(chunks) {
18813
+ const columns = [];
18814
+ for (const chunk of chunks) {
18815
+ const { x, z } = chunk ?? {};
18816
+ if (![x, z].every(Number.isSafeInteger)) {
18817
+ throw new RangeError("Chunk column coordinates must be safe integers");
18818
+ }
18819
+ const sections = Array.from(chunk.sections, (section) => {
18820
+ const { y } = section ?? {};
18821
+ if (!Number.isSafeInteger(y)) throw new RangeError("Chunk section coordinates must be safe integers");
18822
+ return { ...section, y };
18823
+ });
18824
+ columns.push({ ...chunk, x, z, sections });
18825
+ }
18826
+ if (!columns.length) return;
18827
+ await this.culling;
18828
+ let failed = false, failure, sliceStart = performance.now();
18829
+ try {
18830
+ for (const column of columns) {
18831
+ await this.placeColumn(column, void 0, true);
18832
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18833
+ await yieldToEventLoop();
18834
+ sliceStart = performance.now();
18835
+ }
18836
+ }
18837
+ } catch (error) {
18838
+ failed = true;
18839
+ failure = error;
18840
+ } finally {
18841
+ try {
18842
+ const failures = await this.refreshBiomes(true);
18843
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18844
+ } catch (error) {
18845
+ if (!failed) {
18846
+ failed = true;
18847
+ failure = error;
18848
+ }
18849
+ }
18850
+ try {
18851
+ await this.updateCulling([]);
18852
+ } catch (error) {
18853
+ if (!failed) {
18854
+ failed = true;
18855
+ failure = error;
18856
+ }
18857
+ }
18858
+ }
18859
+ if (failed) throw failure;
18860
+ }
18861
+ async placeColumn(chunk, executor, deferCulling = false) {
18807
18862
  if (![chunk.x, chunk.z].every(Number.isSafeInteger)) {
18808
18863
  throw new RangeError("Chunk column coordinates must be safe integers");
18809
18864
  }
@@ -18865,7 +18920,7 @@ var MineRenderWorld = class {
18865
18920
  } finally {
18866
18921
  this.markSectionChanged(target);
18867
18922
  }
18868
- if (performance.now() - sliceStart > 8) {
18923
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18869
18924
  await yieldToEventLoop();
18870
18925
  sliceStart = performance.now();
18871
18926
  }
@@ -18888,9 +18943,12 @@ var MineRenderWorld = class {
18888
18943
  await placeEntities?.();
18889
18944
  } finally {
18890
18945
  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()]);
18946
+ if (deferCulling) await this.updateCulling([...changes.values()], true);
18947
+ else {
18948
+ const failures = await this.refreshBiomes();
18949
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18950
+ await this.updateCulling([...changes.values()]);
18951
+ }
18894
18952
  }
18895
18953
  }
18896
18954
  /**
@@ -18920,7 +18978,7 @@ var MineRenderWorld = class {
18920
18978
  for (const { x, z } of columns.values()) {
18921
18979
  this.entities?.clearColumn(x, z);
18922
18980
  await this.clearChunkColumn(x, z);
18923
- if (performance.now() - sliceStart > 8) {
18981
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18924
18982
  await yieldToEventLoop();
18925
18983
  sliceStart = performance.now();
18926
18984
  }
@@ -19059,15 +19117,25 @@ var MineRenderWorld = class {
19059
19117
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19060
19118
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
19061
19119
  chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19062
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19120
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19063
19121
  await yieldToEventLoop();
19064
19122
  sliceStart = performance.now();
19065
19123
  }
19066
19124
  }
19067
- const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19068
- await chunk.rebuildSectionMesh();
19069
- batch.delete(chunk);
19070
- }));
19125
+ const pending = [];
19126
+ for (const chunk of [...batch.keys()]) {
19127
+ const build = (async () => {
19128
+ await chunk.rebuildSectionMesh();
19129
+ batch.delete(chunk);
19130
+ })();
19131
+ void build.catch(() => void 0);
19132
+ pending.push(build);
19133
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19134
+ await yieldToEventLoop();
19135
+ sliceStart = performance.now();
19136
+ }
19137
+ }
19138
+ const builds = await Promise.allSettled(pending);
19071
19139
  const failure = builds.find((result) => result.status === "rejected");
19072
19140
  if (failure?.status === "rejected") throw failure.reason;
19073
19141
  } catch (error) {
@@ -19184,7 +19252,7 @@ init_BlockEntities();
19184
19252
  // src/world/SectionMesh.ts
19185
19253
  init_CanvasCompat();
19186
19254
  init_Materials();
19187
- import { BufferGeometry as BufferGeometry9, Color as Color12, DoubleSide as DoubleSide8, Float32BufferAttribute as Float32BufferAttribute10, FrontSide as FrontSide4, Group as Group3, Mesh as Mesh12, Uint32BufferAttribute } from "three";
19255
+ import { BufferGeometry as BufferGeometry9, Color as Color12, DoubleSide as DoubleSide8, DynamicDrawUsage, Float32BufferAttribute as Float32BufferAttribute10, FrontSide as FrontSide4, Group as Group3, Matrix4 as Matrix413, Mesh as Mesh12, Uint32BufferAttribute, Vector3 as Vector320 } from "three";
19188
19256
 
19189
19257
  // src/world/SectionGeometry.ts
19190
19258
  init_FluidQuads();
@@ -19511,6 +19579,9 @@ var SectionWorker = class _SectionWorker {
19511
19579
  };
19512
19580
 
19513
19581
  // src/world/SectionMesh.ts
19582
+ var BLOCK_SIZE = 16;
19583
+ var QUAD_VERTICES = 4;
19584
+ var QUAD_INDICES = 6;
19514
19585
  var templateCache = /* @__PURE__ */ new WeakMap();
19515
19586
  var WATER_TINT = new Color12(4159204).toArray();
19516
19587
  function toInput(entries, maxAtlasSize, fluids, lighting) {
@@ -19547,8 +19618,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19547
19618
  if (!data) {
19548
19619
  const indices = template.geometry.getIndex();
19549
19620
  const position = template.geometry.getAttribute("position");
19550
- const quads = indices.count / 6;
19551
- if (indices.count % 6 !== 0 || position.count !== quads * 4) {
19621
+ const quads = indices.count / QUAD_INDICES;
19622
+ if (indices.count % QUAD_INDICES !== 0 || position.count !== quads * QUAD_VERTICES) {
19552
19623
  throw new RangeError("Section template geometry must hold four vertices and six indices per quad");
19553
19624
  }
19554
19625
  data = {
@@ -19558,8 +19629,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19558
19629
  uvs: new Float32Array(quads * 8),
19559
19630
  uvBounds: new Float32Array(quads * 16),
19560
19631
  colors: new Float32Array(quads * 12),
19561
- shade: new Float32Array(quads * 4),
19562
- indices: new Uint16Array(quads * 6),
19632
+ shade: new Float32Array(quads * QUAD_VERTICES),
19633
+ indices: new Uint16Array(quads * QUAD_INDICES),
19563
19634
  cullFaces: new Uint8Array(template.cullFaces),
19564
19635
  biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
19565
19636
  atlas: 0,
@@ -19571,7 +19642,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19571
19642
  const bounds = template.geometry.getAttribute("uvBounds");
19572
19643
  const color2 = template.geometry.getAttribute("color");
19573
19644
  const shade = template.geometry.getAttribute("shade");
19574
- for (let vertex = 0; vertex < quads * 4; vertex++) {
19645
+ for (let vertex = 0; vertex < quads * QUAD_VERTICES; vertex++) {
19575
19646
  data.positions.set([position.getX(vertex), position.getY(vertex), position.getZ(vertex)], vertex * 3);
19576
19647
  data.normals.set([normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex)], vertex * 3);
19577
19648
  data.uvs.set([uv.getX(vertex), uv.getY(vertex)], vertex * 2);
@@ -19584,7 +19655,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19584
19655
  data.colors.set([color2?.getX(vertex) ?? 1, color2?.getY(vertex) ?? 1, color2?.getZ(vertex) ?? 1], vertex * 3);
19585
19656
  data.shade[vertex] = shade?.getX(vertex) ?? 1;
19586
19657
  }
19587
- for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2) - 4 * Math.floor(i2 / 6);
19658
+ for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2) - QUAD_VERTICES * Math.floor(i2 / QUAD_INDICES);
19588
19659
  templateCache.set(template, data);
19589
19660
  }
19590
19661
  id = input.templateData.length;
@@ -19617,6 +19688,54 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19617
19688
  }
19618
19689
  return { input, atlases: atlases2 };
19619
19690
  }
19691
+ function sortTransparentQuads(mesh) {
19692
+ const index = mesh.geometry.getIndex();
19693
+ if (index.count <= QUAD_INDICES) return;
19694
+ index.setUsage(DynamicDrawUsage);
19695
+ const count = index.count / QUAD_INDICES;
19696
+ let source, centers, depths, order;
19697
+ const view = new Matrix413(), localCamera = new Vector320(), key = new Vector320(), previous = new Vector320();
19698
+ let previousOrthographic;
19699
+ mesh.onBeforeRender = (_renderer, _scene, camera) => {
19700
+ localCamera.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(view.copy(mesh.matrixWorld).invert());
19701
+ const orthographic = !!camera.isOrthographicCamera;
19702
+ if (orthographic) {
19703
+ view.multiplyMatrices(camera.matrixWorldInverse, mesh.matrixWorld);
19704
+ key.set(view.elements[2], view.elements[6], view.elements[10]);
19705
+ } else {
19706
+ key.copy(localCamera).divideScalar(BLOCK_SIZE).floor();
19707
+ }
19708
+ if (orthographic === previousOrthographic && key.equals(previous)) return;
19709
+ previous.copy(key);
19710
+ previousOrthographic = orthographic;
19711
+ if (!centers) {
19712
+ source = new Uint32Array(index.array);
19713
+ centers = new Float32Array(count * 3);
19714
+ depths = new Float32Array(count);
19715
+ order = Uint32Array.from({ length: count }, (_, quad) => quad);
19716
+ const positions = mesh.geometry.getAttribute("position");
19717
+ for (let quad = 0; quad < count; quad++) {
19718
+ for (let axis = 0; axis < 3; axis++) {
19719
+ let center = 0;
19720
+ for (let corner = 0; corner < QUAD_VERTICES; corner++) center += positions.getComponent(quad * QUAD_VERTICES + corner, axis);
19721
+ centers[quad * 3 + axis] = center / QUAD_VERTICES;
19722
+ }
19723
+ }
19724
+ }
19725
+ for (let quad = 0; quad < count; quad++) {
19726
+ const x = centers[quad * 3], y = centers[quad * 3 + 1], z = centers[quad * 3 + 2];
19727
+ depths[quad] = orthographic ? key.x * x + key.y * y + key.z * z : -((x - localCamera.x) ** 2 + (y - localCamera.y) ** 2 + (z - localCamera.z) ** 2);
19728
+ }
19729
+ order.sort((a, b) => depths[a] - depths[b] || a - b);
19730
+ if (!order.some((quad, offset) => index.array[offset * QUAD_INDICES] !== source[quad * QUAD_INDICES])) return;
19731
+ for (let offset = 0; offset < count; offset++) {
19732
+ for (let indexOffset = 0; indexOffset < QUAD_INDICES; indexOffset++) {
19733
+ index.array[offset * QUAD_INDICES + indexOffset] = source[order[offset] * QUAD_INDICES + indexOffset];
19734
+ }
19735
+ }
19736
+ index.needsUpdate = true;
19737
+ };
19738
+ }
19620
19739
  var SectionMesh = class _SectionMesh extends Group3 {
19621
19740
  constructor() {
19622
19741
  super(...arguments);
@@ -19626,7 +19745,11 @@ var SectionMesh = class _SectionMesh extends Group3 {
19626
19745
  /** Returns the owning section cell for a triangle hit on this mesh's merged geometry. */
19627
19746
  getBlockIndex(hit) {
19628
19747
  if (hit.faceIndex === void 0 || hit.faceIndex === null) return void 0;
19629
- return this.blockIndices.get(hit.object)?.[Math.floor(hit.faceIndex / 2)];
19748
+ const mesh = hit.object;
19749
+ const owners = this.blockIndices.get(mesh);
19750
+ if (!owners) return void 0;
19751
+ const vertex = mesh.geometry.getIndex().getX(hit.faceIndex * 3);
19752
+ return owners[Math.floor(vertex / QUAD_VERTICES)];
19630
19753
  }
19631
19754
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
19632
19755
  static build(entries, maxAtlasSize = 2048, fluids, lighting) {
@@ -19662,13 +19785,14 @@ var SectionMesh = class _SectionMesh extends Group3 {
19662
19785
  const material = Materials.createShadedCanvasMaterial(canvas, transparent, false, true);
19663
19786
  material.side = FrontSide4;
19664
19787
  if (page.layer === 2) {
19788
+ material.depthWrite = !material.transparent;
19665
19789
  material.side = DoubleSide8;
19666
19790
  material.forceSinglePass = true;
19667
- material.depthWrite = !material.transparent;
19668
19791
  }
19669
19792
  material.vertexColors = true;
19670
19793
  const texture = material.uniforms?.map?.value ?? material.map;
19671
19794
  const mesh = new Mesh12(geometry, material);
19795
+ if (transparent) sortTransparentQuads(mesh);
19672
19796
  section.add(mesh);
19673
19797
  section.blockIndices.set(mesh, page.blockIndices);
19674
19798
  const unsubscribe = [];
@@ -19727,7 +19851,7 @@ var Chunk = class _Chunk {
19727
19851
  this.sectionBlocks = /* @__PURE__ */ new Map();
19728
19852
  this.hiddenBlocks = /* @__PURE__ */ new Set();
19729
19853
  this.fluidCells = new Uint8Array(4096);
19730
- this.blockPosition = new Vector320();
19854
+ this.blockPosition = new Vector321();
19731
19855
  this.meshDirty = false;
19732
19856
  this.meshGeneration = 0;
19733
19857
  this.blockChanges = /* @__PURE__ */ new Set();
@@ -19787,7 +19911,7 @@ var Chunk = class _Chunk {
19787
19911
  const info = owner.chunk.renderedBlocks.get(owner.index);
19788
19912
  if (!info) continue;
19789
19913
  return {
19790
- position: new Vector320(
19914
+ position: new Vector321(
19791
19915
  owner.chunk.x * 16 + owner.index % 16,
19792
19916
  owner.chunk.y * 16 + Math.floor(owner.index / 256),
19793
19917
  owner.chunk.z * 16 + Math.floor(owner.index / 16) % 16
@@ -19802,18 +19926,18 @@ var Chunk = class _Chunk {
19802
19926
  }
19803
19927
  getBlockAt(posOrX, y, z) {
19804
19928
  if (typeof posOrX == "number") {
19805
- return this.getBlockAt(new Vector320(posOrX, y, z));
19929
+ return this.getBlockAt(new Vector321(posOrX, y, z));
19806
19930
  }
19807
19931
  if (isTripleArray(posOrX)) {
19808
- return this.getBlockAt(new Vector320(posOrX[0], posOrX[1], posOrX[2]));
19932
+ return this.getBlockAt(new Vector321(posOrX[0], posOrX[1], posOrX[2]));
19809
19933
  }
19810
19934
  const pos = this.blockPosition.set(posOrX.x - this.x * 16, posOrX.y - this.y * 16, posOrX.z - this.z * 16);
19811
19935
  const index = _Chunk.chunkPosToBlockIndex(pos);
19812
19936
  return this.renderedBlocks.get(index);
19813
19937
  }
19814
19938
  getBiomeAt(posOrX, y, z) {
19815
- if (typeof posOrX === "number") return this.getBiomeAt(new Vector320(posOrX, y, z));
19816
- if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector320(...posOrX));
19939
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector321(posOrX, y, z));
19940
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector321(...posOrX));
19817
19941
  const pos = this.worldPosToChunkPos(posOrX);
19818
19942
  _Chunk.chunkPosToBlockIndex(pos);
19819
19943
  return this.biomeAt ? this.biomeAt(posOrX.x, posOrX.y, posOrX.z) : this.biomes?.[Math.floor(pos.x / 4) + Math.floor(pos.z / 4) * 4 + Math.floor(pos.y / 4) * 16];
@@ -19891,17 +20015,17 @@ var Chunk = class _Chunk {
19891
20015
  this.hiddenBlocks.add(index);
19892
20016
  this.renderedBlocks.get(index)?.object?.setVisible(false);
19893
20017
  }
19894
- const positions = blocks.map(({ index }) => new Vector320(...this.positionAt(index)));
20018
+ const positions = blocks.map(({ index }) => new Vector321(...this.positionAt(index)));
19895
20019
  if (this.onBlocksChanged) await this.onBlocksChanged(positions, deferCulling);
19896
20020
  else await this.rebuildSectionMesh();
19897
20021
  }
19898
20022
  }
19899
20023
  async setBlockAt(posOrX, yOrBlock, z, block) {
19900
20024
  if (typeof posOrX == "number") {
19901
- return this.setBlockAt(new Vector320(posOrX, yOrBlock, z), block);
20025
+ return this.setBlockAt(new Vector321(posOrX, yOrBlock, z), block);
19902
20026
  }
19903
20027
  if (isTripleArray(posOrX)) {
19904
- return this.setBlockAt(new Vector320(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
20028
+ return this.setBlockAt(new Vector321(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
19905
20029
  }
19906
20030
  const worldPos = posOrX;
19907
20031
  const pos = this.worldPosToChunkPos(worldPos);
@@ -19974,7 +20098,7 @@ var Chunk = class _Chunk {
19974
20098
  this.fluidCells[index] = 0;
19975
20099
  this.renderedBlocks.delete(index);
19976
20100
  this.objectBlocks.delete(index);
19977
- const worldPos = new Vector320(
20101
+ const worldPos = new Vector321(
19978
20102
  this.x * 16 + index % 16,
19979
20103
  this.y * 16 + Math.floor(index / 256),
19980
20104
  this.z * 16 + Math.floor(index / 16) % 16
@@ -20112,7 +20236,7 @@ var Chunk = class _Chunk {
20112
20236
  biome,
20113
20237
  biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
20114
20238
  });
20115
- object4.setPosition(new Vector320(
20239
+ object4.setPosition(new Vector321(
20116
20240
  this.x * 256 + index % 16 * 16,
20117
20241
  this.y * 256 + Math.floor(index / 256) * 16,
20118
20242
  this.z * 256 + Math.floor(index / 16) % 16 * 16
@@ -20419,7 +20543,7 @@ var Chunk = class _Chunk {
20419
20543
  async clear(notify = true) {
20420
20544
  if (this.blockChanges.size) await Promise.allSettled(this.blockChanges);
20421
20545
  const positions = notify && this.onBlocksChanged ? [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
20422
- new Vector320(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
20546
+ new Vector321(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
20423
20547
  )) : [];
20424
20548
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
20425
20549
  this.sectionBlocks.clear();
@@ -20455,14 +20579,14 @@ var Chunk = class _Chunk {
20455
20579
  return pos.y * 16 * 16 + pos.z * 16 + pos.x;
20456
20580
  }
20457
20581
  worldPosToChunkPos(pos) {
20458
- return new Vector320(
20582
+ return new Vector321(
20459
20583
  pos.x - this.x * 16,
20460
20584
  pos.y - this.y * 16,
20461
20585
  pos.z - this.z * 16
20462
20586
  );
20463
20587
  }
20464
20588
  chunkPosToWorldPos(pos) {
20465
- return new Vector320(
20589
+ return new Vector321(
20466
20590
  this.x * 16 + pos.x,
20467
20591
  this.y * 16 + pos.y,
20468
20592
  this.z * 16 + pos.z
@@ -20471,6 +20595,7 @@ var Chunk = class _Chunk {
20471
20595
  };
20472
20596
 
20473
20597
  // src/world/WorldStreamer.ts
20598
+ var MAX_PREFETCHED_COLUMNS = 4;
20474
20599
  var WorldStreamer = class {
20475
20600
  constructor(world, source, options = {}) {
20476
20601
  this.world = world;
@@ -20480,6 +20605,7 @@ var WorldStreamer = class {
20480
20605
  this.missing = /* @__PURE__ */ new Map();
20481
20606
  this.failed = /* @__PURE__ */ new Map();
20482
20607
  this.desired = [];
20608
+ this.reads = /* @__PURE__ */ new Map();
20483
20609
  this.disposed = false;
20484
20610
  this.loadRadius = options.loadRadius ?? 2;
20485
20611
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -20524,6 +20650,12 @@ var WorldStreamer = class {
20524
20650
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
20525
20651
  this.loading.controller.abort();
20526
20652
  }
20653
+ for (const [key, read] of this.reads) {
20654
+ if (!this.within(read.position, this.loadRadius)) {
20655
+ read.controller.abort();
20656
+ if (read.ready) this.reads.delete(key);
20657
+ }
20658
+ }
20527
20659
  }
20528
20660
  return this.running ??= Promise.resolve().then(() => this.drain());
20529
20661
  }
@@ -20544,8 +20676,11 @@ var WorldStreamer = class {
20544
20676
  this.desired = [];
20545
20677
  this.center = void 0;
20546
20678
  this.loading?.controller.abort();
20679
+ for (const read of this.reads.values()) read.controller.abort();
20547
20680
  return this.disposal ??= Promise.resolve().then(async () => {
20548
20681
  await this.running?.catch(() => void 0);
20682
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20683
+ this.reads.clear();
20549
20684
  try {
20550
20685
  await this.unloadMany([...this.owned.values()]);
20551
20686
  } finally {
@@ -20571,6 +20706,10 @@ var WorldStreamer = class {
20571
20706
  await this.unloadMany(obsolete);
20572
20707
  continue;
20573
20708
  }
20709
+ if (this.world.placeChunks) {
20710
+ if (await this.placeReadyChunks()) continue;
20711
+ return;
20712
+ }
20574
20713
  const position = this.desired.find((position2) => this.needsLoad(position2));
20575
20714
  if (!position) return;
20576
20715
  let chunk;
@@ -20593,26 +20732,105 @@ var WorldStreamer = class {
20593
20732
  continue;
20594
20733
  }
20595
20734
  this.owned.set(this.key(position), position);
20735
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
20736
+ }
20737
+ } finally {
20738
+ for (const [key, read] of this.reads) {
20739
+ read.controller.abort();
20740
+ if (read.ready) this.reads.delete(key);
20741
+ }
20742
+ this.running = void 0;
20743
+ }
20744
+ }
20745
+ fillReadSlots() {
20746
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
20747
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
20748
+ if (!position) return;
20749
+ const key = this.key(position);
20750
+ const read = {
20751
+ position,
20752
+ controller: new AbortController(),
20753
+ promise: Promise.resolve(),
20754
+ ready: false,
20755
+ failed: false
20756
+ };
20757
+ this.reads.set(key, read);
20758
+ read.promise = (async () => {
20596
20759
  try {
20597
- await this.world.placeChunk(chunk);
20598
- this.loaded.set(this.key(position), position);
20760
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
20761
+ if (read.chunk) this.validateChunk(read.chunk, position);
20599
20762
  } catch (error) {
20600
- try {
20601
- await this.unloadMany([position]);
20602
- } catch (cleanupError) {
20603
- const failure = error instanceof Error ? error : new Error(String(error));
20604
- try {
20605
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20606
- } catch {
20607
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
20608
- }
20609
- throw failure;
20763
+ read.failed = true;
20764
+ read.error = error;
20765
+ } finally {
20766
+ read.ready = true;
20767
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
20768
+ this.reads.delete(key);
20610
20769
  }
20611
- throw error;
20770
+ }
20771
+ })();
20772
+ }
20773
+ }
20774
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
20775
+ async placeReadyChunks() {
20776
+ this.fillReadSlots();
20777
+ const position = this.desired.find((position2) => this.needsLoad(position2));
20778
+ if (!position) {
20779
+ if (!this.reads.size) return false;
20780
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20781
+ return true;
20782
+ }
20783
+ const head = this.reads.get(this.key(position));
20784
+ if (!head) {
20785
+ for (let index = this.desired.length - 1; index >= 0; index--) {
20786
+ const key = this.key(this.desired[index]);
20787
+ if (this.reads.get(key)?.ready) {
20788
+ this.reads.delete(key);
20789
+ return true;
20612
20790
  }
20613
20791
  }
20614
- } finally {
20615
- this.running = void 0;
20792
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
20793
+ return true;
20794
+ }
20795
+ if (!head.ready) {
20796
+ await head.promise;
20797
+ return true;
20798
+ }
20799
+ const chunks = [], positions = [];
20800
+ for (const desired of this.desired) {
20801
+ if (!this.needsLoad(desired)) continue;
20802
+ const key = this.key(desired), read = this.reads.get(key);
20803
+ if (!read?.ready) break;
20804
+ this.reads.delete(key);
20805
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
20806
+ else if (!read.chunk) this.missing.set(key, desired);
20807
+ else {
20808
+ chunks.push(read.chunk);
20809
+ positions.push(desired);
20810
+ }
20811
+ }
20812
+ if (!chunks.length) return true;
20813
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
20814
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
20815
+ return true;
20816
+ }
20817
+ async placeOwnedColumns(positions, place) {
20818
+ try {
20819
+ await place();
20820
+ for (const position of positions) this.loaded.set(this.key(position), position);
20821
+ } catch (error) {
20822
+ try {
20823
+ await this.unloadMany(positions);
20824
+ } catch (cleanupError) {
20825
+ const failure = error instanceof Error ? error : new Error(String(error));
20826
+ try {
20827
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20828
+ } catch {
20829
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
20830
+ }
20831
+ throw failure;
20832
+ }
20833
+ throw error;
20616
20834
  }
20617
20835
  }
20618
20836
  needsLoad(position) {