minerender 2.1.0-alpha.137 → 2.1.0-alpha.139

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.
@@ -2493,11 +2493,21 @@ interface Palette {
2493
2493
  };
2494
2494
  }
2495
2495
  interface PaletteEntry {
2496
- Name: {
2496
+ Name?: {
2497
2497
  type: "string";
2498
2498
  value: string;
2499
2499
  };
2500
- Properties: {
2500
+ id?: {
2501
+ type: "string";
2502
+ value: string;
2503
+ };
2504
+ Properties?: {
2505
+ type: "compound";
2506
+ value: {
2507
+ [property: string]: PalettePropertyValue;
2508
+ };
2509
+ };
2510
+ properties?: {
2501
2511
  type: "compound";
2502
2512
  value: {
2503
2513
  [property: string]: PalettePropertyValue;
@@ -4068,7 +4078,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4068
4078
  private ambientOcclusionCells?;
4069
4079
  private readonly blockChanges;
4070
4080
  private biomeChange?;
4071
- constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number, biomeBlend?: (() => BiomeBlend | undefined) | undefined);
4081
+ constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[], deferCulling?: boolean) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number, biomeBlend?: (() => BiomeBlend | undefined) | undefined);
4072
4082
  /** Finds the nearest owned block hit across sections, testing each shared mesh once. */
4073
4083
  static raycast<SectionMeshing extends boolean>(chunks: Iterable<Chunk<SectionMeshing>>, raycaster: Raycaster): WorldBlockIntersection | undefined;
4074
4084
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
@@ -4080,11 +4090,14 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4080
4090
  getBiomeAt(pos: TripleArray): Maybe<string>;
4081
4091
  /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
4082
4092
  setBiomes(biomes?: readonly string[]): Promise<void>;
4083
- /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
4093
+ /**
4094
+ * Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects.
4095
+ * The owning world can defer neighbor culling until all affected chunks are refreshed.
4096
+ */
4084
4097
  refreshBiomes(columns?: readonly {
4085
4098
  x: number;
4086
4099
  z: number;
4087
- }[]): Promise<void>;
4100
+ }[], deferCulling?: boolean): Promise<void>;
4088
4101
  private refreshBiomeBlocks;
4089
4102
  /**
4090
4103
  * Places a block at integer world block coordinates. Pass `undefined` to remove it.
@@ -4095,7 +4108,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4095
4108
  /**
4096
4109
  * Places a block at integer chunk-local coordinates from 0 to 15.
4097
4110
  */
4098
- setBlockInChunkAt(pos: Vector3, block?: Block, worldPos?: Vector3, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined): Promise<Maybe<BlockInfo<SectionMeshing>>>;
4111
+ setBlockInChunkAt(pos: Vector3, block?: Block, worldPos?: Vector3, onBlocksChanged?: ((positions: Vector3[], deferCulling?: boolean) => Promise<void>) | undefined): Promise<Maybe<BlockInfo<SectionMeshing>>>;
4099
4112
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
4100
4113
  placeBlocks(blocks: readonly {
4101
4114
  index: number;
@@ -4260,6 +4273,14 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4260
4273
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
4261
4274
  */
4262
4275
  unloadChunkColumn(x: number, z: number): Promise<void>;
4276
+ /**
4277
+ * Removes chunk columns together and refreshes surviving neighbors once.
4278
+ * Coordinates are absolute chunk columns. Duplicate and missing columns are ignored.
4279
+ */
4280
+ unloadChunkColumns(positions: readonly {
4281
+ x: number;
4282
+ z: number;
4283
+ }[]): Promise<void>;
4263
4284
  private clearChunkColumn;
4264
4285
  private markSectionChanged;
4265
4286
  /** Removes all chunks and releases their blocks, section meshes, and owned entities. */
@@ -4337,7 +4358,7 @@ declare class WorldStreamer {
4337
4358
  private running?;
4338
4359
  private disposal?;
4339
4360
  private disposed;
4340
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumn">, source: WorldChunkSource, options?: WorldStreamerOptions);
4361
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4341
4362
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4342
4363
  get loadedChunks(): readonly {
4343
4364
  x: number;
@@ -4371,7 +4392,7 @@ declare class WorldStreamer {
4371
4392
  dispose(): Promise<void>;
4372
4393
  private drain;
4373
4394
  private needsLoad;
4374
- private unload;
4395
+ private unloadMany;
4375
4396
  private validateChunk;
4376
4397
  private within;
4377
4398
  private key;
@@ -2493,11 +2493,21 @@ interface Palette {
2493
2493
  };
2494
2494
  }
2495
2495
  interface PaletteEntry {
2496
- Name: {
2496
+ Name?: {
2497
2497
  type: "string";
2498
2498
  value: string;
2499
2499
  };
2500
- Properties: {
2500
+ id?: {
2501
+ type: "string";
2502
+ value: string;
2503
+ };
2504
+ Properties?: {
2505
+ type: "compound";
2506
+ value: {
2507
+ [property: string]: PalettePropertyValue;
2508
+ };
2509
+ };
2510
+ properties?: {
2501
2511
  type: "compound";
2502
2512
  value: {
2503
2513
  [property: string]: PalettePropertyValue;
@@ -4068,7 +4078,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4068
4078
  private ambientOcclusionCells?;
4069
4079
  private readonly blockChanges;
4070
4080
  private biomeChange?;
4071
- constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number, biomeBlend?: (() => BiomeBlend | undefined) | undefined);
4081
+ constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[], deferCulling?: boolean) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number, biomeBlend?: (() => BiomeBlend | undefined) | undefined);
4072
4082
  /** Finds the nearest owned block hit across sections, testing each shared mesh once. */
4073
4083
  static raycast<SectionMeshing extends boolean>(chunks: Iterable<Chunk<SectionMeshing>>, raycaster: Raycaster): WorldBlockIntersection | undefined;
4074
4084
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
@@ -4080,11 +4090,14 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4080
4090
  getBiomeAt(pos: TripleArray): Maybe<string>;
4081
4091
  /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
4082
4092
  setBiomes(biomes?: readonly string[]): Promise<void>;
4083
- /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
4093
+ /**
4094
+ * Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects.
4095
+ * The owning world can defer neighbor culling until all affected chunks are refreshed.
4096
+ */
4084
4097
  refreshBiomes(columns?: readonly {
4085
4098
  x: number;
4086
4099
  z: number;
4087
- }[]): Promise<void>;
4100
+ }[], deferCulling?: boolean): Promise<void>;
4088
4101
  private refreshBiomeBlocks;
4089
4102
  /**
4090
4103
  * Places a block at integer world block coordinates. Pass `undefined` to remove it.
@@ -4095,7 +4108,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4095
4108
  /**
4096
4109
  * Places a block at integer chunk-local coordinates from 0 to 15.
4097
4110
  */
4098
- setBlockInChunkAt(pos: Vector3, block?: Block, worldPos?: Vector3, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined): Promise<Maybe<BlockInfo<SectionMeshing>>>;
4111
+ setBlockInChunkAt(pos: Vector3, block?: Block, worldPos?: Vector3, onBlocksChanged?: ((positions: Vector3[], deferCulling?: boolean) => Promise<void>) | undefined): Promise<Maybe<BlockInfo<SectionMeshing>>>;
4099
4112
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
4100
4113
  placeBlocks(blocks: readonly {
4101
4114
  index: number;
@@ -4260,6 +4273,14 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4260
4273
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
4261
4274
  */
4262
4275
  unloadChunkColumn(x: number, z: number): Promise<void>;
4276
+ /**
4277
+ * Removes chunk columns together and refreshes surviving neighbors once.
4278
+ * Coordinates are absolute chunk columns. Duplicate and missing columns are ignored.
4279
+ */
4280
+ unloadChunkColumns(positions: readonly {
4281
+ x: number;
4282
+ z: number;
4283
+ }[]): Promise<void>;
4263
4284
  private clearChunkColumn;
4264
4285
  private markSectionChanged;
4265
4286
  /** Removes all chunks and releases their blocks, section meshes, and owned entities. */
@@ -4337,7 +4358,7 @@ declare class WorldStreamer {
4337
4358
  private running?;
4338
4359
  private disposal?;
4339
4360
  private disposed;
4340
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumn">, source: WorldChunkSource, options?: WorldStreamerOptions);
4361
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4341
4362
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4342
4363
  get loadedChunks(): readonly {
4343
4364
  x: number;
@@ -4371,7 +4392,7 @@ declare class WorldStreamer {
4371
4392
  dispose(): Promise<void>;
4372
4393
  private drain;
4373
4394
  private needsLoad;
4374
- private unload;
4395
+ private unloadMany;
4375
4396
  private validateChunk;
4376
4397
  private within;
4377
4398
  private key;
@@ -13974,13 +13974,16 @@ var StructureParser = class {
13974
13974
  const blocks = nbt.blocks.value.value.map((block) => {
13975
13975
  const state = palette.value.value[block.state.value];
13976
13976
  if (!state) throw new MineRenderError(`Structure palette index ${block.state.value} does not exist`);
13977
- if (["minecraft:air", "minecraft:cave_air", "minecraft:void_air"].includes(state.Name.value)) return void 0;
13977
+ const name = (state.Name ?? state.id)?.value;
13978
+ if (!name) throw new MineRenderError(`Structure palette entry ${block.state.value} has no block name`);
13979
+ if (["minecraft:air", "minecraft:cave_air", "minecraft:void_air"].includes(name)) return void 0;
13978
13980
  const props = {};
13979
- for (let k in state.Properties?.value) {
13980
- props[k] = state.Properties.value[k].value;
13981
+ const properties = (state.Properties ?? state.properties)?.value;
13982
+ for (let k in properties) {
13983
+ props[k] = properties[k].value;
13981
13984
  }
13982
13985
  return {
13983
- type: state.Name.value,
13986
+ type: name,
13984
13987
  properties: props,
13985
13988
  position: block.pos.value.value,
13986
13989
  nbt: block.nbt
@@ -18711,8 +18714,14 @@ var MineRenderWorld = class {
18711
18714
  }
18712
18715
  return queued;
18713
18716
  }
18714
- async refreshBiomes() {
18715
- if (this.biomeRefreshing) await this.biomeRefreshing;
18717
+ async refreshBiomes(deferCulling = false) {
18718
+ if (this.biomeRefreshing) {
18719
+ await this.biomeRefreshing;
18720
+ if (!deferCulling) {
18721
+ if (this.pendingCulling.size) await this.updateCulling([]);
18722
+ else await this.culling;
18723
+ }
18724
+ }
18716
18725
  if (!this.pendingBiomes.size) return /* @__PURE__ */ new Map();
18717
18726
  return this.biomeRefreshing ??= Promise.resolve().then(async () => {
18718
18727
  const failures = /* @__PURE__ */ new Map();
@@ -18724,7 +18733,7 @@ var MineRenderWorld = class {
18724
18733
  this.pendingBiomes.delete(chunk);
18725
18734
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) !== chunk) continue;
18726
18735
  try {
18727
- await chunk.refreshBiomes(columns);
18736
+ await chunk.refreshBiomes(columns, deferCulling);
18728
18737
  } catch (error) {
18729
18738
  failures.set(chunk, error);
18730
18739
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
@@ -18986,15 +18995,35 @@ var MineRenderWorld = class {
18986
18995
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
18987
18996
  */
18988
18997
  async unloadChunkColumn(x, z) {
18989
- if (!Number.isInteger(x) || !Number.isInteger(z)) {
18990
- throw new RangeError("Chunk column coordinates must be integers");
18998
+ return this.unloadChunkColumns([{ x, z }]);
18999
+ }
19000
+ /**
19001
+ * Removes chunk columns together and refreshes surviving neighbors once.
19002
+ * Coordinates are absolute chunk columns. Duplicate and missing columns are ignored.
19003
+ */
19004
+ async unloadChunkColumns(positions) {
19005
+ const columns = /* @__PURE__ */ new Map();
19006
+ for (const position of positions) {
19007
+ const { x, z } = position ?? {};
19008
+ if (!Number.isInteger(x) || !Number.isInteger(z)) {
19009
+ throw new RangeError("Chunk column coordinates must be integers");
19010
+ }
19011
+ columns.set(`${x}_${z}`, { x, z });
18991
19012
  }
18992
- this.entities?.clearColumn(x, z);
19013
+ if (!columns.size) return;
18993
19014
  await this.culling;
19015
+ let sliceStart = performance.now();
18994
19016
  try {
18995
- await this.clearChunkColumn(x, z);
19017
+ for (const { x, z } of columns.values()) {
19018
+ this.entities?.clearColumn(x, z);
19019
+ await this.clearChunkColumn(x, z);
19020
+ if (performance.now() - sliceStart > 8) {
19021
+ await yieldToEventLoop();
19022
+ sliceStart = performance.now();
19023
+ }
19024
+ }
18996
19025
  } finally {
18997
- const failures = await this.refreshBiomes();
19026
+ const failures = await this.refreshBiomes(true);
18998
19027
  if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18999
19028
  await this.updateCulling([]);
19000
19029
  }
@@ -19057,7 +19086,7 @@ var MineRenderWorld = class {
19057
19086
  }
19058
19087
  this.sectionModels?.clear();
19059
19088
  }
19060
- updateCulling(positions) {
19089
+ updateCulling(positions, defer = false) {
19061
19090
  const neighbor = new import_three45.Vector3();
19062
19091
  for (const pos of positions) {
19063
19092
  const cx = Math.floor(pos.x / 16), cy = Math.floor(pos.y / 16), cz = Math.floor(pos.z / 16);
@@ -19079,58 +19108,72 @@ var MineRenderWorld = class {
19079
19108
  }
19080
19109
  }
19081
19110
  }
19111
+ if (defer) return Promise.resolve();
19082
19112
  return this.culling ??= Promise.resolve().then(async () => {
19083
19113
  let sliceStart = performance.now();
19084
19114
  try {
19085
19115
  const culling = await loadBlockCulling();
19086
19116
  while (this.pendingCulling.size) {
19087
- const batch = [...this.pendingCulling];
19117
+ const batch = new Map(this.pendingCulling);
19088
19118
  this.pendingCulling.clear();
19089
- for (const [chunk, indices] of batch) {
19090
- const neighbors = CUBE_FACE_OFFSETS.map(([x, y, z]) => this._chunks.get(`${chunk.x + x}_${chunk.y + y}_${chunk.z + z}`));
19091
- const neighborAt = (x, y, z) => {
19092
- const lx = x - chunk.x * 16, ly = y - chunk.y * 16, lz = z - chunk.z * 16;
19093
- const section = neighbors[lx >= 16 ? 0 : lx < 0 ? 1 : ly >= 16 ? 2 : ly < 0 ? 3 : lz >= 16 ? 4 : 5];
19094
- return [section, (ly & 15) * 256 + (lz & 15) * 16 + (lx & 15)];
19095
- };
19096
- const masks = chunk.updateCullMasks(indices === "all" ? void 0 : indices, (x, y, z) => {
19097
- const [section, i] = neighborAt(x, y, z);
19098
- return section?.isOccludingIndex(i) ?? false;
19099
- }, culling, (x, y, z) => {
19100
- const [section, i] = neighborAt(x, y, z);
19101
- return section?.visibleBlockIndex(i);
19102
- });
19103
- const fluidIndices = indices === "all" ? function* () {
19104
- for (let index = 0; index < 4096; index++) yield index;
19105
- }() : indices;
19106
- for (const index of fluidIndices) {
19107
- if (!chunk.fluidByteIndex(index) || !chunk.isBlockVisibleIndex(index)) continue;
19108
- const lx = index % 16, ly = Math.floor(index / 256), lz = Math.floor(index / 16) % 16;
19109
- const wx = chunk.x * 16 + lx, wy = chunk.y * 16 + ly, wz = chunk.z * 16 + lz;
19110
- const block = chunk.getBlockAt(neighbor.set(wx, wy, wz));
19111
- if (block.object?.fluidKind) {
19112
- await block.object.updateFluid((x, y, z) => {
19113
- const nx = lx + x, ny = ly + y, nz = lz + z;
19114
- const section = nx >= 0 && nx < 16 && ny >= 0 && ny < 16 && nz >= 0 && nz < 16 ? chunk : this._chunks.get(`${chunk.x + Math.floor(nx / 16)}_${chunk.y + Math.floor(ny / 16)}_${chunk.z + Math.floor(nz / 16)}`);
19115
- const neighborIndex = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
19116
- const byte = section?.fluidByteIndex(neighborIndex) ?? 0;
19117
- return {
19118
- fluid: fluidKindOf(byte),
19119
- level: byte & 15,
19120
- solid: section?.isOccludingIndex(neighborIndex) ?? false
19121
- };
19122
- });
19119
+ try {
19120
+ for (const [chunk, indices] of batch) {
19121
+ const neighbors = CUBE_FACE_OFFSETS.map(([x, y, z]) => this._chunks.get(`${chunk.x + x}_${chunk.y + y}_${chunk.z + z}`));
19122
+ const neighborAt = (x, y, z) => {
19123
+ const lx = x - chunk.x * 16, ly = y - chunk.y * 16, lz = z - chunk.z * 16;
19124
+ const section = neighbors[lx >= 16 ? 0 : lx < 0 ? 1 : ly >= 16 ? 2 : ly < 0 ? 3 : lz >= 16 ? 4 : 5];
19125
+ return [section, (ly & 15) * 256 + (lz & 15) * 16 + (lx & 15)];
19126
+ };
19127
+ const masks = chunk.updateCullMasks(indices === "all" ? void 0 : indices, (x, y, z) => {
19128
+ const [section, i] = neighborAt(x, y, z);
19129
+ return section?.isOccludingIndex(i) ?? false;
19130
+ }, culling, (x, y, z) => {
19131
+ const [section, i] = neighborAt(x, y, z);
19132
+ return section?.visibleBlockIndex(i);
19133
+ });
19134
+ const fluidIndices = indices === "all" ? function* () {
19135
+ for (let index = 0; index < 4096; index++) yield index;
19136
+ }() : indices;
19137
+ for (const index of fluidIndices) {
19138
+ if (!chunk.fluidByteIndex(index) || !chunk.isBlockVisibleIndex(index)) continue;
19139
+ const lx = index % 16, ly = Math.floor(index / 256), lz = Math.floor(index / 16) % 16;
19140
+ const wx = chunk.x * 16 + lx, wy = chunk.y * 16 + ly, wz = chunk.z * 16 + lz;
19141
+ const block = chunk.getBlockAt(neighbor.set(wx, wy, wz));
19142
+ if (block.object?.fluidKind) {
19143
+ await block.object.updateFluid((x, y, z) => {
19144
+ const nx = lx + x, ny = ly + y, nz = lz + z;
19145
+ const section = nx >= 0 && nx < 16 && ny >= 0 && ny < 16 && nz >= 0 && nz < 16 ? chunk : this._chunks.get(`${chunk.x + Math.floor(nx / 16)}_${chunk.y + Math.floor(ny / 16)}_${chunk.z + Math.floor(nz / 16)}`);
19146
+ const neighborIndex = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
19147
+ const byte = section?.fluidByteIndex(neighborIndex) ?? 0;
19148
+ return {
19149
+ fluid: fluidKindOf(byte),
19150
+ level: byte & 15,
19151
+ solid: section?.isOccludingIndex(neighborIndex) ?? false
19152
+ };
19153
+ });
19154
+ }
19155
+ }
19156
+ for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19157
+ await chunk.refreshLight(indices === "all" ? void 0 : indices);
19158
+ chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19159
+ if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19160
+ await yieldToEventLoop();
19161
+ sliceStart = performance.now();
19123
19162
  }
19124
19163
  }
19125
- for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19126
- await chunk.refreshLight(indices === "all" ? void 0 : indices);
19127
- chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19128
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19129
- await yieldToEventLoop();
19130
- sliceStart = performance.now();
19164
+ const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19165
+ await chunk.rebuildSectionMesh();
19166
+ batch.delete(chunk);
19167
+ }));
19168
+ const failure = builds.find((result) => result.status === "rejected");
19169
+ if (failure?.status === "rejected") throw failure.reason;
19170
+ } catch (error) {
19171
+ for (const [chunk, indices] of batch) {
19172
+ const pending = this.pendingCulling.get(chunk);
19173
+ this.pendingCulling.set(chunk, indices === "all" || pending === "all" ? "all" : /* @__PURE__ */ new Set([...indices, ...pending ?? []]));
19131
19174
  }
19175
+ throw error;
19132
19176
  }
19133
- await Promise.all(batch.map(([chunk]) => chunk.rebuildSectionMesh()));
19134
19177
  }
19135
19178
  } finally {
19136
19179
  this.culling = void 0;
@@ -19147,7 +19190,7 @@ var MineRenderWorld = class {
19147
19190
  x,
19148
19191
  y,
19149
19192
  z,
19150
- (positions) => this.updateCulling(positions),
19193
+ (positions, defer) => this.updateCulling(positions, defer),
19151
19194
  this.sectionModels,
19152
19195
  (x2, y2, z2) => {
19153
19196
  const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
@@ -19882,11 +19925,14 @@ var Chunk = class _Chunk {
19882
19925
  this.biomes = copy;
19883
19926
  return this.refreshBiomes();
19884
19927
  }
19885
- /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
19886
- refreshBiomes(columns) {
19887
- return this.changeBlocks(() => this.refreshBiomeBlocks(columns), true);
19928
+ /**
19929
+ * Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects.
19930
+ * The owning world can defer neighbor culling until all affected chunks are refreshed.
19931
+ */
19932
+ refreshBiomes(columns, deferCulling = false) {
19933
+ return this.changeBlocks(() => this.refreshBiomeBlocks(columns, deferCulling), true);
19888
19934
  }
19889
- async refreshBiomeBlocks(columns) {
19935
+ async refreshBiomeBlocks(columns, deferCulling = false) {
19890
19936
  const blend = columns && this.biomeBlend?.();
19891
19937
  const ownColumn = columns?.some((column) => column.x === this.x && column.z === this.z);
19892
19938
  const affected = /* @__PURE__ */ new Map();
@@ -19943,7 +19989,7 @@ var Chunk = class _Chunk {
19943
19989
  this.renderedBlocks.get(index)?.object?.setVisible(false);
19944
19990
  }
19945
19991
  const positions = blocks.map(({ index }) => new import_three47.Vector3(...this.positionAt(index)));
19946
- if (this.onBlocksChanged) await this.onBlocksChanged(positions);
19992
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions, deferCulling);
19947
19993
  else await this.rebuildSectionMesh();
19948
19994
  }
19949
19995
  }
@@ -20597,19 +20643,12 @@ var WorldStreamer = class {
20597
20643
  this.loading?.controller.abort();
20598
20644
  return this.disposal ??= Promise.resolve().then(async () => {
20599
20645
  await this.running?.catch(() => void 0);
20600
- let failure;
20601
- let failed = false;
20602
- for (const position of this.owned.values()) {
20603
- try {
20604
- await this.unload(position);
20605
- } catch (error) {
20606
- failed = true;
20607
- failure ??= error;
20608
- }
20646
+ try {
20647
+ await this.unloadMany([...this.owned.values()]);
20648
+ } finally {
20649
+ this.missing.clear();
20650
+ this.failed.clear();
20609
20651
  }
20610
- this.missing.clear();
20611
- this.failed.clear();
20612
- if (failed) throw failure;
20613
20652
  }).catch((error) => {
20614
20653
  this.disposal = void 0;
20615
20654
  throw error;
@@ -20624,9 +20663,9 @@ var WorldStreamer = class {
20624
20663
  for (const [key, position2] of this.failed) {
20625
20664
  if (!this.within(position2, this.unloadRadius)) this.failed.delete(key);
20626
20665
  }
20627
- const obsolete = [...this.owned.values()].find((position2) => !this.loaded.has(this.key(position2)) || !this.within(position2, this.unloadRadius));
20628
- if (obsolete) {
20629
- await this.unload(obsolete);
20666
+ const obsolete = [...this.owned.values()].filter((position2) => !this.loaded.has(this.key(position2)) || !this.within(position2, this.unloadRadius));
20667
+ if (obsolete.length) {
20668
+ await this.unloadMany(obsolete);
20630
20669
  continue;
20631
20670
  }
20632
20671
  const position = this.desired.find((position2) => this.needsLoad(position2));
@@ -20656,7 +20695,7 @@ var WorldStreamer = class {
20656
20695
  this.loaded.set(this.key(position), position);
20657
20696
  } catch (error) {
20658
20697
  try {
20659
- await this.unload(position);
20698
+ await this.unloadMany([position]);
20660
20699
  } catch (cleanupError) {
20661
20700
  const failure = error instanceof Error ? error : new Error(String(error));
20662
20701
  try {
@@ -20677,10 +20716,18 @@ var WorldStreamer = class {
20677
20716
  const key = this.key(position);
20678
20717
  return !this.loaded.has(key) && !this.missing.has(key) && !this.failed.has(key);
20679
20718
  }
20680
- async unload(position) {
20681
- await this.world.unloadChunkColumn(position.x, position.z);
20682
- this.owned.delete(this.key(position));
20683
- this.loaded.delete(this.key(position));
20719
+ async unloadMany(positions) {
20720
+ if (!positions.length) return;
20721
+ try {
20722
+ await this.world.unloadChunkColumns(positions.map(({ x, z }) => ({ x, z })));
20723
+ } catch (error) {
20724
+ for (const position of positions) this.loaded.delete(this.key(position));
20725
+ throw error;
20726
+ }
20727
+ for (const position of positions) {
20728
+ this.owned.delete(this.key(position));
20729
+ this.loaded.delete(this.key(position));
20730
+ }
20684
20731
  }
20685
20732
  validateChunk(chunk, position) {
20686
20733
  if (chunk.x !== position.x || chunk.z !== position.z) {